diff --git a/.gitignore b/.gitignore index 5657f6e..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +0,0 @@ -vendor \ No newline at end of file diff --git a/composer.json b/composer.json index da3fb5a..ccebaaf 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,8 @@ { "require": { "slim/slim": "^3.12" + }, + "require-dev": { + "robmorgan/phinx": "^0.12.9" } } diff --git a/composer.lock b/composer.lock index 150de6a..dec95f5 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": "a200524ba4252697eaecc09aadda13fd", + "content-hash": "1ee13aac746df8331fd8b0c724cdb261", "packages": [ { "name": "nikic/fast-route", @@ -288,7 +288,1470 @@ "time": "2019-11-28T17:40:33+00:00" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "cakephp/core", + "version": "4.3.3", + "source": { + "type": "git", + "url": "https://github.com/cakephp/core.git", + "reference": "d80bcd1d94e1e90741ad7dbae882db8711239d07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/core/zipball/d80bcd1d94e1e90741ad7dbae882db8711239d07", + "reference": "d80bcd1d94e1e90741ad7dbae882db8711239d07", + "shasum": "" + }, + "require": { + "cakephp/utility": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "cakephp/cache": "To use Configure::store() and restore().", + "cakephp/event": "To use PluginApplicationInterface or plugin applications.", + "league/container": "To use Container and ServiceProvider classes" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Core\\": "." + }, + "files": [ + "functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/core/graphs/contributors" + } + ], + "description": "CakePHP Framework Core classes", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "core", + "framework" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/core" + }, + "time": "2021-12-17T14:28:04+00:00" + }, + { + "name": "cakephp/database", + "version": "4.3.3", + "source": { + "type": "git", + "url": "https://github.com/cakephp/database.git", + "reference": "02197e9b1517ecdea546b8e7780682b34c967d3f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/database/zipball/02197e9b1517ecdea546b8e7780682b34c967d3f", + "reference": "02197e9b1517ecdea546b8e7780682b34c967d3f", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "cakephp/datasource": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "cakephp/i18n": "If you are using locale-aware datetime formats or Chronos types." + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Database\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/database/graphs/contributors" + } + ], + "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API", + "homepage": "https://cakephp.org", + "keywords": [ + "abstraction", + "cakephp", + "database", + "database abstraction", + "pdo" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/database" + }, + "time": "2021-12-17T14:28:04+00:00" + }, + { + "name": "cakephp/datasource", + "version": "4.3.3", + "source": { + "type": "git", + "url": "https://github.com/cakephp/datasource.git", + "reference": "efd2a22555d982c2ab129c5e9e053971ed606624" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/datasource/zipball/efd2a22555d982c2ab129c5e9e053971ed606624", + "reference": "efd2a22555d982c2ab129c5e9e053971ed606624", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "php": ">=7.2.0", + "psr/log": "^1.1", + "psr/simple-cache": "^1.0" + }, + "suggest": { + "cakephp/cache": "If you decide to use Query caching.", + "cakephp/collection": "If you decide to use ResultSetInterface.", + "cakephp/utility": "If you decide to use EntityTrait." + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Datasource\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/datasource/graphs/contributors" + } + ], + "description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "connection management", + "datasource", + "entity", + "query" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/datasource" + }, + "time": "2021-12-17T14:28:04+00:00" + }, + { + "name": "cakephp/utility", + "version": "4.3.3", + "source": { + "type": "git", + "url": "https://github.com/cakephp/utility.git", + "reference": "00410267c41036e20a43fbf0cb2ddedbd893a8a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/utility/zipball/00410267c41036e20a43fbf0cb2ddedbd893a8a5", + "reference": "00410267c41036e20a43fbf0cb2ddedbd893a8a5", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()", + "lib-ICU": "To use Text::transliterate() or Text::slug()" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Utility\\": "." + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/utility/graphs/contributors" + } + ], + "description": "CakePHP Utility classes such as Inflector, String, Hash, and Security", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "hash", + "inflector", + "security", + "string", + "utility" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/utility" + }, + "time": "2021-12-17T14:28:04+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "robmorgan/phinx", + "version": "0.12.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/phinx.git", + "reference": "5a0146a74c1bc195d1f5da86afa3b68badf7d90e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/phinx/zipball/5a0146a74c1bc195d1f5da86afa3b68badf7d90e", + "reference": "5a0146a74c1bc195d1f5da86afa3b68badf7d90e", + "shasum": "" + }, + "require": { + "cakephp/database": "^4.0", + "php": ">=7.2", + "psr/container": "^1.0 || ^2.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^4.0", + "ext-json": "*", + "ext-pdo": "*", + "phpunit/phpunit": "^8.5|^9.3", + "sebastian/comparator": ">=1.2.3", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "ext-json": "Install if using JSON configuration format", + "ext-pdo": "PDO extension is needed", + "symfony/yaml": "Install if using YAML configuration format" + }, + "bin": [ + "bin/phinx" + ], + "type": "library", + "autoload": { + "psr-4": { + "Phinx\\": "src/Phinx/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Morgan", + "email": "robbym@gmail.com", + "homepage": "https://robmorgan.id.au", + "role": "Lead Developer" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://shadowhand.me", + "role": "Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Developer" + }, + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/phinx/graphs/contributors", + "role": "Developer" + } + ], + "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.", + "homepage": "https://phinx.org", + "keywords": [ + "database", + "database migrations", + "db", + "migrations", + "phinx" + ], + "support": { + "issues": "https://github.com/cakephp/phinx/issues", + "source": "https://github.com/cakephp/phinx/tree/0.12.9" + }, + "time": "2021-10-12T10:49:25+00:00" + }, + { + "name": "symfony/config", + "version": "v5.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "2e082dae50da563c639119b7b52347a2a3db4ba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/2e082dae50da563c639119b7b52347a2a3db4ba5", + "reference": "2e082dae50da563c639119b7b52347a2a3db4ba5", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" + }, + "conflict": { + "symfony/finder": "<4.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v5.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-12-15T11:06:13+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a2c6b7ced2eb7799a35375fb9022519282b5405e", + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-12-20T16:11:12+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/c726b64c1ccfe2896cb7df2e1331c357ad1c8ced", + "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced", + "shasum": "" + }, + "require": { + "php": ">=8.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-01T23:48:49+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "52b3c9cce673b014915445a432339f282e002ce6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/52b3c9cce673b014915445a432339f282e002ce6", + "reference": "52b3c9cce673b014915445a432339f282e002ce6", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-29T07:35:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-23T21:10:46+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-06-05T21:20:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", + "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-13T13:58:33+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-13T13:58:11+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d664541b99d6fb0247ec5ff32e87238582236204" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d664541b99d6fb0247ec5ff32e87238582236204", + "reference": "d664541b99d6fb0247ec5ff32e87238582236204", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-04T16:37:19+00:00" + }, + { + "name": "symfony/string", + "version": "v6.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "bae261d0c3ac38a1f802b4dfed42094296100631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/bae261d0c3ac38a1f802b4dfed42094296100631", + "reference": "bae261d0c3ac38a1f802b4dfed42094296100631", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-12-16T22:13:01+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], diff --git a/db/migrations/20220115023659_product.php b/db/migrations/20220115023659_product.php new file mode 100644 index 0000000..95afecb --- /dev/null +++ b/db/migrations/20220115023659_product.php @@ -0,0 +1,30 @@ +table('product', array('id' => 'id')); + + // Create table structure + $product->addColumn('name', 'string', ['limit' => 64]) + ->addColumn('price', 'string', ['limit' => 64]) + ->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } +} diff --git a/db/seeds/Product.php b/db/seeds/Product.php new file mode 100644 index 0000000..abe8976 --- /dev/null +++ b/db/seeds/Product.php @@ -0,0 +1,44 @@ + 'AROON 2.1 BLACK XS', + 'price' => '349000' + ), + array( + 'name' => 'BLACK PLEATS JOGGER S', + 'price' => '499000' + ), + array( + 'name' => 'BOYS DO CRY SHIRT XL', + 'price' => '299000' + ), + array( + 'name' => 'CARGO BLUE XL', + 'price' => '499000' + ), + array( + 'name' => 'CARSON BLUE XXL', + 'price' => '399000' + ) + ); + + $product = $this->table('product'); + $product->insert($data)->save(); + } +} diff --git a/phinx.php b/phinx.php new file mode 100644 index 0000000..6e5f0c8 --- /dev/null +++ b/phinx.php @@ -0,0 +1,41 @@ + [ + 'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations', + 'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds' + ], + 'environments' => [ + 'default_migration_table' => 'phinxlog', + 'default_environment' => 'development', + 'production' => [ + 'adapter' => 'mysql', + 'host' => 'localhost', + 'name' => 'production_db', + 'user' => 'root', + 'pass' => '', + 'port' => '3306', + 'charset' => 'utf8', + ], + 'development' => [ + 'adapter' => 'mysql', + 'host' => 'localhost', + 'name' => 'product_data', + 'user' => 'root', + 'pass' => 'root', + 'port' => '3306', + 'charset' => 'utf8', + ], + 'testing' => [ + 'adapter' => 'mysql', + 'host' => 'localhost', + 'name' => 'testing_db', + 'user' => 'root', + 'pass' => '', + 'port' => '3306', + 'charset' => 'utf8', + ] + ], + 'version_order' => 'creation' +]; diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..c26bca3 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) { + include("phpvfscomposer://" . __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx'); + exit(0); + } +} + +include __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx'; diff --git a/vendor/bin/phinx.bat b/vendor/bin/phinx.bat new file mode 100644 index 0000000..48561bf --- /dev/null +++ b/vendor/bin/phinx.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/phinx +SET COMPOSER_BIN_DIR=%~dp0 +php "%BIN_TARGET%" %* diff --git a/vendor/cakephp/core/App.php b/vendor/cakephp/core/App.php new file mode 100644 index 0000000..5d7f1f0 --- /dev/null +++ b/vendor/cakephp/core/App.php @@ -0,0 +1,266 @@ + + * @link https://book.cakephp.org/4/en/core-libraries/app.html#finding-paths-to-namespaces + */ + public static function path(string $type, ?string $plugin = null): array + { + if ($plugin === null && $type[0] === strtolower($type[0])) { + return (array)Configure::read('App.paths.' . $type); + } + + if ($type === 'templates') { + /** @psalm-suppress PossiblyNullArgument */ + return [Plugin::templatePath($plugin)]; + } + + if ($type === 'locales') { + /** @psalm-suppress PossiblyNullArgument */ + return [Plugin::path($plugin) . 'resources' . DIRECTORY_SEPARATOR . 'locales' . DIRECTORY_SEPARATOR]; + } + + deprecationWarning( + 'App::path() is deprecated for class path.' + . ' Use \Cake\Core\App::classPath() or \Cake\Core\Plugin::classPath() instead.' + ); + + return static::classPath($type, $plugin); + } + + /** + * Gets the path to a class type in the application or a plugin. + * + * Example: + * + * ``` + * App::classPath('Model/Table'); + * ``` + * + * Will return the path for tables - e.g. `src/Model/Table/`. + * + * ``` + * App::classPath('Model/Table', 'My/Plugin'); + * ``` + * + * Will return the plugin based path for those. + * + * @param string $type Package type. + * @param string|null $plugin Plugin name. + * @return array + */ + public static function classPath(string $type, ?string $plugin = null): array + { + if ($plugin !== null) { + return [ + Plugin::classPath($plugin) . $type . DIRECTORY_SEPARATOR, + ]; + } + + return [APP . $type . DIRECTORY_SEPARATOR]; + } + + /** + * Returns the full path to a package inside the CakePHP core + * + * Usage: + * + * ``` + * App::core('Cache/Engine'); + * ``` + * + * Will return the full path to the cache engines package. + * + * @param string $type Package type. + * @return array Full path to package + */ + public static function core(string $type): array + { + if ($type === 'templates') { + return [CORE_PATH . 'templates' . DIRECTORY_SEPARATOR]; + } + + return [CAKE . str_replace('/', DIRECTORY_SEPARATOR, $type) . DIRECTORY_SEPARATOR]; + } +} diff --git a/vendor/cakephp/core/BasePlugin.php b/vendor/cakephp/core/BasePlugin.php new file mode 100644 index 0000000..08e5905 --- /dev/null +++ b/vendor/cakephp/core/BasePlugin.php @@ -0,0 +1,305 @@ + $options Options + */ + public function __construct(array $options = []) + { + foreach (static::VALID_HOOKS as $key) { + if (isset($options[$key])) { + $this->{"{$key}Enabled"} = (bool)$options[$key]; + } + } + foreach (['name', 'path', 'classPath', 'configPath', 'templatePath'] as $path) { + if (isset($options[$path])) { + $this->{$path} = $options[$path]; + } + } + + $this->initialize(); + } + + /** + * Initialization hook called from constructor. + * + * @return void + */ + public function initialize(): void + { + } + + /** + * @inheritDoc + */ + public function getName(): string + { + if ($this->name) { + return $this->name; + } + $parts = explode('\\', static::class); + array_pop($parts); + $this->name = implode('/', $parts); + + return $this->name; + } + + /** + * @inheritDoc + */ + public function getPath(): string + { + if ($this->path) { + return $this->path; + } + $reflection = new ReflectionClass($this); + $path = dirname($reflection->getFileName()); + + // Trim off src + if (substr($path, -3) === 'src') { + $path = substr($path, 0, -3); + } + $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + + return $this->path; + } + + /** + * @inheritDoc + */ + public function getConfigPath(): string + { + if ($this->configPath) { + return $this->configPath; + } + $path = $this->getPath(); + + return $path . 'config' . DIRECTORY_SEPARATOR; + } + + /** + * @inheritDoc + */ + public function getClassPath(): string + { + if ($this->classPath) { + return $this->classPath; + } + $path = $this->getPath(); + + return $path . 'src' . DIRECTORY_SEPARATOR; + } + + /** + * @inheritDoc + */ + public function getTemplatePath(): string + { + if ($this->templatePath) { + return $this->templatePath; + } + $path = $this->getPath(); + + return $this->templatePath = $path . 'templates' . DIRECTORY_SEPARATOR; + } + + /** + * @inheritDoc + */ + public function enable(string $hook) + { + $this->checkHook($hook); + $this->{"{$hook}Enabled}"} = true; + + return $this; + } + + /** + * @inheritDoc + */ + public function disable(string $hook) + { + $this->checkHook($hook); + $this->{"{$hook}Enabled"} = false; + + return $this; + } + + /** + * @inheritDoc + */ + public function isEnabled(string $hook): bool + { + $this->checkHook($hook); + + return $this->{"{$hook}Enabled"} === true; + } + + /** + * Check if a hook name is valid + * + * @param string $hook The hook name to check + * @throws \InvalidArgumentException on invalid hooks + * @return void + */ + protected function checkHook(string $hook): void + { + if (!in_array($hook, static::VALID_HOOKS, true)) { + throw new InvalidArgumentException( + "`$hook` is not a valid hook name. Must be one of " . implode(', ', static::VALID_HOOKS) + ); + } + } + + /** + * @inheritDoc + */ + public function routes(RouteBuilder $routes): void + { + $path = $this->getConfigPath() . 'routes.php'; + if (is_file($path)) { + $return = require $path; + if ($return instanceof Closure) { + $return($routes); + } + } + } + + /** + * @inheritDoc + */ + public function bootstrap(PluginApplicationInterface $app): void + { + $bootstrap = $this->getConfigPath() . 'bootstrap.php'; + if (is_file($bootstrap)) { + require $bootstrap; + } + } + + /** + * @inheritDoc + */ + public function console(CommandCollection $commands): CommandCollection + { + return $commands->addMany($commands->discoverPlugin($this->getName())); + } + + /** + * @inheritDoc + */ + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + return $middlewareQueue; + } + + /** + * Register container services for this plugin. + * + * @param \Cake\Core\ContainerInterface $container The container to add services to. + * @return void + */ + public function services(ContainerInterface $container): void + { + } +} diff --git a/vendor/cakephp/core/ClassLoader.php b/vendor/cakephp/core/ClassLoader.php new file mode 100644 index 0000000..e2a7525 --- /dev/null +++ b/vendor/cakephp/core/ClassLoader.php @@ -0,0 +1,139 @@ + + */ + protected $_prefixes = []; + + /** + * Register loader with SPL autoloader stack. + * + * @return void + */ + public function register(): void + { + /** @var callable $callable */ + $callable = [$this, 'loadClass']; + spl_autoload_register($callable); + } + + /** + * Adds a base directory for a namespace prefix. + * + * @param string $prefix The namespace prefix. + * @param string $baseDir A base directory for class files in the + * namespace. + * @param bool $prepend If true, prepend the base directory to the stack + * instead of appending it; this causes it to be searched first rather + * than last. + * @return void + */ + public function addNamespace(string $prefix, string $baseDir, bool $prepend = false): void + { + $prefix = trim($prefix, '\\') . '\\'; + + $baseDir = rtrim($baseDir, '/') . DIRECTORY_SEPARATOR; + $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/'; + + $this->_prefixes[$prefix] = $this->_prefixes[$prefix] ?? []; + + if ($prepend) { + array_unshift($this->_prefixes[$prefix], $baseDir); + } else { + $this->_prefixes[$prefix][] = $baseDir; + } + } + + /** + * Loads the class file for a given class name. + * + * @param string $class The fully-qualified class name. + * @return string|false The mapped file name on success, or boolean false on + * failure. + */ + public function loadClass(string $class) + { + $prefix = $class; + + while (($pos = strrpos($prefix, '\\')) !== false) { + $prefix = substr($class, 0, $pos + 1); + $relativeClass = substr($class, $pos + 1); + + $mappedFile = $this->_loadMappedFile($prefix, $relativeClass); + if ($mappedFile) { + return $mappedFile; + } + + $prefix = rtrim($prefix, '\\'); + } + + return false; + } + + /** + * Load the mapped file for a namespace prefix and relative class. + * + * @param string $prefix The namespace prefix. + * @param string $relativeClass The relative class name. + * @return string|false Boolean false if no mapped file can be loaded, or the + * name of the mapped file that was loaded. + */ + protected function _loadMappedFile(string $prefix, string $relativeClass) + { + if (!isset($this->_prefixes[$prefix])) { + return false; + } + + foreach ($this->_prefixes[$prefix] as $baseDir) { + $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; + + if ($this->_requireFile($file)) { + return $file; + } + } + + return false; + } + + /** + * If a file exists, require it from the file system. + * + * @param string $file The file to require. + * @return bool True if the file exists, false if not. + */ + protected function _requireFile(string $file): bool + { + if (file_exists($file)) { + require $file; + + return true; + } + + return false; + } +} diff --git a/vendor/cakephp/core/Configure.php b/vendor/cakephp/core/Configure.php new file mode 100644 index 0000000..e4e8f10 --- /dev/null +++ b/vendor/cakephp/core/Configure.php @@ -0,0 +1,498 @@ + false, + ]; + + /** + * Configured engine classes, used to load config files from resources + * + * @see \Cake\Core\Configure::load() + * @var array<\Cake\Core\Configure\ConfigEngineInterface> + */ + protected static $_engines = []; + + /** + * Flag to track whether ini_set exists. + * + * @var bool|null + */ + protected static $_hasIniSet; + + /** + * Used to store a dynamic variable in Configure. + * + * Usage: + * ``` + * Configure::write('One.key1', 'value of the Configure::One[key1]'); + * Configure::write(['One.key1' => 'value of the Configure::One[key1]']); + * Configure::write('One', [ + * 'key1' => 'value of the Configure::One[key1]', + * 'key2' => 'value of the Configure::One[key2]' + * ]); + * + * Configure::write([ + * 'One.key1' => 'value of the Configure::One[key1]', + * 'One.key2' => 'value of the Configure::One[key2]' + * ]); + * ``` + * + * @param array|string $config The key to write, can be a dot notation value. + * Alternatively can be an array containing key(s) and value(s). + * @param mixed $value Value to set for var + * @return void + * @link https://book.cakephp.org/4/en/development/configuration.html#writing-configuration-data + */ + public static function write($config, $value = null): void + { + if (!is_array($config)) { + $config = [$config => $value]; + } + + foreach ($config as $name => $value) { + static::$_values = Hash::insert(static::$_values, $name, $value); + } + + if (isset($config['debug'])) { + if (static::$_hasIniSet === null) { + static::$_hasIniSet = function_exists('ini_set'); + } + if (static::$_hasIniSet) { + ini_set('display_errors', $config['debug'] ? '1' : '0'); + } + } + } + + /** + * Used to read information stored in Configure. It's not + * possible to store `null` values in Configure. + * + * Usage: + * ``` + * Configure::read('Name'); will return all values for Name + * Configure::read('Name.key'); will return only the value of Configure::Name[key] + * ``` + * + * @param string|null $var Variable to obtain. Use '.' to access array elements. + * @param mixed $default The return value when the configure does not exist + * @return mixed Value stored in configure, or null. + * @link https://book.cakephp.org/4/en/development/configuration.html#reading-configuration-data + */ + public static function read(?string $var = null, $default = null) + { + if ($var === null) { + return static::$_values; + } + + return Hash::get(static::$_values, $var, $default); + } + + /** + * Returns true if given variable is set in Configure. + * + * @param string $var Variable name to check for + * @return bool True if variable is there + */ + public static function check(string $var): bool + { + if (empty($var)) { + return false; + } + + return static::read($var) !== null; + } + + /** + * Used to get information stored in Configure. It's not + * possible to store `null` values in Configure. + * + * Acts as a wrapper around Configure::read() and Configure::check(). + * The configure key/value pair fetched via this method is expected to exist. + * In case it does not an exception will be thrown. + * + * Usage: + * ``` + * Configure::readOrFail('Name'); will return all values for Name + * Configure::readOrFail('Name.key'); will return only the value of Configure::Name[key] + * ``` + * + * @param string $var Variable to obtain. Use '.' to access array elements. + * @return mixed Value stored in configure. + * @throws \RuntimeException if the requested configuration is not set. + * @link https://book.cakephp.org/4/en/development/configuration.html#reading-configuration-data + */ + public static function readOrFail(string $var) + { + if (!static::check($var)) { + throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var)); + } + + return static::read($var); + } + + /** + * Used to delete a variable from Configure. + * + * Usage: + * ``` + * Configure::delete('Name'); will delete the entire Configure::Name + * Configure::delete('Name.key'); will delete only the Configure::Name[key] + * ``` + * + * @param string $var the var to be deleted + * @return void + * @link https://book.cakephp.org/4/en/development/configuration.html#deleting-configuration-data + */ + public static function delete(string $var): void + { + static::$_values = Hash::remove(static::$_values, $var); + } + + /** + * Used to consume information stored in Configure. It's not + * possible to store `null` values in Configure. + * + * Acts as a wrapper around Configure::consume() and Configure::check(). + * The configure key/value pair consumed via this method is expected to exist. + * In case it does not an exception will be thrown. + * + * @param string $var Variable to consume. Use '.' to access array elements. + * @return mixed Value stored in configure. + * @throws \RuntimeException if the requested configuration is not set. + * @since 3.6.0 + */ + public static function consumeOrFail(string $var) + { + if (!static::check($var)) { + throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var)); + } + + return static::consume($var); + } + + /** + * Used to read and delete a variable from Configure. + * + * This is primarily used during bootstrapping to move configuration data + * out of configure into the various other classes in CakePHP. + * + * @param string $var The key to read and remove. + * @return array|string|null + */ + public static function consume(string $var) + { + if (strpos($var, '.') === false) { + if (!isset(static::$_values[$var])) { + return null; + } + $value = static::$_values[$var]; + unset(static::$_values[$var]); + + return $value; + } + $value = Hash::get(static::$_values, $var); + static::delete($var); + + return $value; + } + + /** + * Add a new engine to Configure. Engines allow you to read configuration + * files in various formats/storage locations. CakePHP comes with two built-in engines + * PhpConfig and IniConfig. You can also implement your own engine classes in your application. + * + * To add a new engine to Configure: + * + * ``` + * Configure::config('ini', new IniConfig()); + * ``` + * + * @param string $name The name of the engine being configured. This alias is used later to + * read values from a specific engine. + * @param \Cake\Core\Configure\ConfigEngineInterface $engine The engine to append. + * @return void + */ + public static function config(string $name, ConfigEngineInterface $engine): void + { + static::$_engines[$name] = $engine; + } + + /** + * Returns true if the Engine objects is configured. + * + * @param string $name Engine name. + * @return bool + */ + public static function isConfigured(string $name): bool + { + return isset(static::$_engines[$name]); + } + + /** + * Gets the names of the configured Engine objects. + * + * @return array + */ + public static function configured(): array + { + $engines = array_keys(static::$_engines); + + return array_map(function ($key) { + return (string)$key; + }, $engines); + } + + /** + * Remove a configured engine. This will unset the engine + * and make any future attempts to use it cause an Exception. + * + * @param string $name Name of the engine to drop. + * @return bool Success + */ + public static function drop(string $name): bool + { + if (!isset(static::$_engines[$name])) { + return false; + } + unset(static::$_engines[$name]); + + return true; + } + + /** + * Loads stored configuration information from a resource. You can add + * config file resource engines with `Configure::config()`. + * + * Loaded configuration information will be merged with the current + * runtime configuration. You can load configuration files from plugins + * by preceding the filename with the plugin name. + * + * `Configure::load('Users.user', 'default')` + * + * Would load the 'user' config file using the default config engine. You can load + * app config files by giving the name of the resource you want loaded. + * + * ``` + * Configure::load('setup', 'default'); + * ``` + * + * If using `default` config and no engine has been configured for it yet, + * one will be automatically created using PhpConfig + * + * @param string $key name of configuration resource to load. + * @param string $config Name of the configured engine to use to read the resource identified by $key. + * @param bool $merge if config files should be merged instead of simply overridden + * @return bool True if load successful. + * @throws \Cake\Core\Exception\CakeException if the $config engine is not found + * @link https://book.cakephp.org/4/en/development/configuration.html#reading-and-writing-configuration-files + */ + public static function load(string $key, string $config = 'default', bool $merge = true): bool + { + $engine = static::_getEngine($config); + if (!$engine) { + throw new CakeException( + sprintf( + 'Config %s engine not found when attempting to load %s.', + $config, + $key + ) + ); + } + + $values = $engine->read($key); + + if ($merge) { + $values = Hash::merge(static::$_values, $values); + } + + static::write($values); + + return true; + } + + /** + * Dump data currently in Configure into $key. The serialization format + * is decided by the config engine attached as $config. For example, if the + * 'default' adapter is a PhpConfig, the generated file will be a PHP + * configuration file loadable by the PhpConfig. + * + * ### Usage + * + * Given that the 'default' engine is an instance of PhpConfig. + * Save all data in Configure to the file `my_config.php`: + * + * ``` + * Configure::dump('my_config', 'default'); + * ``` + * + * Save only the error handling configuration: + * + * ``` + * Configure::dump('error', 'default', ['Error', 'Exception']; + * ``` + * + * @param string $key The identifier to create in the config adapter. + * This could be a filename or a cache key depending on the adapter being used. + * @param string $config The name of the configured adapter to dump data with. + * @param array $keys The name of the top-level keys you want to dump. + * This allows you save only some data stored in Configure. + * @return bool Success + * @throws \Cake\Core\Exception\CakeException if the adapter does not implement a `dump` method. + */ + public static function dump(string $key, string $config = 'default', array $keys = []): bool + { + $engine = static::_getEngine($config); + if (!$engine) { + throw new CakeException(sprintf('There is no "%s" config engine.', $config)); + } + $values = static::$_values; + if (!empty($keys)) { + $values = array_intersect_key($values, array_flip($keys)); + } + + return $engine->dump($key, $values); + } + + /** + * Get the configured engine. Internally used by `Configure::load()` and `Configure::dump()` + * Will create new PhpConfig for default if not configured yet. + * + * @param string $config The name of the configured adapter + * @return \Cake\Core\Configure\ConfigEngineInterface|null Engine instance or null + */ + protected static function _getEngine(string $config): ?ConfigEngineInterface + { + if (!isset(static::$_engines[$config])) { + if ($config !== 'default') { + return null; + } + static::config($config, new PhpConfig()); + } + + return static::$_engines[$config]; + } + + /** + * Used to determine the current version of CakePHP. + * + * Usage + * ``` + * Configure::version(); + * ``` + * + * @return string Current version of CakePHP + */ + public static function version(): string + { + $version = static::read('Cake.version'); + if ($version !== null) { + return $version; + } + + $path = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'config/config.php'; + if (is_file($path)) { + $config = require $path; + static::write($config); + + return static::read('Cake.version'); + } + + return 'unknown'; + } + + /** + * Used to write runtime configuration into Cache. Stored runtime configuration can be + * restored using `Configure::restore()`. These methods can be used to enable configuration managers + * frontends, or other GUI type interfaces for configuration. + * + * @param string $name The storage name for the saved configuration. + * @param string $cacheConfig The cache configuration to save into. Defaults to 'default' + * @param array|null $data Either an array of data to store, or leave empty to store all values. + * @return bool Success + * @throws \RuntimeException + */ + public static function store(string $name, string $cacheConfig = 'default', ?array $data = null): bool + { + if ($data === null) { + $data = static::$_values; + } + if (!class_exists(Cache::class)) { + throw new RuntimeException('You must install cakephp/cache to use Configure::store()'); + } + + return Cache::write($name, $data, $cacheConfig); + } + + /** + * Restores configuration data stored in the Cache into configure. Restored + * values will overwrite existing ones. + * + * @param string $name Name of the stored config file to load. + * @param string $cacheConfig Name of the Cache configuration to read from. + * @return bool Success. + * @throws \RuntimeException + */ + public static function restore(string $name, string $cacheConfig = 'default'): bool + { + if (!class_exists(Cache::class)) { + throw new RuntimeException('You must install cakephp/cache to use Configure::restore()'); + } + $values = Cache::read($name, $cacheConfig); + if ($values) { + static::write($values); + + return true; + } + + return false; + } + + /** + * Clear all values stored in Configure. + * + * @return void + */ + public static function clear(): void + { + static::$_values = []; + } +} diff --git a/vendor/cakephp/core/Configure/ConfigEngineInterface.php b/vendor/cakephp/core/Configure/ConfigEngineInterface.php new file mode 100644 index 0000000..a8f59ad --- /dev/null +++ b/vendor/cakephp/core/Configure/ConfigEngineInterface.php @@ -0,0 +1,44 @@ + ['password' => 'secret']]` + * + * You can nest properties as deeply as needed using `.`'s. In addition to using `.` you + * can use standard ini section notation to create nested structures: + * + * ``` + * [section] + * key = value + * ``` + * + * Once loaded into Configure, the above would be accessed using: + * + * `Configure::read('section.key'); + * + * You can also use `.` separated values in section names to create more deeply + * nested structures. + * + * IniConfig also manipulates how the special ini values of + * 'yes', 'no', 'on', 'off', 'null' are handled. These values will be + * converted to their boolean equivalents. + * + * @see https://secure.php.net/parse_ini_file + */ +class IniConfig implements ConfigEngineInterface +{ + use FileConfigTrait; + + /** + * File extension. + * + * @var string + */ + protected $_extension = '.ini'; + + /** + * The section to read, if null all sections will be read. + * + * @var string|null + */ + protected $_section; + + /** + * Build and construct a new ini file parser. The parser can be used to read + * ini files that are on the filesystem. + * + * @param string|null $path Path to load ini config files from. Defaults to CONFIG. + * @param string|null $section Only get one section, leave null to parse and fetch + * all sections in the ini file. + */ + public function __construct(?string $path = null, ?string $section = null) + { + if ($path === null) { + $path = CONFIG; + } + $this->_path = $path; + $this->_section = $section; + } + + /** + * Read an ini file and return the results as an array. + * + * @param string $key The identifier to read from. If the key has a . it will be treated + * as a plugin prefix. The chosen file must be on the engine's path. + * @return array Parsed configuration values. + * @throws \Cake\Core\Exception\CakeException when files don't exist. + * Or when files contain '..' as this could lead to abusive reads. + */ + public function read(string $key): array + { + $file = $this->_getFilePath($key, true); + + $contents = parse_ini_file($file, true); + if ($this->_section && isset($contents[$this->_section])) { + $values = $this->_parseNestedValues($contents[$this->_section]); + } else { + $values = []; + foreach ($contents as $section => $attribs) { + if (is_array($attribs)) { + $values[$section] = $this->_parseNestedValues($attribs); + } else { + $parse = $this->_parseNestedValues([$attribs]); + $values[$section] = array_shift($parse); + } + } + } + + return $values; + } + + /** + * parses nested values out of keys. + * + * @param array $values Values to be exploded. + * @return array Array of values exploded + */ + protected function _parseNestedValues(array $values): array + { + foreach ($values as $key => $value) { + if ($value === '1') { + $value = true; + } + if ($value === '') { + $value = false; + } + unset($values[$key]); + if (strpos((string)$key, '.') !== false) { + $values = Hash::insert($values, $key, $value); + } else { + $values[$key] = $value; + } + } + + return $values; + } + + /** + * Dumps the state of Configure data into an ini formatted string. + * + * @param string $key The identifier to write to. If the key has a . it will be treated + * as a plugin prefix. + * @param array $data The data to convert to ini file. + * @return bool Success. + */ + public function dump(string $key, array $data): bool + { + $result = []; + foreach ($data as $k => $value) { + $isSection = false; + /** @psalm-suppress InvalidArrayAccess */ + if ($k[0] !== '[') { + $result[] = "[$k]"; + $isSection = true; + } + if (is_array($value)) { + $kValues = Hash::flatten($value, '.'); + foreach ($kValues as $k2 => $v) { + $result[] = "$k2 = " . $this->_value($v); + } + } + if ($isSection) { + $result[] = ''; + } + } + $contents = trim(implode("\n", $result)); + + $filename = $this->_getFilePath($key); + + return file_put_contents($filename, $contents) > 0; + } + + /** + * Converts a value into the ini equivalent + * + * @param mixed $value Value to export. + * @return string String value for ini file. + */ + protected function _value($value): string + { + if ($value === null) { + return 'null'; + } + if ($value === true) { + return 'true'; + } + if ($value === false) { + return 'false'; + } + + return (string)$value; + } +} diff --git a/vendor/cakephp/core/Configure/Engine/JsonConfig.php b/vendor/cakephp/core/Configure/Engine/JsonConfig.php new file mode 100644 index 0000000..66e8a90 --- /dev/null +++ b/vendor/cakephp/core/Configure/Engine/JsonConfig.php @@ -0,0 +1,115 @@ +_path = $path; + } + + /** + * Read a config file and return its contents. + * + * Files with `.` in the name will be treated as values in plugins. Instead of + * reading from the initialized path, plugin keys will be located using Plugin::path(). + * + * @param string $key The identifier to read from. If the key has a . it will be treated + * as a plugin prefix. + * @return array Parsed configuration values. + * @throws \Cake\Core\Exception\CakeException When files don't exist or when + * files contain '..' (as this could lead to abusive reads) or when there + * is an error parsing the JSON string. + */ + public function read(string $key): array + { + $file = $this->_getFilePath($key, true); + + $values = json_decode(file_get_contents($file), true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new CakeException(sprintf( + 'Error parsing JSON string fetched from config file "%s.json": %s', + $key, + json_last_error_msg() + )); + } + if (!is_array($values)) { + throw new CakeException(sprintf( + 'Decoding JSON config file "%s.json" did not return an array', + $key + )); + } + + return $values; + } + + /** + * Converts the provided $data into a JSON string that can be used saved + * into a file and loaded later. + * + * @param string $key The identifier to write to. If the key has a . it will + * be treated as a plugin prefix. + * @param array $data Data to dump. + * @return bool Success + */ + public function dump(string $key, array $data): bool + { + $filename = $this->_getFilePath($key); + + return file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT)) > 0; + } +} diff --git a/vendor/cakephp/core/Configure/Engine/PhpConfig.php b/vendor/cakephp/core/Configure/Engine/PhpConfig.php new file mode 100644 index 0000000..6bdf3a4 --- /dev/null +++ b/vendor/cakephp/core/Configure/Engine/PhpConfig.php @@ -0,0 +1,114 @@ + false, + * 'Security' => [ + * 'salt' => 'its-secret' + * ], + * 'App' => [ + * 'namespace' => 'App' + * ] + * ]; + * ``` + * + * @see \Cake\Core\Configure::load() for how to load custom configuration files. + */ +class PhpConfig implements ConfigEngineInterface +{ + use FileConfigTrait; + + /** + * File extension. + * + * @var string + */ + protected $_extension = '.php'; + + /** + * Constructor for PHP Config file reading. + * + * @param string|null $path The path to read config files from. Defaults to CONFIG. + */ + public function __construct(?string $path = null) + { + if ($path === null) { + $path = CONFIG; + } + $this->_path = $path; + } + + /** + * Read a config file and return its contents. + * + * Files with `.` in the name will be treated as values in plugins. Instead of + * reading from the initialized path, plugin keys will be located using Plugin::path(). + * + * @param string $key The identifier to read from. If the key has a . it will be treated + * as a plugin prefix. + * @return array Parsed configuration values. + * @throws \Cake\Core\Exception\CakeException when files don't exist or they don't contain `$config`. + * Or when files contain '..' as this could lead to abusive reads. + */ + public function read(string $key): array + { + $file = $this->_getFilePath($key, true); + + $config = null; + + $return = include $file; + if (is_array($return)) { + return $return; + } + + throw new CakeException(sprintf('Config file "%s" did not return an array', $key . '.php')); + } + + /** + * Converts the provided $data into a string of PHP code that can + * be used saved into a file and loaded later. + * + * @param string $key The identifier to write to. If the key has a . it will be treated + * as a plugin prefix. + * @param array $data Data to dump. + * @return bool Success + */ + public function dump(string $key, array $data): bool + { + $contents = '_getFilePath($key); + + return file_put_contents($filename, $contents) > 0; + } +} diff --git a/vendor/cakephp/core/Configure/FileConfigTrait.php b/vendor/cakephp/core/Configure/FileConfigTrait.php new file mode 100644 index 0000000..34931c8 --- /dev/null +++ b/vendor/cakephp/core/Configure/FileConfigTrait.php @@ -0,0 +1,71 @@ +_path . $key; + } + + $file .= $this->_extension; + + if (!$checkExists || is_file($file)) { + return $file; + } + + $realPath = realpath($file); + if ($realPath !== false && is_file($realPath)) { + return $realPath; + } + + throw new CakeException(sprintf('Could not load configuration file: %s', $file)); + } +} diff --git a/vendor/cakephp/core/ConsoleApplicationInterface.php b/vendor/cakephp/core/ConsoleApplicationInterface.php new file mode 100644 index 0000000..b2a532a --- /dev/null +++ b/vendor/cakephp/core/ConsoleApplicationInterface.php @@ -0,0 +1,42 @@ +_attributes = $message; + $message = vsprintf($this->_messageTemplate, $message); + } + parent::__construct($message, $code ?? $this->_defaultCode, $previous); + } + + /** + * Get the passed in attributes + * + * @return array + */ + public function getAttributes(): array + { + return $this->_attributes; + } + + /** + * Get/set the response header to be used + * + * See also {@link \Cake\Http\Response::withHeader()} + * + * @param array|string|null $header A single header string or an associative + * array of "header name" => "header value" + * @param string|null $value The header value. + * @return array|null + * @deprecated 4.2.0 Use `HttpException::setHeaders()` instead. Response headers + * should be set for HttpException only. + */ + public function responseHeader($header = null, $value = null): ?array + { + if ($header === null) { + return $this->_responseHeaders; + } + + deprecationWarning( + 'Setting HTTP response headers from Exception directly is deprecated. ' . + 'If your exceptions extend Exception, they must now extend HttpException. ' . + 'You should only set HTTP headers on HttpException instances via the `setHeaders()` method.' + ); + if (is_array($header)) { + return $this->_responseHeaders = $header; + } + + return $this->_responseHeaders = [$header => $value]; + } +} + +// phpcs:disable +class_exists('Cake\Core\Exception\Exception'); +// phpcs:enable diff --git a/vendor/cakephp/core/Exception/Exception.php b/vendor/cakephp/core/Exception/Exception.php new file mode 100644 index 0000000..fb5d49f --- /dev/null +++ b/vendor/cakephp/core/Exception/Exception.php @@ -0,0 +1,16 @@ + + */ + protected $_config = []; + + /** + * Whether the config property has already been configured with defaults + * + * @var bool + */ + protected $_configInitialized = false; + + /** + * Sets the config. + * + * ### Usage + * + * Setting a specific value: + * + * ``` + * $this->setConfig('key', $value); + * ``` + * + * Setting a nested value: + * + * ``` + * $this->setConfig('some.nested.key', $value); + * ``` + * + * Updating multiple config settings at the same time: + * + * ``` + * $this->setConfig(['one' => 'value', 'another' => 'value']); + * ``` + * + * @param array|string $key The key to set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true. + * @return $this + * @throws \Cake\Core\Exception\CakeException When trying to set a key that is invalid. + */ + public function setConfig($key, $value = null, $merge = true) + { + if (!$this->_configInitialized) { + $this->_config = $this->_defaultConfig; + $this->_configInitialized = true; + } + + $this->_configWrite($key, $value, $merge); + + return $this; + } + + /** + * Returns the config. + * + * ### Usage + * + * Reading the whole config: + * + * ``` + * $this->getConfig(); + * ``` + * + * Reading a specific value: + * + * ``` + * $this->getConfig('key'); + * ``` + * + * Reading a nested value: + * + * ``` + * $this->getConfig('some.nested.key'); + * ``` + * + * Reading with default value: + * + * ``` + * $this->getConfig('some-key', 'default-value'); + * ``` + * + * @param string|null $key The key to get or null for the whole config. + * @param mixed $default The return value when the key does not exist. + * @return mixed Configuration data at the named key or null if the key does not exist. + */ + public function getConfig(?string $key = null, $default = null) + { + if (!$this->_configInitialized) { + $this->_config = $this->_defaultConfig; + $this->_configInitialized = true; + } + + $return = $this->_configRead($key); + + return $return ?? $default; + } + + /** + * Returns the config for this specific key. + * + * The config value for this key must exist, it can never be null. + * + * @param string $key The key to get. + * @return mixed Configuration data at the named key + * @throws \InvalidArgumentException + */ + public function getConfigOrFail(string $key) + { + $config = $this->getConfig($key); + if ($config === null) { + throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key)); + } + + return $config; + } + + /** + * Merge provided config with existing config. Unlike `config()` which does + * a recursive merge for nested keys, this method does a simple merge. + * + * Setting a specific value: + * + * ``` + * $this->configShallow('key', $value); + * ``` + * + * Setting a nested value: + * + * ``` + * $this->configShallow('some.nested.key', $value); + * ``` + * + * Updating multiple config settings at the same time: + * + * ``` + * $this->configShallow(['one' => 'value', 'another' => 'value']); + * ``` + * + * @param array|string $key The key to set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @return $this + */ + public function configShallow($key, $value = null) + { + if (!$this->_configInitialized) { + $this->_config = $this->_defaultConfig; + $this->_configInitialized = true; + } + + $this->_configWrite($key, $value, 'shallow'); + + return $this; + } + + /** + * Reads a config key. + * + * @param string|null $key Key to read. + * @return mixed + */ + protected function _configRead(?string $key) + { + if ($key === null) { + return $this->_config; + } + + if (strpos($key, '.') === false) { + return $this->_config[$key] ?? null; + } + + $return = $this->_config; + + foreach (explode('.', $key) as $k) { + if (!is_array($return) || !isset($return[$k])) { + $return = null; + break; + } + + $return = $return[$k]; + } + + return $return; + } + + /** + * Writes a config key. + * + * @param array|string $key Key to write to. + * @param mixed $value Value to write. + * @param string|bool $merge True to merge recursively, 'shallow' for simple merge, + * false to overwrite, defaults to false. + * @return void + * @throws \Cake\Core\Exception\CakeException if attempting to clobber existing config + */ + protected function _configWrite($key, $value, $merge = false): void + { + if (is_string($key) && $value === null) { + $this->_configDelete($key); + + return; + } + + if ($merge) { + $update = is_array($key) ? $key : [$key => $value]; + if ($merge === 'shallow') { + $this->_config = array_merge($this->_config, Hash::expand($update)); + } else { + $this->_config = Hash::merge($this->_config, Hash::expand($update)); + } + + return; + } + + if (is_array($key)) { + foreach ($key as $k => $val) { + $this->_configWrite($k, $val); + } + + return; + } + + if (strpos($key, '.') === false) { + $this->_config[$key] = $value; + + return; + } + + $update = &$this->_config; + $stack = explode('.', $key); + + foreach ($stack as $k) { + if (!is_array($update)) { + throw new CakeException(sprintf('Cannot set %s value', $key)); + } + + $update[$k] = $update[$k] ?? []; + + $update = &$update[$k]; + } + + $update = $value; + } + + /** + * Deletes a single config key. + * + * @param string $key Key to delete. + * @return void + * @throws \Cake\Core\Exception\CakeException if attempting to clobber existing config + */ + protected function _configDelete(string $key): void + { + if (strpos($key, '.') === false) { + unset($this->_config[$key]); + + return; + } + + $update = &$this->_config; + $stack = explode('.', $key); + $length = count($stack); + + foreach ($stack as $i => $k) { + if (!is_array($update)) { + throw new CakeException(sprintf('Cannot unset %s value', $key)); + } + + if (!isset($update[$k])) { + break; + } + + if ($i === $length - 1) { + unset($update[$k]); + break; + } + + $update = &$update[$k]; + } + } +} diff --git a/vendor/cakephp/core/LICENSE.txt b/vendor/cakephp/core/LICENSE.txt new file mode 100644 index 0000000..b938c9e --- /dev/null +++ b/vendor/cakephp/core/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/cakephp/core/ObjectRegistry.php b/vendor/cakephp/core/ObjectRegistry.php new file mode 100644 index 0000000..5fc7e5a --- /dev/null +++ b/vendor/cakephp/core/ObjectRegistry.php @@ -0,0 +1,419 @@ + + * @psalm-var array + */ + protected $_loaded = []; + + /** + * Loads/constructs an object instance. + * + * Will return the instance in the registry if it already exists. + * If a subclass provides event support, you can use `$config['enabled'] = false` + * to exclude constructed objects from being registered for events. + * + * Using {@link \Cake\Controller\Component::$components} as an example. You can alias + * an object by setting the 'className' key, i.e., + * + * ``` + * protected $components = [ + * 'Email' => [ + * 'className' => 'App\Controller\Component\AliasedEmailComponent' + * ]; + * ]; + * ``` + * + * All calls to the `Email` component would use `AliasedEmail` instead. + * + * @param string $name The name/class of the object to load. + * @param array $config Additional settings to use when loading the object. + * @return mixed + * @psalm-return TObject + * @throws \Exception If the class cannot be found. + */ + public function load(string $name, array $config = []) + { + if (isset($config['className'])) { + $objName = $name; + $name = $config['className']; + } else { + [, $objName] = pluginSplit($name); + } + + $loaded = isset($this->_loaded[$objName]); + if ($loaded && !empty($config)) { + $this->_checkDuplicate($objName, $config); + } + if ($loaded) { + return $this->_loaded[$objName]; + } + + $className = $name; + if (is_string($name)) { + $className = $this->_resolveClassName($name); + if ($className === null) { + [$plugin, $name] = pluginSplit($name); + $this->_throwMissingClassError($name, $plugin); + } + } + + /** + * @psalm-var TObject $instance + * @psalm-suppress PossiblyNullArgument + **/ + $instance = $this->_create($className, $objName, $config); + $this->_loaded[$objName] = $instance; + + return $instance; + } + + /** + * Check for duplicate object loading. + * + * If a duplicate is being loaded and has different configuration, that is + * bad and an exception will be raised. + * + * An exception is raised, as replacing the object will not update any + * references other objects may have. Additionally, simply updating the runtime + * configuration is not a good option as we may be missing important constructor + * logic dependent on the configuration. + * + * @param string $name The name of the alias in the registry. + * @param array $config The config data for the new instance. + * @return void + * @throws \RuntimeException When a duplicate is found. + */ + protected function _checkDuplicate(string $name, array $config): void + { + $existing = $this->_loaded[$name]; + $msg = sprintf('The "%s" alias has already been loaded.', $name); + $hasConfig = method_exists($existing, 'getConfig'); + if (!$hasConfig) { + throw new RuntimeException($msg); + } + if (empty($config)) { + return; + } + $existingConfig = $existing->getConfig(); + unset($config['enabled'], $existingConfig['enabled']); + + $failure = null; + foreach ($config as $key => $value) { + if (!array_key_exists($key, $existingConfig)) { + $failure = " The `{$key}` was not defined in the previous configuration data."; + break; + } + if (isset($existingConfig[$key]) && $existingConfig[$key] !== $value) { + $failure = sprintf( + ' The `%s` key has a value of `%s` but previously had a value of `%s`', + $key, + json_encode($value), + json_encode($existingConfig[$key]) + ); + break; + } + } + if ($failure) { + throw new RuntimeException($msg . $failure); + } + } + + /** + * Should resolve the classname for a given object type. + * + * @param string $class The class to resolve. + * @return string|null The resolved name or null for failure. + * @psalm-return class-string|null + */ + abstract protected function _resolveClassName(string $class): ?string; + + /** + * Throw an exception when the requested object name is missing. + * + * @param string $class The class that is missing. + * @param string|null $plugin The plugin $class is missing from. + * @return void + * @throws \Exception + */ + abstract protected function _throwMissingClassError(string $class, ?string $plugin): void; + + /** + * Create an instance of a given classname. + * + * This method should construct and do any other initialization logic + * required. + * + * @param object|string $class The class to build. + * @param string $alias The alias of the object. + * @param array $config The Configuration settings for construction + * @return object + * @psalm-param TObject|string $class + * @psalm-return TObject + */ + abstract protected function _create($class, string $alias, array $config); + + /** + * Get the list of loaded objects. + * + * @return array List of object names. + */ + public function loaded(): array + { + return array_keys($this->_loaded); + } + + /** + * Check whether a given object is loaded. + * + * @param string $name The object name to check for. + * @return bool True is object is loaded else false. + */ + public function has(string $name): bool + { + return isset($this->_loaded[$name]); + } + + /** + * Get loaded object instance. + * + * @param string $name Name of object. + * @return object Object instance. + * @throws \RuntimeException If not loaded or found. + * @psalm-return TObject + */ + public function get(string $name) + { + if (!isset($this->_loaded[$name])) { + throw new RuntimeException(sprintf('Unknown object "%s"', $name)); + } + + return $this->_loaded[$name]; + } + + /** + * Provide public read access to the loaded objects + * + * @param string $name Name of property to read + * @return object|null + * @psalm-return TObject|null + */ + public function __get(string $name) + { + return $this->_loaded[$name] ?? null; + } + + /** + * Provide isset access to _loaded + * + * @param string $name Name of object being checked. + * @return bool + */ + public function __isset(string $name): bool + { + return $this->has($name); + } + + /** + * Sets an object. + * + * @param string $name Name of a property to set. + * @param object $object Object to set. + * @psalm-param TObject $object + * @return void + */ + public function __set(string $name, $object): void + { + $this->set($name, $object); + } + + /** + * Unsets an object. + * + * @param string $name Name of a property to unset. + * @return void + */ + public function __unset(string $name): void + { + $this->unload($name); + } + + /** + * Normalizes an object array, creates an array that makes lazy loading + * easier + * + * @param array $objects Array of child objects to normalize. + * @return array Array of normalized objects. + */ + public function normalizeArray(array $objects): array + { + $normal = []; + foreach ($objects as $i => $objectName) { + $config = []; + if (!is_int($i)) { + $config = (array)$objectName; + $objectName = $i; + } + [, $name] = pluginSplit($objectName); + if (isset($config['class'])) { + $normal[$name] = $config + ['config' => []]; + } else { + $normal[$name] = ['class' => $objectName, 'config' => $config]; + } + } + + return $normal; + } + + /** + * Clear loaded instances in the registry. + * + * If the registry subclass has an event manager, the objects will be detached from events as well. + * + * @return $this + */ + public function reset() + { + foreach (array_keys($this->_loaded) as $name) { + $this->unload((string)$name); + } + + return $this; + } + + /** + * Set an object directly into the registry by name. + * + * If this collection implements events, the passed object will + * be attached into the event manager + * + * @param string $name The name of the object to set in the registry. + * @param object $object instance to store in the registry + * @return $this + * @psalm-param TObject $object + * @psalm-suppress MoreSpecificReturnType + */ + public function set(string $name, object $object) + { + [, $objName] = pluginSplit($name); + + // Just call unload if the object was loaded before + if (array_key_exists($name, $this->_loaded)) { + $this->unload($name); + } + if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) { + $this->getEventManager()->on($object); + } + $this->_loaded[$objName] = $object; + + /** @psalm-suppress LessSpecificReturnStatement */ + return $this; + } + + /** + * Remove an object from the registry. + * + * If this registry has an event manager, the object will be detached from any events as well. + * + * @param string $name The name of the object to remove from the registry. + * @return $this + * @psalm-suppress MoreSpecificReturnType + */ + public function unload(string $name) + { + if (empty($this->_loaded[$name])) { + [$plugin, $name] = pluginSplit($name); + $this->_throwMissingClassError($name, $plugin); + } + + $object = $this->_loaded[$name]; + if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) { + $this->getEventManager()->off($object); + } + unset($this->_loaded[$name]); + + /** @psalm-suppress LessSpecificReturnStatement */ + return $this; + } + + /** + * Returns an array iterator. + * + * @return \Traversable + * @psalm-return \Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->_loaded); + } + + /** + * Returns the number of loaded objects. + * + * @return int + */ + public function count(): int + { + return count($this->_loaded); + } + + /** + * Debug friendly object properties. + * + * @return array + */ + public function __debugInfo(): array + { + $properties = get_object_vars($this); + if (isset($properties['_loaded'])) { + $properties['_loaded'] = array_keys($properties['_loaded']); + } + + return $properties; + } +} diff --git a/vendor/cakephp/core/Plugin.php b/vendor/cakephp/core/Plugin.php new file mode 100644 index 0000000..67e1a63 --- /dev/null +++ b/vendor/cakephp/core/Plugin.php @@ -0,0 +1,136 @@ +get($name); + + return $plugin->getPath(); + } + + /** + * Returns the filesystem path for plugin's folder containing class files. + * + * @param string $name name of the plugin in CamelCase format. + * @return string Path to the plugin folder containing class files. + * @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded. + */ + public static function classPath(string $name): string + { + $plugin = static::getCollection()->get($name); + + return $plugin->getClassPath(); + } + + /** + * Returns the filesystem path for plugin's folder containing config files. + * + * @param string $name name of the plugin in CamelCase format. + * @return string Path to the plugin folder containing config files. + * @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded. + */ + public static function configPath(string $name): string + { + $plugin = static::getCollection()->get($name); + + return $plugin->getConfigPath(); + } + + /** + * Returns the filesystem path for plugin's folder containing template files. + * + * @param string $name name of the plugin in CamelCase format. + * @return string Path to the plugin folder containing template files. + * @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded. + */ + public static function templatePath(string $name): string + { + $plugin = static::getCollection()->get($name); + + return $plugin->getTemplatePath(); + } + + /** + * Returns true if the plugin $plugin is already loaded. + * + * @param string $plugin Plugin name. + * @return bool + * @since 3.7.0 + */ + public static function isLoaded(string $plugin): bool + { + return static::getCollection()->has($plugin); + } + + /** + * Return a list of loaded plugins. + * + * @return array A list of plugins that have been loaded + */ + public static function loaded(): array + { + $names = []; + foreach (static::getCollection() as $plugin) { + $names[] = $plugin->getName(); + } + sort($names); + + return $names; + } + + /** + * Get the shared plugin collection. + * + * This method should generally not be used during application + * runtime as plugins should be set during Application startup. + * + * @return \Cake\Core\PluginCollection + */ + public static function getCollection(): PluginCollection + { + if (!isset(static::$plugins)) { + static::$plugins = new PluginCollection(); + } + + return static::$plugins; + } +} diff --git a/vendor/cakephp/core/PluginApplicationInterface.php b/vendor/cakephp/core/PluginApplicationInterface.php new file mode 100644 index 0000000..0ba0771 --- /dev/null +++ b/vendor/cakephp/core/PluginApplicationInterface.php @@ -0,0 +1,75 @@ + $config The configuration data for the plugin if using a string for $name + * @return $this + */ + public function addPlugin($name, array $config = []); + + /** + * Run bootstrap logic for loaded plugins. + * + * @return void + */ + public function pluginBootstrap(): void; + + /** + * Run routes hooks for loaded plugins + * + * @param \Cake\Routing\RouteBuilder $routes The route builder to use. + * @return \Cake\Routing\RouteBuilder + */ + public function pluginRoutes(RouteBuilder $routes): RouteBuilder; + + /** + * Run middleware hooks for plugins + * + * @param \Cake\Http\MiddlewareQueue $middleware The MiddlewareQueue to use. + * @return \Cake\Http\MiddlewareQueue + */ + public function pluginMiddleware(MiddlewareQueue $middleware): MiddlewareQueue; + + /** + * Run console hooks for plugins + * + * @param \Cake\Console\CommandCollection $commands The CommandCollection to use. + * @return \Cake\Console\CommandCollection + */ + public function pluginConsole(CommandCollection $commands): CommandCollection; +} diff --git a/vendor/cakephp/core/PluginCollection.php b/vendor/cakephp/core/PluginCollection.php new file mode 100644 index 0000000..2be443e --- /dev/null +++ b/vendor/cakephp/core/PluginCollection.php @@ -0,0 +1,343 @@ + + */ + protected $plugins = []; + + /** + * Names of plugins + * + * @var array + */ + protected $names = []; + + /** + * Iterator position stack. + * + * @var array + */ + protected $positions = []; + + /** + * Loop depth + * + * @var int + */ + protected $loopDepth = -1; + + /** + * Constructor + * + * @param array<\Cake\Core\PluginInterface> $plugins The map of plugins to add to the collection. + */ + public function __construct(array $plugins = []) + { + foreach ($plugins as $plugin) { + $this->add($plugin); + } + $this->loadConfig(); + } + + /** + * Load the path information stored in vendor/cakephp-plugins.php + * + * This file is generated by the cakephp/plugin-installer package and used + * to locate plugins on the filesystem as applications can use `extra.plugin-paths` + * in their composer.json file to move plugin outside of vendor/ + * + * @internal + * @return void + */ + protected function loadConfig(): void + { + if (Configure::check('plugins')) { + return; + } + $vendorFile = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php'; + if (!is_file($vendorFile)) { + $vendorFile = dirname(dirname(dirname(dirname(__DIR__)))) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php'; + if (!is_file($vendorFile)) { + Configure::write(['plugins' => []]); + + return; + } + } + + $config = require $vendorFile; + Configure::write($config); + } + + /** + * Locate a plugin path by looking at configuration data. + * + * This will use the `plugins` Configure key, and fallback to enumerating `App::path('plugins')` + * + * This method is not part of the official public API as plugins with + * no plugin class are being phased out. + * + * @param string $name The plugin name to locate a path for. Will return '' when a plugin cannot be found. + * @return string + * @throws \Cake\Core\Exception\MissingPluginException when a plugin path cannot be resolved. + * @internal + */ + public function findPath(string $name): string + { + // Ensure plugin config is loaded each time. This is necessary primarily + // for testing because the Configure::clear() call in TestCase::tearDown() + // wipes out all configuration including plugin paths config. + $this->loadConfig(); + + $path = Configure::read('plugins.' . $name); + if ($path) { + return $path; + } + + $pluginPath = str_replace('/', DIRECTORY_SEPARATOR, $name); + $paths = App::path('plugins'); + foreach ($paths as $path) { + if (is_dir($path . $pluginPath)) { + return $path . $pluginPath . DIRECTORY_SEPARATOR; + } + } + + throw new MissingPluginException(['plugin' => $name]); + } + + /** + * Add a plugin to the collection + * + * Plugins will be keyed by their names. + * + * @param \Cake\Core\PluginInterface $plugin The plugin to load. + * @return $this + */ + public function add(PluginInterface $plugin) + { + $name = $plugin->getName(); + $this->plugins[$name] = $plugin; + $this->names = array_keys($this->plugins); + + return $this; + } + + /** + * Remove a plugin from the collection if it exists. + * + * @param string $name The named plugin. + * @return $this + */ + public function remove(string $name) + { + unset($this->plugins[$name]); + $this->names = array_keys($this->plugins); + + return $this; + } + + /** + * Remove all plugins from the collection + * + * @return $this + */ + public function clear() + { + $this->plugins = []; + $this->names = []; + $this->positions = []; + $this->loopDepth = -1; + + return $this; + } + + /** + * Check whether the named plugin exists in the collection. + * + * @param string $name The named plugin. + * @return bool + */ + public function has(string $name): bool + { + return isset($this->plugins[$name]); + } + + /** + * Get the a plugin by name. + * + * If a plugin isn't already loaded it will be autoloaded on first access + * and that plugins loaded this way may miss some hook methods. + * + * @param string $name The plugin to get. + * @return \Cake\Core\PluginInterface The plugin. + * @throws \Cake\Core\Exception\MissingPluginException when unknown plugins are fetched. + */ + public function get(string $name): PluginInterface + { + if ($this->has($name)) { + return $this->plugins[$name]; + } + + $plugin = $this->create($name); + $this->add($plugin); + + return $plugin; + } + + /** + * Create a plugin instance from a name/classname and configuration. + * + * @param string $name The plugin name or classname + * @param array $config Configuration options for the plugin. + * @return \Cake\Core\PluginInterface + * @throws \Cake\Core\Exception\MissingPluginException When plugin instance could not be created. + */ + public function create(string $name, array $config = []): PluginInterface + { + if (strpos($name, '\\') !== false) { + /** @var \Cake\Core\PluginInterface */ + return new $name($config); + } + + $config += ['name' => $name]; + /** @var class-string<\Cake\Core\PluginInterface> $className */ + $className = str_replace('/', '\\', $name) . '\\' . 'Plugin'; + if (!class_exists($className)) { + $className = BasePlugin::class; + if (empty($config['path'])) { + $config['path'] = $this->findPath($name); + } + } + + return new $className($config); + } + + /** + * Implementation of Countable. + * + * Get the number of plugins in the collection. + * + * @return int + */ + public function count(): int + { + return count($this->plugins); + } + + /** + * Part of Iterator Interface + * + * @return void + */ + public function next(): void + { + $this->positions[$this->loopDepth]++; + } + + /** + * Part of Iterator Interface + * + * @return string + */ + public function key(): string + { + return $this->names[$this->positions[$this->loopDepth]]; + } + + /** + * Part of Iterator Interface + * + * @return \Cake\Core\PluginInterface + */ + public function current(): PluginInterface + { + $position = $this->positions[$this->loopDepth]; + $name = $this->names[$position]; + + return $this->plugins[$name]; + } + + /** + * Part of Iterator Interface + * + * @return void + */ + public function rewind(): void + { + $this->positions[] = 0; + $this->loopDepth += 1; + } + + /** + * Part of Iterator Interface + * + * @return bool + */ + public function valid(): bool + { + $valid = isset($this->names[$this->positions[$this->loopDepth]]); + if (!$valid) { + array_pop($this->positions); + $this->loopDepth -= 1; + } + + return $valid; + } + + /** + * Filter the plugins to those with the named hook enabled. + * + * @param string $hook The hook to filter plugins by + * @return \Generator<\Cake\Core\PluginInterface> A generator containing matching plugins. + * @throws \InvalidArgumentException on invalid hooks + */ + public function with(string $hook): Generator + { + if (!in_array($hook, PluginInterface::VALID_HOOKS, true)) { + throw new InvalidArgumentException("The `{$hook}` hook is not a known plugin hook."); + } + foreach ($this as $plugin) { + if ($plugin->isEnabled($hook)) { + yield $plugin; + } + } + } +} diff --git a/vendor/cakephp/core/PluginInterface.php b/vendor/cakephp/core/PluginInterface.php new file mode 100644 index 0000000..e251cdf --- /dev/null +++ b/vendor/cakephp/core/PluginInterface.php @@ -0,0 +1,135 @@ + + */ + public const VALID_HOOKS = ['bootstrap', 'console', 'middleware', 'routes', 'services']; + + /** + * Get the name of this plugin. + * + * @return string + */ + public function getName(): string; + + /** + * Get the filesystem path to this plugin + * + * @return string + */ + public function getPath(): string; + + /** + * Get the filesystem path to configuration for this plugin + * + * @return string + */ + public function getConfigPath(): string; + + /** + * Get the filesystem path to configuration for this plugin + * + * @return string + */ + public function getClassPath(): string; + + /** + * Get the filesystem path to templates for this plugin + * + * @return string + */ + public function getTemplatePath(): string; + + /** + * Load all the application configuration and bootstrap logic. + * + * The default implementation of this method will include the `config/bootstrap.php` in the plugin if it exist. You + * can override this method to replace that behavior. + * + * The host application is provided as an argument. This allows you to load additional + * plugin dependencies, or attach events. + * + * @param \Cake\Core\PluginApplicationInterface $app The host application + * @return void + */ + public function bootstrap(PluginApplicationInterface $app): void; + + /** + * Add console commands for the plugin. + * + * @param \Cake\Console\CommandCollection $commands The command collection to update + * @return \Cake\Console\CommandCollection + */ + public function console(CommandCollection $commands): CommandCollection; + + /** + * Add middleware for the plugin. + * + * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update. + * @return \Cake\Http\MiddlewareQueue + */ + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue; + + /** + * Add routes for the plugin. + * + * The default implementation of this method will include the `config/routes.php` in the plugin if it exists. You + * can override this method to replace that behavior. + * + * @param \Cake\Routing\RouteBuilder $routes The route builder to update. + * @return void + */ + public function routes(RouteBuilder $routes): void; + + /** + * Disables the named hook + * + * @param string $hook The hook to disable + * @return $this + */ + public function disable(string $hook); + + /** + * Enables the named hook + * + * @param string $hook The hook to disable + * @return $this + */ + public function enable(string $hook); + + /** + * Check if the named hook is enabled + * + * @param string $hook The hook to check + * @return bool + */ + public function isEnabled(string $hook): bool; +} diff --git a/vendor/cakephp/core/README.md b/vendor/cakephp/core/README.md new file mode 100644 index 0000000..f26cba6 --- /dev/null +++ b/vendor/cakephp/core/README.md @@ -0,0 +1,37 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/core.svg?style=flat-square)](https://packagist.org/packages/cakephp/core) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Core Classes + +A set of classes used for configuration files reading and storing. +This repository contains the classes that are used as glue for creating the CakePHP framework. + +## Usage + +You can use the `Configure` class to store arbitrary configuration data: + +```php +use Cake\Core\Configure; +use Cake\Core\Configure\Engine\PhpConfig; + +Configure::write('Company.name','Pizza, Inc.'); +Configure::read('Company.name'); // Returns: 'Pizza, Inc.' +``` + +It also possible to load configuration from external files: + +```php +Configure::config('default', new PhpConfig('/path/to/config/folder')); +Configure::load('app', 'default', false); +Configure::load('other_config', 'default'); +``` + +And write the configuration back into files: + +```php +Configure::dump('my_config', 'default'); +``` + +## Documentation + +Please make sure you check the [official documentation](https://book.cakephp.org/4/en/development/configuration.html) diff --git a/vendor/cakephp/core/Retry/CommandRetry.php b/vendor/cakephp/core/Retry/CommandRetry.php new file mode 100644 index 0000000..1211695 --- /dev/null +++ b/vendor/cakephp/core/Retry/CommandRetry.php @@ -0,0 +1,94 @@ +strategy = $strategy; + $this->maxRetries = $maxRetries; + } + + /** + * The number of retries to perform in case of failure + * + * @param callable $action The callable action to execute with a retry strategy + * @return mixed The return value of the passed action callable + * @throws \Exception + */ + public function run(callable $action) + { + $this->numRetries = 0; + while (true) { + try { + return $action(); + } catch (Exception $e) { + if ( + $this->numRetries < $this->maxRetries && + $this->strategy->shouldRetry($e, $this->numRetries) + ) { + $this->numRetries++; + continue; + } + + throw $e; + } + } + } + + /** + * Returns the last number of retry attemps. + * + * @return int + */ + public function getRetries(): int + { + return $this->numRetries; + } +} diff --git a/vendor/cakephp/core/Retry/RetryStrategyInterface.php b/vendor/cakephp/core/Retry/RetryStrategyInterface.php new file mode 100644 index 0000000..e2ef9e2 --- /dev/null +++ b/vendor/cakephp/core/Retry/RetryStrategyInterface.php @@ -0,0 +1,35 @@ + + * @see ServiceProvider::provides() + */ + protected $provides = []; + + /** + * Get the container. + * + * This method's actual return type and documented return type differ + * because PHP 7.2 doesn't support return type narrowing. + * + * @return \Cake\Core\ContainerInterface + */ + public function getContainer(): DefinitionContainerInterface + { + $container = parent::getContainer(); + + if (!($container instanceof ContainerInterface)) { + $message = sprintf( + 'Unexpected container type. Expected `%s` got `%s` instead.', + ContainerInterface::class, + getTypeName($container) + ); + throw new RuntimeException($message); + } + + return $container; + } + + /** + * Delegate to the bootstrap() method + * + * This method wraps the league/container function so users + * only need to use the CakePHP bootstrap() interface. + * + * @return void + */ + public function boot(): void + { + $this->bootstrap($this->getContainer()); + } + + /** + * Bootstrap hook for ServiceProviders + * + * This hook should be implemented if your service provider + * needs to register additional service providers, load configuration + * files or do any other work when the service provider is added to the + * container. + * + * @param \Cake\Core\ContainerInterface $container The container to add services to. + * @return void + */ + public function bootstrap(ContainerInterface $container): void + { + } + + /** + * Call the abstract services() method. + * + * This method primarily exists as a shim between the interface + * that league/container has and the one we want to offer in CakePHP. + * + * @return void + */ + public function register(): void + { + $this->services($this->getContainer()); + } + + /** + * The provides method is a way to let the container know that a service + * is provided by this service provider. + * + * Every service that is registered via this service provider must have an + * alias added to this array or it will be ignored. + * + * @param string $id Identifier. + * @return bool + */ + public function provides(string $id): bool + { + return in_array($id, $this->provides, true); + } + + /** + * Register the services in a provider. + * + * All services registered in this method should also be included in the $provides + * property so that services can be located. + * + * @param \Cake\Core\ContainerInterface $container The container to add services to. + * @return void + */ + abstract public function services(ContainerInterface $container): void; +} diff --git a/vendor/cakephp/core/StaticConfigTrait.php b/vendor/cakephp/core/StaticConfigTrait.php new file mode 100644 index 0000000..c11a12c --- /dev/null +++ b/vendor/cakephp/core/StaticConfigTrait.php @@ -0,0 +1,325 @@ + + */ + protected static $_config = []; + + /** + * This method can be used to define configuration adapters for an application. + * + * To change an adapter's configuration at runtime, first drop the adapter and then + * reconfigure it. + * + * Adapters will not be constructed until the first operation is done. + * + * ### Usage + * + * Assuming that the class' name is `Cache` the following scenarios + * are supported: + * + * Setting a cache engine up. + * + * ``` + * Cache::setConfig('default', $settings); + * ``` + * + * Injecting a constructed adapter in: + * + * ``` + * Cache::setConfig('default', $instance); + * ``` + * + * Configure multiple adapters at once: + * + * ``` + * Cache::setConfig($arrayOfConfig); + * ``` + * + * @param array|string $key The name of the configuration, or an array of multiple configs. + * @param object|array|null $config An array of name => configuration data for adapter. + * @throws \BadMethodCallException When trying to modify an existing config. + * @throws \LogicException When trying to store an invalid structured config array. + * @return void + */ + public static function setConfig($key, $config = null): void + { + if ($config === null) { + if (!is_array($key)) { + throw new LogicException('If config is null, key must be an array.'); + } + foreach ($key as $name => $settings) { + static::setConfig($name, $settings); + } + + return; + } + + if (isset(static::$_config[$key])) { + /** @psalm-suppress PossiblyInvalidArgument */ + throw new BadMethodCallException(sprintf('Cannot reconfigure existing key "%s"', $key)); + } + + if (is_object($config)) { + $config = ['className' => $config]; + } + + if (isset($config['url'])) { + $parsed = static::parseDsn($config['url']); + unset($config['url']); + $config = $parsed + $config; + } + + if (isset($config['engine']) && empty($config['className'])) { + $config['className'] = $config['engine']; + unset($config['engine']); + } + /** @psalm-suppress InvalidPropertyAssignmentValue */ + static::$_config[$key] = $config; + } + + /** + * Reads existing configuration. + * + * @param string $key The name of the configuration. + * @return mixed|null Configuration data at the named key or null if the key does not exist. + */ + public static function getConfig(string $key) + { + return static::$_config[$key] ?? null; + } + + /** + * Reads existing configuration for a specific key. + * + * The config value for this key must exist, it can never be null. + * + * @param string $key The name of the configuration. + * @return mixed Configuration data at the named key. + * @throws \InvalidArgumentException If value does not exist. + */ + public static function getConfigOrFail(string $key) + { + if (!isset(static::$_config[$key])) { + throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key)); + } + + return static::$_config[$key]; + } + + /** + * Drops a constructed adapter. + * + * If you wish to modify an existing configuration, you should drop it, + * change configuration and then re-add it. + * + * If the implementing objects supports a `$_registry` object the named configuration + * will also be unloaded from the registry. + * + * @param string $config An existing configuration you wish to remove. + * @return bool Success of the removal, returns false when the config does not exist. + */ + public static function drop(string $config): bool + { + if (!isset(static::$_config[$config])) { + return false; + } + /** @psalm-suppress RedundantPropertyInitializationCheck */ + if (isset(static::$_registry)) { + static::$_registry->unload($config); + } + unset(static::$_config[$config]); + + return true; + } + + /** + * Returns an array containing the named configurations + * + * @return array Array of configurations. + */ + public static function configured(): array + { + $configurations = array_keys(static::$_config); + + return array_map(function ($key) { + return (string)$key; + }, $configurations); + } + + /** + * Parses a DSN into a valid connection configuration + * + * This method allows setting a DSN using formatting similar to that used by PEAR::DB. + * The following is an example of its usage: + * + * ``` + * $dsn = 'mysql://user:pass@localhost/database?'; + * $config = ConnectionManager::parseDsn($dsn); + * + * $dsn = 'Cake\Log\Engine\FileLog://?types=notice,info,debug&file=debug&path=LOGS'; + * $config = Log::parseDsn($dsn); + * + * $dsn = 'smtp://user:secret@localhost:25?timeout=30&client=null&tls=null'; + * $config = Email::parseDsn($dsn); + * + * $dsn = 'file:///?className=\My\Cache\Engine\FileEngine'; + * $config = Cache::parseDsn($dsn); + * + * $dsn = 'File://?prefix=myapp_cake_core_&serialize=true&duration=+2 minutes&path=/tmp/persistent/'; + * $config = Cache::parseDsn($dsn); + * ``` + * + * For all classes, the value of `scheme` is set as the value of both the `className` + * unless they have been otherwise specified. + * + * Note that querystring arguments are also parsed and set as values in the returned configuration. + * + * @param string $dsn The DSN string to convert to a configuration array + * @return array The configuration array to be stored after parsing the DSN + * @throws \InvalidArgumentException If not passed a string, or passed an invalid string + */ + public static function parseDsn(string $dsn): array + { + if (empty($dsn)) { + return []; + } + + $pattern = <<<'REGEXP' +{ + ^ + (?P<_scheme> + (?P[\w\\\\]+):// + ) + (?P<_username> + (?P.*?) + (?P<_password> + :(?P.*?) + )? + @ + )? + (?P<_host> + (?P[^?#/:@]+) + (?P<_port> + :(?P\d+) + )? + )? + (?P<_path> + (?P/[^?#]*) + )? + (?P<_query> + \?(?P[^#]*) + )? + (?P<_fragment> + \#(?P.*) + )? + $ +}x +REGEXP; + + preg_match($pattern, $dsn, $parsed); + + if (!$parsed) { + throw new InvalidArgumentException("The DSN string '{$dsn}' could not be parsed."); + } + + $exists = []; + foreach ($parsed as $k => $v) { + if (is_int($k)) { + unset($parsed[$k]); + } elseif (strpos($k, '_') === 0) { + $exists[substr($k, 1)] = ($v !== ''); + unset($parsed[$k]); + } elseif ($v === '' && !$exists[$k]) { + unset($parsed[$k]); + } + } + + $query = ''; + + if (isset($parsed['query'])) { + $query = $parsed['query']; + unset($parsed['query']); + } + + parse_str($query, $queryArgs); + + foreach ($queryArgs as $key => $value) { + if ($value === 'true') { + $queryArgs[$key] = true; + } elseif ($value === 'false') { + $queryArgs[$key] = false; + } elseif ($value === 'null') { + $queryArgs[$key] = null; + } + } + + $parsed = $queryArgs + $parsed; + + if (empty($parsed['className'])) { + $classMap = static::getDsnClassMap(); + + $parsed['className'] = $parsed['scheme']; + if (isset($classMap[$parsed['scheme']])) { + /** @psalm-suppress PossiblyNullArrayOffset */ + $parsed['className'] = $classMap[$parsed['scheme']]; + } + } + + return $parsed; + } + + /** + * Updates the DSN class map for this class. + * + * @param array $map Additions/edits to the class map to apply. + * @return void + * @psalm-param array $map + */ + public static function setDsnClassMap(array $map): void + { + static::$_dsnClassMap = $map + static::$_dsnClassMap; + } + + /** + * Returns the DSN class map for this class. + * + * @return array + * @psalm-return array + */ + public static function getDsnClassMap(): array + { + return static::$_dsnClassMap; + } +} diff --git a/vendor/cakephp/core/composer.json b/vendor/cakephp/core/composer.json new file mode 100644 index 0000000..ffd17ba --- /dev/null +++ b/vendor/cakephp/core/composer.json @@ -0,0 +1,41 @@ +{ + "name": "cakephp/core", + "description": "CakePHP Framework Core classes", + "type": "library", + "keywords": [ + "cakephp", + "framework", + "core" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/core/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/core" + }, + "require": { + "php": ">=7.2.0", + "cakephp/utility": "^4.0" + }, + "suggest": { + "cakephp/event": "To use PluginApplicationInterface or plugin applications.", + "cakephp/cache": "To use Configure::store() and restore().", + "league/container": "To use Container and ServiceProvider classes" + }, + "autoload": { + "psr-4": { + "Cake\\Core\\": "." + }, + "files": [ + "functions.php" + ] + } +} diff --git a/vendor/cakephp/core/functions.php b/vendor/cakephp/core/functions.php new file mode 100644 index 0000000..2ca8619 --- /dev/null +++ b/vendor/cakephp/core/functions.php @@ -0,0 +1,336 @@ + $t) { + $texts[$k] = h($t, $double, $charset); + } + + return $texts; + } elseif (is_object($text)) { + if (method_exists($text, '__toString')) { + $text = $text->__toString(); + } else { + $text = '(object)' . get_class($text); + } + } elseif ($text === null || is_scalar($text)) { + return $text; + } + + static $defaultCharset = false; + if ($defaultCharset === false) { + $defaultCharset = mb_internal_encoding() ?: 'UTF-8'; + } + + return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, $charset ?: $defaultCharset, $double); + } + +} + +if (!function_exists('pluginSplit')) { + /** + * Splits a dot syntax plugin name into its plugin and class name. + * If $name does not have a dot, then index 0 will be null. + * + * Commonly used like + * ``` + * list($plugin, $name) = pluginSplit($name); + * ``` + * + * @param string $name The name you want to plugin split. + * @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it. + * @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null. + * @return array Array with 2 indexes. 0 => plugin name, 1 => class name. + * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pluginSplit + * @psalm-return array{string|null, string} + */ + function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array + { + if (strpos($name, '.') !== false) { + $parts = explode('.', $name, 2); + if ($dotAppend) { + $parts[0] .= '.'; + } + + /** @psalm-var array{string, string}*/ + return $parts; + } + + return [$plugin, $name]; + } + +} + +if (!function_exists('namespaceSplit')) { + /** + * Split the namespace from the classname. + * + * Commonly used like `list($namespace, $className) = namespaceSplit($class);`. + * + * @param string $class The full class name, ie `Cake\Core\App`. + * @return array Array with 2 indexes. 0 => namespace, 1 => classname. + */ + function namespaceSplit(string $class): array + { + $pos = strrpos($class, '\\'); + if ($pos === false) { + return ['', $class]; + } + + return [substr($class, 0, $pos), substr($class, $pos + 1)]; + } + +} + +if (!function_exists('pr')) { + /** + * print_r() convenience function. + * + * In terminals this will act similar to using print_r() directly, when not run on CLI + * print_r() will also wrap `
` tags around the output of given variable. Similar to debug().
+     *
+     * This function returns the same variable that was passed.
+     *
+     * @param mixed $var Variable to print out.
+     * @return mixed the same $var that was passed to this function
+     * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pr
+     * @see debug()
+     */
+    function pr($var)
+    {
+        if (!Configure::read('debug')) {
+            return $var;
+        }
+
+        $template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '
%s
' : "\n%s\n\n"; + printf($template, trim(print_r($var, true))); + + return $var; + } + +} + +if (!function_exists('pj')) { + /** + * JSON pretty print convenience function. + * + * In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on CLI + * will also wrap `
` tags around the output of given variable. Similar to pr().
+     *
+     * This function returns the same variable that was passed.
+     *
+     * @param mixed $var Variable to print out.
+     * @return mixed the same $var that was passed to this function
+     * @see pr()
+     * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pj
+     */
+    function pj($var)
+    {
+        if (!Configure::read('debug')) {
+            return $var;
+        }
+
+        $template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '
%s
' : "\n%s\n\n"; + printf($template, trim(json_encode($var, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))); + + return $var; + } + +} + +if (!function_exists('env')) { + /** + * Gets an environment variable from available sources, and provides emulation + * for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on + * IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom + * environment information. + * + * @param string $key Environment variable name. + * @param string|bool|null $default Specify a default value in case the environment variable is not defined. + * @return string|bool|null Environment variable setting. + * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#env + */ + function env(string $key, $default = null) + { + if ($key === 'HTTPS') { + if (isset($_SERVER['HTTPS'])) { + return !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; + } + + return strpos((string)env('SCRIPT_URI'), 'https://') === 0; + } + + if ($key === 'SCRIPT_NAME' && env('CGI_MODE') && isset($_ENV['SCRIPT_URL'])) { + $key = 'SCRIPT_URL'; + } + + $val = $_SERVER[$key] ?? $_ENV[$key] ?? null; + if ($val == null && getenv($key) !== false) { + $val = getenv($key); + } + + if ($key === 'REMOTE_ADDR' && $val === env('SERVER_ADDR')) { + $addr = env('HTTP_PC_REMOTE_ADDR'); + if ($addr !== null) { + $val = $addr; + } + } + + if ($val !== null) { + return $val; + } + + switch ($key) { + case 'DOCUMENT_ROOT': + $name = (string)env('SCRIPT_NAME'); + $filename = (string)env('SCRIPT_FILENAME'); + $offset = 0; + if (!strpos($name, '.php')) { + $offset = 4; + } + + return substr($filename, 0, -(strlen($name) + $offset)); + case 'PHP_SELF': + return str_replace((string)env('DOCUMENT_ROOT'), '', (string)env('SCRIPT_FILENAME')); + case 'CGI_MODE': + return PHP_SAPI === 'cgi'; + } + + return $default; + } + +} + +if (!function_exists('triggerWarning')) { + /** + * Triggers an E_USER_WARNING. + * + * @param string $message The warning message. + * @return void + */ + function triggerWarning(string $message): void + { + $trace = debug_backtrace(); + if (isset($trace[1])) { + $frame = $trace[1]; + $frame += ['file' => '[internal]', 'line' => '??']; + $message = sprintf( + '%s - %s, line: %s', + $message, + $frame['file'], + $frame['line'] + ); + } + trigger_error($message, E_USER_WARNING); + } +} + +if (!function_exists('deprecationWarning')) { + /** + * Helper method for outputting deprecation warnings + * + * @param string $message The message to output as a deprecation warning. + * @param int $stackFrame The stack frame to include in the error. Defaults to 1 + * as that should point to application/plugin code. + * @return void + */ + function deprecationWarning(string $message, int $stackFrame = 1): void + { + if (!(error_reporting() & E_USER_DEPRECATED)) { + return; + } + + $trace = debug_backtrace(); + if (isset($trace[$stackFrame])) { + $frame = $trace[$stackFrame]; + $frame += ['file' => '[internal]', 'line' => '??']; + + $relative = str_replace(DIRECTORY_SEPARATOR, '/', substr($frame['file'], strlen(ROOT) + 1)); + $patterns = (array)Configure::read('Error.ignoredDeprecationPaths'); + foreach ($patterns as $pattern) { + $pattern = str_replace(DIRECTORY_SEPARATOR, '/', $pattern); + if (fnmatch($pattern, $relative)) { + return; + } + } + + $message = sprintf( + "%s\n%s, line: %s\n" . + 'You can disable all deprecation warnings by setting `Error.errorLevel` to ' . + '`E_ALL & ~E_USER_DEPRECATED`. Adding `%s` to `Error.ignoredDeprecationPaths` ' . + 'in your `config/app.php` config will mute deprecations from that file only.', + $message, + $frame['file'], + $frame['line'], + $relative + ); + } + + static $errors = []; + $checksum = md5($message); + $duplicate = (bool)Configure::read('Error.allowDuplicateDeprecations', false); + if (isset($errors[$checksum]) && !$duplicate) { + return; + } + if (!$duplicate) { + $errors[$checksum] = true; + } + + trigger_error($message, E_USER_DEPRECATED); + } +} + +if (!function_exists('getTypeName')) { + /** + * Returns the objects class or var type of it's not an object + * + * @param mixed $var Variable to check + * @return string Returns the class name or variable type + */ + function getTypeName($var): string + { + return is_object($var) ? get_class($var) : gettype($var); + } +} diff --git a/vendor/cakephp/database/Connection.php b/vendor/cakephp/database/Connection.php new file mode 100644 index 0000000..5153d0e --- /dev/null +++ b/vendor/cakephp/database/Connection.php @@ -0,0 +1,966 @@ + + */ + protected $_config; + + /** + * Driver object, responsible for creating the real connection + * and provide specific SQL dialect. + * + * @var \Cake\Database\DriverInterface + */ + protected $_driver; + + /** + * Contains how many nested transactions have been started. + * + * @var int + */ + protected $_transactionLevel = 0; + + /** + * Whether a transaction is active in this connection. + * + * @var bool + */ + protected $_transactionStarted = false; + + /** + * Whether this connection can and should use savepoints for nested + * transactions. + * + * @var bool + */ + protected $_useSavePoints = false; + + /** + * Whether to log queries generated during this connection. + * + * @var bool + */ + protected $_logQueries = false; + + /** + * Logger object instance. + * + * @var \Psr\Log\LoggerInterface|null + */ + protected $_logger; + + /** + * Cacher object instance. + * + * @var \Psr\SimpleCache\CacheInterface|null + */ + protected $cacher; + + /** + * The schema collection object + * + * @var \Cake\Database\Schema\CollectionInterface|null + */ + protected $_schemaCollection; + + /** + * NestedTransactionRollbackException object instance, will be stored if + * the rollback method is called in some nested transaction. + * + * @var \Cake\Database\Exception\NestedTransactionRollbackException|null + */ + protected $nestedTransactionRollbackException; + + /** + * Constructor. + * + * ### Available options: + * + * - `driver` Sort name or FCQN for driver. + * - `log` Boolean indicating whether to use query logging. + * - `name` Connection name. + * - `cacheMetaData` Boolean indicating whether metadata (datasource schemas) should be cached. + * If set to a string it will be used as the name of cache config to use. + * - `cacheKeyPrefix` Custom prefix to use when generation cache keys. Defaults to connection name. + * + * @param array $config Configuration array. + */ + public function __construct(array $config) + { + $this->_config = $config; + + $driver = ''; + if (!empty($config['driver'])) { + $driver = $config['driver']; + } + $this->setDriver($driver, $config); + + if (!empty($config['log'])) { + $this->enableQueryLogging((bool)$config['log']); + } + } + + /** + * Destructor + * + * Disconnects the driver to release the connection. + */ + public function __destruct() + { + if ($this->_transactionStarted && class_exists(Log::class)) { + Log::warning('The connection is going to be closed but there is an active transaction.'); + } + } + + /** + * @inheritDoc + */ + public function config(): array + { + return $this->_config; + } + + /** + * @inheritDoc + */ + public function configName(): string + { + return $this->_config['name'] ?? ''; + } + + /** + * Sets the driver instance. If a string is passed it will be treated + * as a class name and will be instantiated. + * + * @param \Cake\Database\DriverInterface|string $driver The driver instance to use. + * @param array $config Config for a new driver. + * @throws \Cake\Database\Exception\MissingDriverException When a driver class is missing. + * @throws \Cake\Database\Exception\MissingExtensionException When a driver's PHP extension is missing. + * @return $this + */ + public function setDriver($driver, $config = []) + { + if (is_string($driver)) { + /** @psalm-var class-string<\Cake\Database\DriverInterface>|null $className */ + $className = App::className($driver, 'Database/Driver'); + if ($className === null) { + throw new MissingDriverException(['driver' => $driver]); + } + $driver = new $className($config); + } + if (!$driver->enabled()) { + throw new MissingExtensionException(['driver' => get_class($driver), 'name' => $config['name']]); + } + + $this->_driver = $driver; + + return $this; + } + + /** + * Get the retry wrapper object that is allows recovery from server disconnects + * while performing certain database actions, such as executing a query. + * + * @return \Cake\Core\Retry\CommandRetry The retry wrapper + */ + public function getDisconnectRetry(): CommandRetry + { + return new CommandRetry(new ReconnectStrategy($this)); + } + + /** + * Gets the driver instance. + * + * @return \Cake\Database\DriverInterface + */ + public function getDriver(): DriverInterface + { + return $this->_driver; + } + + /** + * Connects to the configured database. + * + * @throws \Cake\Database\Exception\MissingConnectionException If database connection could not be established. + * @return bool true, if the connection was already established or the attempt was successful. + */ + public function connect(): bool + { + try { + return $this->_driver->connect(); + } catch (MissingConnectionException $e) { + throw $e; + } catch (Throwable $e) { + throw new MissingConnectionException( + [ + 'driver' => App::shortName(get_class($this->_driver), 'Database/Driver'), + 'reason' => $e->getMessage(), + ], + null, + $e + ); + } + } + + /** + * Disconnects from database server. + * + * @return void + */ + public function disconnect(): void + { + $this->_driver->disconnect(); + } + + /** + * Returns whether connection to database server was already established. + * + * @return bool + */ + public function isConnected(): bool + { + return $this->_driver->isConnected(); + } + + /** + * Prepares a SQL statement to be executed. + * + * @param \Cake\Database\Query|string $query The SQL to convert into a prepared statement. + * @return \Cake\Database\StatementInterface + */ + public function prepare($query): StatementInterface + { + return $this->getDisconnectRetry()->run(function () use ($query) { + $statement = $this->_driver->prepare($query); + + if ($this->_logQueries) { + $statement = $this->_newLogger($statement); + } + + return $statement; + }); + } + + /** + * Executes a query using $params for interpolating values and $types as a hint for each + * those params. + * + * @param string $sql SQL to be executed and interpolated with $params + * @param array $params list or associative array of params to be interpolated in $sql as values + * @param array $types list or associative array of types to be used for casting values in query + * @return \Cake\Database\StatementInterface executed statement + */ + public function execute(string $sql, array $params = [], array $types = []): StatementInterface + { + return $this->getDisconnectRetry()->run(function () use ($sql, $params, $types) { + $statement = $this->prepare($sql); + if (!empty($params)) { + $statement->bind($params, $types); + } + $statement->execute(); + + return $statement; + }); + } + + /** + * Compiles a Query object into a SQL string according to the dialect for this + * connection's driver + * + * @param \Cake\Database\Query $query The query to be compiled + * @param \Cake\Database\ValueBinder $binder Value binder + * @return string + */ + public function compileQuery(Query $query, ValueBinder $binder): string + { + return $this->getDriver()->compileQuery($query, $binder)[1]; + } + + /** + * Executes the provided query after compiling it for the specific driver + * dialect and returns the executed Statement object. + * + * @param \Cake\Database\Query $query The query to be executed + * @return \Cake\Database\StatementInterface executed statement + */ + public function run(Query $query): StatementInterface + { + return $this->getDisconnectRetry()->run(function () use ($query) { + $statement = $this->prepare($query); + $query->getValueBinder()->attachTo($statement); + $statement->execute(); + + return $statement; + }); + } + + /** + * Executes a SQL statement and returns the Statement object as result. + * + * @param string $sql The SQL query to execute. + * @return \Cake\Database\StatementInterface + */ + public function query(string $sql): StatementInterface + { + return $this->getDisconnectRetry()->run(function () use ($sql) { + $statement = $this->prepare($sql); + $statement->execute(); + + return $statement; + }); + } + + /** + * Create a new Query instance for this connection. + * + * @return \Cake\Database\Query + */ + public function newQuery(): Query + { + return new Query($this); + } + + /** + * Sets a Schema\Collection object for this connection. + * + * @param \Cake\Database\Schema\CollectionInterface $collection The schema collection object + * @return $this + */ + public function setSchemaCollection(SchemaCollectionInterface $collection) + { + $this->_schemaCollection = $collection; + + return $this; + } + + /** + * Gets a Schema\Collection object for this connection. + * + * @return \Cake\Database\Schema\CollectionInterface + */ + public function getSchemaCollection(): SchemaCollectionInterface + { + if ($this->_schemaCollection !== null) { + return $this->_schemaCollection; + } + + if (!empty($this->_config['cacheMetadata'])) { + return $this->_schemaCollection = new CachedCollection( + new SchemaCollection($this), + empty($this->_config['cacheKeyPrefix']) ? $this->configName() : $this->_config['cacheKeyPrefix'], + $this->getCacher() + ); + } + + return $this->_schemaCollection = new SchemaCollection($this); + } + + /** + * Executes an INSERT query on the specified table. + * + * @param string $table the table to insert values in + * @param array $values values to be inserted + * @param array $types list of associative array containing the types to be used for casting + * @return \Cake\Database\StatementInterface + */ + public function insert(string $table, array $values, array $types = []): StatementInterface + { + return $this->getDisconnectRetry()->run(function () use ($table, $values, $types) { + $columns = array_keys($values); + + return $this->newQuery()->insert($columns, $types) + ->into($table) + ->values($values) + ->execute(); + }); + } + + /** + * Executes an UPDATE statement on the specified table. + * + * @param string $table the table to update rows from + * @param array $values values to be updated + * @param array $conditions conditions to be set for update statement + * @param array $types list of associative array containing the types to be used for casting + * @return \Cake\Database\StatementInterface + */ + public function update(string $table, array $values, array $conditions = [], array $types = []): StatementInterface + { + return $this->getDisconnectRetry()->run(function () use ($table, $values, $conditions, $types) { + return $this->newQuery()->update($table) + ->set($values, $types) + ->where($conditions, $types) + ->execute(); + }); + } + + /** + * Executes a DELETE statement on the specified table. + * + * @param string $table the table to delete rows from + * @param array $conditions conditions to be set for delete statement + * @param array $types list of associative array containing the types to be used for casting + * @return \Cake\Database\StatementInterface + */ + public function delete(string $table, array $conditions = [], array $types = []): StatementInterface + { + return $this->getDisconnectRetry()->run(function () use ($table, $conditions, $types) { + return $this->newQuery()->delete($table) + ->where($conditions, $types) + ->execute(); + }); + } + + /** + * Starts a new transaction. + * + * @return void + */ + public function begin(): void + { + if (!$this->_transactionStarted) { + if ($this->_logQueries) { + $this->log('BEGIN'); + } + + $this->getDisconnectRetry()->run(function (): void { + $this->_driver->beginTransaction(); + }); + + $this->_transactionLevel = 0; + $this->_transactionStarted = true; + $this->nestedTransactionRollbackException = null; + + return; + } + + $this->_transactionLevel++; + if ($this->isSavePointsEnabled()) { + $this->createSavePoint((string)$this->_transactionLevel); + } + } + + /** + * Commits current transaction. + * + * @return bool true on success, false otherwise + */ + public function commit(): bool + { + if (!$this->_transactionStarted) { + return false; + } + + if ($this->_transactionLevel === 0) { + if ($this->wasNestedTransactionRolledback()) { + /** @var \Cake\Database\Exception\NestedTransactionRollbackException $e */ + $e = $this->nestedTransactionRollbackException; + $this->nestedTransactionRollbackException = null; + throw $e; + } + + $this->_transactionStarted = false; + $this->nestedTransactionRollbackException = null; + if ($this->_logQueries) { + $this->log('COMMIT'); + } + + return $this->_driver->commitTransaction(); + } + if ($this->isSavePointsEnabled()) { + $this->releaseSavePoint((string)$this->_transactionLevel); + } + + $this->_transactionLevel--; + + return true; + } + + /** + * Rollback current transaction. + * + * @param bool|null $toBeginning Whether the transaction should be rolled back to the + * beginning of it. Defaults to false if using savepoints, or true if not. + * @return bool + */ + public function rollback(?bool $toBeginning = null): bool + { + if (!$this->_transactionStarted) { + return false; + } + + $useSavePoint = $this->isSavePointsEnabled(); + if ($toBeginning === null) { + $toBeginning = !$useSavePoint; + } + if ($this->_transactionLevel === 0 || $toBeginning) { + $this->_transactionLevel = 0; + $this->_transactionStarted = false; + $this->nestedTransactionRollbackException = null; + if ($this->_logQueries) { + $this->log('ROLLBACK'); + } + $this->_driver->rollbackTransaction(); + + return true; + } + + $savePoint = $this->_transactionLevel--; + if ($useSavePoint) { + $this->rollbackSavepoint($savePoint); + } elseif ($this->nestedTransactionRollbackException === null) { + $this->nestedTransactionRollbackException = new NestedTransactionRollbackException(); + } + + return true; + } + + /** + * Enables/disables the usage of savepoints, enables only if driver the allows it. + * + * If you are trying to enable this feature, make sure you check + * `isSavePointsEnabled()` to verify that savepoints were enabled successfully. + * + * @param bool $enable Whether save points should be used. + * @return $this + */ + public function enableSavePoints(bool $enable = true) + { + if ($enable === false) { + $this->_useSavePoints = false; + } else { + $this->_useSavePoints = $this->_driver->supports(DriverInterface::FEATURE_SAVEPOINT); + } + + return $this; + } + + /** + * Disables the usage of savepoints. + * + * @return $this + */ + public function disableSavePoints() + { + $this->_useSavePoints = false; + + return $this; + } + + /** + * Returns whether this connection is using savepoints for nested transactions + * + * @return bool true if enabled, false otherwise + */ + public function isSavePointsEnabled(): bool + { + return $this->_useSavePoints; + } + + /** + * Creates a new save point for nested transactions. + * + * @param string|int $name Save point name or id + * @return void + */ + public function createSavePoint($name): void + { + $this->execute($this->_driver->savePointSQL($name))->closeCursor(); + } + + /** + * Releases a save point by its name. + * + * @param string|int $name Save point name or id + * @return void + */ + public function releaseSavePoint($name): void + { + $sql = $this->_driver->releaseSavePointSQL($name); + if ($sql) { + $this->execute($sql)->closeCursor(); + } + } + + /** + * Rollback a save point by its name. + * + * @param string|int $name Save point name or id + * @return void + */ + public function rollbackSavepoint($name): void + { + $this->execute($this->_driver->rollbackSavePointSQL($name))->closeCursor(); + } + + /** + * Run driver specific SQL to disable foreign key checks. + * + * @return void + */ + public function disableForeignKeys(): void + { + $this->getDisconnectRetry()->run(function (): void { + $this->execute($this->_driver->disableForeignKeySQL())->closeCursor(); + }); + } + + /** + * Run driver specific SQL to enable foreign key checks. + * + * @return void + */ + public function enableForeignKeys(): void + { + $this->getDisconnectRetry()->run(function (): void { + $this->execute($this->_driver->enableForeignKeySQL())->closeCursor(); + }); + } + + /** + * Returns whether the driver supports adding or dropping constraints + * to already created tables. + * + * @return bool true if driver supports dynamic constraints + * @deprecated 4.3.0 Fixtures no longer dynamically drop and create constraints. + */ + public function supportsDynamicConstraints(): bool + { + return $this->_driver->supportsDynamicConstraints(); + } + + /** + * @inheritDoc + */ + public function transactional(callable $callback) + { + $this->begin(); + + try { + $result = $callback($this); + } catch (Throwable $e) { + $this->rollback(false); + throw $e; + } + + if ($result === false) { + $this->rollback(false); + + return false; + } + + try { + $this->commit(); + } catch (NestedTransactionRollbackException $e) { + $this->rollback(false); + throw $e; + } + + return $result; + } + + /** + * Returns whether some nested transaction has been already rolled back. + * + * @return bool + */ + protected function wasNestedTransactionRolledback(): bool + { + return $this->nestedTransactionRollbackException instanceof NestedTransactionRollbackException; + } + + /** + * @inheritDoc + */ + public function disableConstraints(callable $callback) + { + return $this->getDisconnectRetry()->run(function () use ($callback) { + $this->disableForeignKeys(); + + try { + $result = $callback($this); + } finally { + $this->enableForeignKeys(); + } + + return $result; + }); + } + + /** + * Checks if a transaction is running. + * + * @return bool True if a transaction is running else false. + */ + public function inTransaction(): bool + { + return $this->_transactionStarted; + } + + /** + * Quotes value to be used safely in database query. + * + * This uses `PDO::quote()` and requires `supportsQuoting()` to work. + * + * @param mixed $value The value to quote. + * @param \Cake\Database\TypeInterface|string|int $type Type to be used for determining kind of quoting to perform + * @return string Quoted value + */ + public function quote($value, $type = 'string'): string + { + [$value, $type] = $this->cast($value, $type); + + return $this->_driver->quote($value, $type); + } + + /** + * Checks if using `quote()` is supported. + * + * This is not required to use `quoteIdentifier()`. + * + * @return bool + */ + public function supportsQuoting(): bool + { + return $this->_driver->supports(DriverInterface::FEATURE_QUOTE); + } + + /** + * Quotes a database identifier (a column name, table name, etc..) to + * be used safely in queries without the risk of using reserved words. + * + * This does not require `supportsQuoting()` to work. + * + * @param string $identifier The identifier to quote. + * @return string + */ + public function quoteIdentifier(string $identifier): string + { + return $this->_driver->quoteIdentifier($identifier); + } + + /** + * Enables or disables metadata caching for this connection + * + * Changing this setting will not modify existing schema collections objects. + * + * @param string|bool $cache Either boolean false to disable metadata caching, or + * true to use `_cake_model_` or the name of the cache config to use. + * @return void + */ + public function cacheMetadata($cache): void + { + $this->_schemaCollection = null; + $this->_config['cacheMetadata'] = $cache; + if (is_string($cache)) { + $this->cacher = null; + } + } + + /** + * @inheritDoc + */ + public function setCacher(CacheInterface $cacher) + { + $this->cacher = $cacher; + + return $this; + } + + /** + * @inheritDoc + */ + public function getCacher(): CacheInterface + { + if ($this->cacher !== null) { + return $this->cacher; + } + + $configName = $this->_config['cacheMetadata'] ?? '_cake_model_'; + if (!is_string($configName)) { + $configName = '_cake_model_'; + } + + if (!class_exists(Cache::class)) { + throw new RuntimeException( + 'To use caching you must either set a cacher using Connection::setCacher()' . + ' or require the cakephp/cache package in your composer config.' + ); + } + + return $this->cacher = Cache::pool($configName); + } + + /** + * Enable/disable query logging + * + * @param bool $enable Enable/disable query logging + * @return $this + */ + public function enableQueryLogging(bool $enable = true) + { + $this->_logQueries = $enable; + + return $this; + } + + /** + * Disable query logging + * + * @return $this + */ + public function disableQueryLogging() + { + $this->_logQueries = false; + + return $this; + } + + /** + * Check if query logging is enabled. + * + * @return bool + */ + public function isQueryLoggingEnabled(): bool + { + return $this->_logQueries; + } + + /** + * Sets a logger + * + * @param \Psr\Log\LoggerInterface $logger Logger object + * @return $this + * @psalm-suppress ImplementedReturnTypeMismatch + */ + public function setLogger(LoggerInterface $logger) + { + $this->_logger = $logger; + + return $this; + } + + /** + * Gets the logger object + * + * @return \Psr\Log\LoggerInterface logger instance + */ + public function getLogger(): LoggerInterface + { + if ($this->_logger !== null) { + return $this->_logger; + } + + if (!class_exists(QueryLogger::class)) { + throw new RuntimeException( + 'For logging you must either set a logger using Connection::setLogger()' . + ' or require the cakephp/log package in your composer config.' + ); + } + + return $this->_logger = new QueryLogger(['connection' => $this->configName()]); + } + + /** + * Logs a Query string using the configured logger object. + * + * @param string $sql string to be logged + * @return void + */ + public function log(string $sql): void + { + $query = new LoggedQuery(); + $query->query = $sql; + $this->getLogger()->debug((string)$query, ['query' => $query]); + } + + /** + * Returns a new statement object that will log the activity + * for the passed original statement instance. + * + * @param \Cake\Database\StatementInterface $statement the instance to be decorated + * @return \Cake\Database\Log\LoggingStatement + */ + protected function _newLogger(StatementInterface $statement): LoggingStatement + { + $log = new LoggingStatement($statement, $this->_driver); + $log->setLogger($this->getLogger()); + + return $log; + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo(): array + { + $secrets = [ + 'password' => '*****', + 'username' => '*****', + 'host' => '*****', + 'database' => '*****', + 'port' => '*****', + ]; + $replace = array_intersect_key($secrets, $this->_config); + $config = $replace + $this->_config; + + return [ + 'config' => $config, + 'driver' => $this->_driver, + 'transactionLevel' => $this->_transactionLevel, + 'transactionStarted' => $this->_transactionStarted, + 'useSavePoints' => $this->_useSavePoints, + 'logQueries' => $this->_logQueries, + 'logger' => $this->_logger, + ]; + } +} diff --git a/vendor/cakephp/database/ConstraintsInterface.php b/vendor/cakephp/database/ConstraintsInterface.php new file mode 100644 index 0000000..f1fe3c1 --- /dev/null +++ b/vendor/cakephp/database/ConstraintsInterface.php @@ -0,0 +1,49 @@ + DB-specific error codes that allow connect retry + */ + protected const RETRY_ERROR_CODES = []; + + /** + * Instance of PDO. + * + * @var \PDO + */ + protected $_connection; + + /** + * Configuration data. + * + * @var array + */ + protected $_config; + + /** + * Base configuration that is merged into the user + * supplied configuration data. + * + * @var array + */ + protected $_baseConfig = []; + + /** + * Indicates whether the driver is doing automatic identifier quoting + * for all queries + * + * @var bool + */ + protected $_autoQuoting = false; + + /** + * The server version + * + * @var string|null + */ + protected $_version; + + /** + * The last number of connection retry attempts. + * + * @var int + */ + protected $connectRetries = 0; + + /** + * Constructor + * + * @param array $config The configuration for the driver. + * @throws \InvalidArgumentException + */ + public function __construct(array $config = []) + { + if (empty($config['username']) && !empty($config['login'])) { + throw new InvalidArgumentException( + 'Please pass "username" instead of "login" for connecting to the database' + ); + } + $config += $this->_baseConfig; + $this->_config = $config; + if (!empty($config['quoteIdentifiers'])) { + $this->enableAutoQuoting(); + } + } + + /** + * Establishes a connection to the database server + * + * @param string $dsn A Driver-specific PDO-DSN + * @param array $config configuration to be used for creating connection + * @return bool true on success + */ + protected function _connect(string $dsn, array $config): bool + { + $action = function () use ($dsn, $config) { + $this->setConnection(new PDO( + $dsn, + $config['username'] ?: null, + $config['password'] ?: null, + $config['flags'] + )); + }; + + $retry = new CommandRetry(new ErrorCodeWaitStrategy(static::RETRY_ERROR_CODES, 5), 4); + try { + $retry->run($action); + } catch (PDOException $e) { + throw new MissingConnectionException( + [ + 'driver' => App::shortName(static::class, 'Database/Driver'), + 'reason' => $e->getMessage(), + ], + null, + $e + ); + } finally { + $this->connectRetries = $retry->getRetries(); + } + + return true; + } + + /** + * @inheritDoc + */ + abstract public function connect(): bool; + + /** + * @inheritDoc + */ + public function disconnect(): void + { + /** @psalm-suppress PossiblyNullPropertyAssignmentValue */ + $this->_connection = null; + $this->_version = null; + } + + /** + * Returns connected server version. + * + * @return string + */ + public function version(): string + { + if ($this->_version === null) { + $this->connect(); + $this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + return $this->_version; + } + + /** + * Get the internal PDO connection instance. + * + * @return \PDO + */ + public function getConnection() + { + if ($this->_connection === null) { + throw new MissingConnectionException([ + 'driver' => App::shortName(static::class, 'Database/Driver'), + 'reason' => 'Unknown', + ]); + } + + return $this->_connection; + } + + /** + * Set the internal PDO connection instance. + * + * @param \PDO $connection PDO instance. + * @return $this + * @psalm-suppress MoreSpecificImplementedParamType + */ + public function setConnection($connection) + { + $this->_connection = $connection; + + return $this; + } + + /** + * @inheritDoc + */ + abstract public function enabled(): bool; + + /** + * @inheritDoc + */ + public function prepare($query): StatementInterface + { + $this->connect(); + $statement = $this->_connection->prepare($query instanceof Query ? $query->sql() : $query); + + return new PDOStatement($statement, $this); + } + + /** + * @inheritDoc + */ + public function beginTransaction(): bool + { + $this->connect(); + if ($this->_connection->inTransaction()) { + return true; + } + + return $this->_connection->beginTransaction(); + } + + /** + * @inheritDoc + */ + public function commitTransaction(): bool + { + $this->connect(); + if (!$this->_connection->inTransaction()) { + return false; + } + + return $this->_connection->commit(); + } + + /** + * @inheritDoc + */ + public function rollbackTransaction(): bool + { + $this->connect(); + if (!$this->_connection->inTransaction()) { + return false; + } + + return $this->_connection->rollBack(); + } + + /** + * Returns whether a transaction is active for connection. + * + * @return bool + */ + public function inTransaction(): bool + { + $this->connect(); + + return $this->_connection->inTransaction(); + } + + /** + * @inheritDoc + */ + public function supportsSavePoints(): bool + { + deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.'); + + return $this->supports(static::FEATURE_SAVEPOINT); + } + + /** + * Returns true if the server supports common table expressions. + * + * @return bool + * @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead + */ + public function supportsCTEs(): bool + { + deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.'); + + return $this->supports(static::FEATURE_CTE); + } + + /** + * @inheritDoc + */ + public function quote($value, $type = PDO::PARAM_STR): string + { + $this->connect(); + + return $this->_connection->quote((string)$value, $type); + } + + /** + * Checks if the driver supports quoting, as PDO_ODBC does not support it. + * + * @return bool + * @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead + */ + public function supportsQuoting(): bool + { + deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.'); + + return $this->supports(static::FEATURE_QUOTE); + } + + /** + * @inheritDoc + */ + abstract public function queryTranslator(string $type): Closure; + + /** + * @inheritDoc + */ + abstract public function schemaDialect(): SchemaDialect; + + /** + * @inheritDoc + */ + abstract public function quoteIdentifier(string $identifier): string; + + /** + * @inheritDoc + */ + public function schemaValue($value): string + { + if ($value === null) { + return 'NULL'; + } + if ($value === false) { + return 'FALSE'; + } + if ($value === true) { + return 'TRUE'; + } + if (is_float($value)) { + return str_replace(',', '.', (string)$value); + } + /** @psalm-suppress InvalidArgument */ + if ( + ( + is_int($value) || + $value === '0' + ) || + ( + is_numeric($value) && + strpos($value, ',') === false && + substr($value, 0, 1) !== '0' && + strpos($value, 'e') === false + ) + ) { + return (string)$value; + } + + return $this->_connection->quote((string)$value, PDO::PARAM_STR); + } + + /** + * @inheritDoc + */ + public function schema(): string + { + return $this->_config['schema']; + } + + /** + * @inheritDoc + */ + public function lastInsertId(?string $table = null, ?string $column = null) + { + $this->connect(); + + if ($this->_connection instanceof PDO) { + return $this->_connection->lastInsertId($table); + } + + return $this->_connection->lastInsertId($table); + } + + /** + * @inheritDoc + */ + public function isConnected(): bool + { + if ($this->_connection === null) { + $connected = false; + } else { + try { + $connected = (bool)$this->_connection->query('SELECT 1'); + } catch (PDOException $e) { + $connected = false; + } + } + + return $connected; + } + + /** + * @inheritDoc + */ + public function enableAutoQuoting(bool $enable = true) + { + $this->_autoQuoting = $enable; + + return $this; + } + + /** + * @inheritDoc + */ + public function disableAutoQuoting() + { + $this->_autoQuoting = false; + + return $this; + } + + /** + * @inheritDoc + */ + public function isAutoQuotingEnabled(): bool + { + return $this->_autoQuoting; + } + + /** + * Returns whether the driver supports the feature. + * + * Defaults to true for FEATURE_QUOTE and FEATURE_SAVEPOINT. + * + * @param string $feature Driver feature name + * @return bool + */ + public function supports(string $feature): bool + { + switch ($feature) { + case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION: + case static::FEATURE_QUOTE: + case static::FEATURE_SAVEPOINT: + return true; + } + + return false; + } + + /** + * @inheritDoc + */ + public function compileQuery(Query $query, ValueBinder $binder): array + { + $processor = $this->newCompiler(); + $translator = $this->queryTranslator($query->type()); + $query = $translator($query); + + return [$query, $processor->compile($query, $binder)]; + } + + /** + * @inheritDoc + */ + public function newCompiler(): QueryCompiler + { + return new QueryCompiler(); + } + + /** + * @inheritDoc + */ + public function newTableSchema(string $table, array $columns = []): TableSchema + { + $className = TableSchema::class; + if (isset($this->_config['tableSchema'])) { + /** @var class-string<\Cake\Database\Schema\TableSchema> $className */ + $className = $this->_config['tableSchema']; + } + + return new $className($table, $columns); + } + + /** + * Returns the maximum alias length allowed. + * This can be different from the maximum identifier length for columns. + * + * @return int|null Maximum alias length or null if no limit + */ + public function getMaxAliasLength(): ?int + { + return static::MAX_ALIAS_LENGTH; + } + + /** + * Returns the number of connection retry attempts made. + * + * @return int + */ + public function getConnectRetries(): int + { + return $this->connectRetries; + } + + /** + * Destructor + */ + public function __destruct() + { + /** @psalm-suppress PossiblyNullPropertyAssignmentValue */ + $this->_connection = null; + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'connected' => $this->_connection !== null, + ]; + } +} diff --git a/vendor/cakephp/database/Driver/Mysql.php b/vendor/cakephp/database/Driver/Mysql.php new file mode 100644 index 0000000..68f46ff --- /dev/null +++ b/vendor/cakephp/database/Driver/Mysql.php @@ -0,0 +1,344 @@ + + */ + protected $_baseConfig = [ + 'persistent' => true, + 'host' => 'localhost', + 'username' => 'root', + 'password' => '', + 'database' => 'cake', + 'port' => '3306', + 'flags' => [], + 'encoding' => 'utf8mb4', + 'timezone' => null, + 'init' => [], + ]; + + /** + * The schema dialect for this driver + * + * @var \Cake\Database\Schema\MysqlSchemaDialect|null + */ + protected $_schemaDialect; + + /** + * String used to start a database identifier quoting to make it safe + * + * @var string + */ + protected $_startQuote = '`'; + + /** + * String used to end a database identifier quoting to make it safe + * + * @var string + */ + protected $_endQuote = '`'; + + /** + * Server type. + * + * If the underlying server is MariaDB, its value will get set to `'mariadb'` + * after `version()` method is called. + * + * @var string + */ + protected $serverType = self::SERVER_TYPE_MYSQL; + + /** + * Mapping of feature to db server version for feature availability checks. + * + * @var array> + */ + protected $featureVersions = [ + 'mysql' => [ + 'json' => '5.7.0', + 'cte' => '8.0.0', + 'window' => '8.0.0', + ], + 'mariadb' => [ + 'json' => '10.2.7', + 'cte' => '10.2.1', + 'window' => '10.2.0', + ], + ]; + + /** + * Establishes a connection to the database server + * + * @return bool true on success + */ + public function connect(): bool + { + if ($this->_connection) { + return true; + } + $config = $this->_config; + + if ($config['timezone'] === 'UTC') { + $config['timezone'] = '+0:00'; + } + + if (!empty($config['timezone'])) { + $config['init'][] = sprintf("SET time_zone = '%s'", $config['timezone']); + } + + $config['flags'] += [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ]; + + if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) { + $config['flags'][PDO::MYSQL_ATTR_SSL_KEY] = $config['ssl_key']; + $config['flags'][PDO::MYSQL_ATTR_SSL_CERT] = $config['ssl_cert']; + } + if (!empty($config['ssl_ca'])) { + $config['flags'][PDO::MYSQL_ATTR_SSL_CA] = $config['ssl_ca']; + } + + if (empty($config['unix_socket'])) { + $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; + } else { + $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}"; + } + + if (!empty($config['encoding'])) { + $dsn .= ";charset={$config['encoding']}"; + } + + $this->_connect($dsn, $config); + + if (!empty($config['init'])) { + $connection = $this->getConnection(); + foreach ((array)$config['init'] as $command) { + $connection->exec($command); + } + } + + return true; + } + + /** + * Returns whether php is able to use this driver for connecting to database + * + * @return bool true if it is valid to use this driver + */ + public function enabled(): bool + { + return in_array('mysql', PDO::getAvailableDrivers(), true); + } + + /** + * Prepares a sql statement to be executed + * + * @param \Cake\Database\Query|string $query The query to prepare. + * @return \Cake\Database\StatementInterface + */ + public function prepare($query): StatementInterface + { + $this->connect(); + $isObject = $query instanceof Query; + /** + * @psalm-suppress PossiblyInvalidMethodCall + * @psalm-suppress PossiblyInvalidArgument + */ + $statement = $this->_connection->prepare($isObject ? $query->sql() : $query); + $result = new MysqlStatement($statement, $this); + /** @psalm-suppress PossiblyInvalidMethodCall */ + if ($isObject && $query->isBufferedResultsEnabled() === false) { + $result->bufferResults(false); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function schemaDialect(): SchemaDialect + { + if ($this->_schemaDialect === null) { + $this->_schemaDialect = new MysqlSchemaDialect($this); + } + + return $this->_schemaDialect; + } + + /** + * @inheritDoc + */ + public function schema(): string + { + return $this->_config['database']; + } + + /** + * @inheritDoc + */ + public function disableForeignKeySQL(): string + { + return 'SET foreign_key_checks = 0'; + } + + /** + * @inheritDoc + */ + public function enableForeignKeySQL(): string + { + return 'SET foreign_key_checks = 1'; + } + + /** + * @inheritDoc + */ + public function supports(string $feature): bool + { + switch ($feature) { + case static::FEATURE_CTE: + case static::FEATURE_JSON: + case static::FEATURE_WINDOW: + return version_compare( + $this->version(), + $this->featureVersions[$this->serverType][$feature], + '>=' + ); + } + + return parent::supports($feature); + } + + /** + * @inheritDoc + */ + public function supportsDynamicConstraints(): bool + { + return true; + } + + /** + * Returns true if the connected server is MariaDB. + * + * @return bool + */ + public function isMariadb(): bool + { + $this->version(); + + return $this->serverType === static::SERVER_TYPE_MARIADB; + } + + /** + * Returns connected server version. + * + * @return string + */ + public function version(): string + { + if ($this->_version === null) { + $this->connect(); + $this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); + + if (strpos($this->_version, 'MariaDB') !== false) { + $this->serverType = static::SERVER_TYPE_MARIADB; + preg_match('/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-MariaDB[^:]*)/', $this->_version, $matches); + $this->_version = $matches[1]; + } + } + + return $this->_version; + } + + /** + * Returns true if the server supports common table expressions. + * + * @return bool + * @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_CTE)` instead + */ + public function supportsCTEs(): bool + { + deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.'); + + return $this->supports(static::FEATURE_CTE); + } + + /** + * Returns true if the server supports native JSON columns + * + * @return bool + * @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_JSON)` instead + */ + public function supportsNativeJson(): bool + { + deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.'); + + return $this->supports(static::FEATURE_JSON); + } + + /** + * Returns true if the connected server supports window functions. + * + * @return bool + * @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_WINDOW)` instead + */ + public function supportsWindowFunctions(): bool + { + deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.'); + + return $this->supports(static::FEATURE_WINDOW); + } +} diff --git a/vendor/cakephp/database/Driver/Postgres.php b/vendor/cakephp/database/Driver/Postgres.php new file mode 100644 index 0000000..2a32264 --- /dev/null +++ b/vendor/cakephp/database/Driver/Postgres.php @@ -0,0 +1,348 @@ + + */ + protected $_baseConfig = [ + 'persistent' => true, + 'host' => 'localhost', + 'username' => 'root', + 'password' => '', + 'database' => 'cake', + 'schema' => 'public', + 'port' => 5432, + 'encoding' => 'utf8', + 'timezone' => null, + 'flags' => [], + 'init' => [], + ]; + + /** + * The schema dialect class for this driver + * + * @var \Cake\Database\Schema\PostgresSchemaDialect|null + */ + protected $_schemaDialect; + + /** + * String used to start a database identifier quoting to make it safe + * + * @var string + */ + protected $_startQuote = '"'; + + /** + * String used to end a database identifier quoting to make it safe + * + * @var string + */ + protected $_endQuote = '"'; + + /** + * Establishes a connection to the database server + * + * @return bool true on success + */ + public function connect(): bool + { + if ($this->_connection) { + return true; + } + $config = $this->_config; + $config['flags'] += [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ]; + if (empty($config['unix_socket'])) { + $dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; + } else { + $dsn = "pgsql:dbname={$config['database']}"; + } + + $this->_connect($dsn, $config); + $this->_connection = $connection = $this->getConnection(); + if (!empty($config['encoding'])) { + $this->setEncoding($config['encoding']); + } + + if (!empty($config['schema'])) { + $this->setSchema($config['schema']); + } + + if (!empty($config['timezone'])) { + $config['init'][] = sprintf('SET timezone = %s', $connection->quote($config['timezone'])); + } + + foreach ($config['init'] as $command) { + $connection->exec($command); + } + + return true; + } + + /** + * Returns whether php is able to use this driver for connecting to database + * + * @return bool true if it is valid to use this driver + */ + public function enabled(): bool + { + return in_array('pgsql', PDO::getAvailableDrivers(), true); + } + + /** + * @inheritDoc + */ + public function schemaDialect(): SchemaDialect + { + if ($this->_schemaDialect === null) { + $this->_schemaDialect = new PostgresSchemaDialect($this); + } + + return $this->_schemaDialect; + } + + /** + * Sets connection encoding + * + * @param string $encoding The encoding to use. + * @return void + */ + public function setEncoding(string $encoding): void + { + $this->connect(); + $this->_connection->exec('SET NAMES ' . $this->_connection->quote($encoding)); + } + + /** + * Sets connection default schema, if any relation defined in a query is not fully qualified + * postgres will fallback to looking the relation into defined default schema + * + * @param string $schema The schema names to set `search_path` to. + * @return void + */ + public function setSchema(string $schema): void + { + $this->connect(); + $this->_connection->exec('SET search_path TO ' . $this->_connection->quote($schema)); + } + + /** + * @inheritDoc + */ + public function disableForeignKeySQL(): string + { + return 'SET CONSTRAINTS ALL DEFERRED'; + } + + /** + * @inheritDoc + */ + public function enableForeignKeySQL(): string + { + return 'SET CONSTRAINTS ALL IMMEDIATE'; + } + + /** + * @inheritDoc + */ + public function supports(string $feature): bool + { + switch ($feature) { + case static::FEATURE_CTE: + case static::FEATURE_JSON: + case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS: + case static::FEATURE_WINDOW: + return true; + + case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION: + return false; + } + + return parent::supports($feature); + } + + /** + * @inheritDoc + */ + public function supportsDynamicConstraints(): bool + { + return true; + } + + /** + * @inheritDoc + */ + protected function _transformDistinct(Query $query): Query + { + return $query; + } + + /** + * @inheritDoc + */ + protected function _insertQueryTranslator(Query $query): Query + { + if (!$query->clause('epilog')) { + $query->epilog('RETURNING *'); + } + + return $query; + } + + /** + * @inheritDoc + */ + protected function _expressionTranslators(): array + { + return [ + IdentifierExpression::class => '_transformIdentifierExpression', + FunctionExpression::class => '_transformFunctionExpression', + StringExpression::class => '_transformStringExpression', + ]; + } + + /** + * Changes identifer expression into postgresql format. + * + * @param \Cake\Database\Expression\IdentifierExpression $expression The expression to tranform. + * @return void + */ + protected function _transformIdentifierExpression(IdentifierExpression $expression): void + { + $collation = $expression->getCollation(); + if ($collation) { + // use trim() to work around expression being transformed multiple times + $expression->setCollation('"' . trim($collation, '"') . '"'); + } + } + + /** + * Receives a FunctionExpression and changes it so that it conforms to this + * SQL dialect. + * + * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert + * to postgres SQL. + * @return void + */ + protected function _transformFunctionExpression(FunctionExpression $expression): void + { + switch ($expression->getName()) { + case 'CONCAT': + // CONCAT function is expressed as exp1 || exp2 + $expression->setName('')->setConjunction(' ||'); + break; + case 'DATEDIFF': + $expression + ->setName('') + ->setConjunction('-') + ->iterateParts(function ($p) { + if (is_string($p)) { + $p = ['value' => [$p => 'literal'], 'type' => null]; + } else { + $p['value'] = [$p['value']]; + } + + return new FunctionExpression('DATE', $p['value'], [$p['type']]); + }); + break; + case 'CURRENT_DATE': + $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']); + $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']); + break; + case 'CURRENT_TIME': + $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']); + $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']); + break; + case 'NOW': + $expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']); + break; + case 'RAND': + $expression->setName('RANDOM'); + break; + case 'DATE_ADD': + $expression + ->setName('') + ->setConjunction(' + INTERVAL') + ->iterateParts(function ($p, $key) { + if ($key === 1) { + $p = sprintf("'%s'", $p); + } + + return $p; + }); + break; + case 'DAYOFWEEK': + $expression + ->setName('EXTRACT') + ->setConjunction(' ') + ->add(['DOW FROM' => 'literal'], [], true) + ->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1 + break; + } + } + + /** + * Changes string expression into postgresql format. + * + * @param \Cake\Database\Expression\StringExpression $expression The string expression to tranform. + * @return void + */ + protected function _transformStringExpression(StringExpression $expression): void + { + // use trim() to work around expression being transformed multiple times + $expression->setCollation('"' . trim($expression->getCollation(), '"') . '"'); + } + + /** + * {@inheritDoc} + * + * @return \Cake\Database\PostgresCompiler + */ + public function newCompiler(): QueryCompiler + { + return new PostgresCompiler(); + } +} diff --git a/vendor/cakephp/database/Driver/SqlDialectTrait.php b/vendor/cakephp/database/Driver/SqlDialectTrait.php new file mode 100644 index 0000000..949a109 --- /dev/null +++ b/vendor/cakephp/database/Driver/SqlDialectTrait.php @@ -0,0 +1,308 @@ +_startQuote . $identifier . $this->_endQuote; + } + + // string.string + if (preg_match('/^[\w-]+\.[^ \*]*$/u', $identifier)) { + $items = explode('.', $identifier); + + return $this->_startQuote . implode($this->_endQuote . '.' . $this->_startQuote, $items) . $this->_endQuote; + } + + // string.* + if (preg_match('/^[\w-]+\.\*$/u', $identifier)) { + return $this->_startQuote . str_replace('.*', $this->_endQuote . '.*', $identifier); + } + + // Functions + if (preg_match('/^([\w-]+)\((.*)\)$/', $identifier, $matches)) { + return $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')'; + } + + // Alias.field AS thing + if (preg_match('/^([\w-]+(\.[\w\s-]+|\(.*\))*)\s+AS\s*([\w-]+)$/ui', $identifier, $matches)) { + return $this->quoteIdentifier($matches[1]) . ' AS ' . $this->quoteIdentifier($matches[3]); + } + + // string.string with spaces + if (preg_match('/^([\w-]+\.[\w][\w\s\-]*[\w])(.*)/u', $identifier, $matches)) { + $items = explode('.', $matches[1]); + $field = implode($this->_endQuote . '.' . $this->_startQuote, $items); + + return $this->_startQuote . $field . $this->_endQuote . $matches[2]; + } + + if (preg_match('/^[\w_\s-]*[\w_-]+/u', $identifier)) { + return $this->_startQuote . $identifier . $this->_endQuote; + } + + return $identifier; + } + + /** + * Returns a callable function that will be used to transform a passed Query object. + * This function, in turn, will return an instance of a Query object that has been + * transformed to accommodate any specificities of the SQL dialect in use. + * + * @param string $type the type of query to be transformed + * (select, insert, update, delete) + * @return \Closure + */ + public function queryTranslator(string $type): Closure + { + return function ($query) use ($type) { + if ($this->isAutoQuotingEnabled()) { + $query = (new IdentifierQuoter($this))->quote($query); + } + + /** @var \Cake\ORM\Query $query */ + $query = $this->{'_' . $type . 'QueryTranslator'}($query); + $translators = $this->_expressionTranslators(); + if (!$translators) { + return $query; + } + + $query->traverseExpressions(function ($expression) use ($translators, $query): void { + foreach ($translators as $class => $method) { + if ($expression instanceof $class) { + $this->{$method}($expression, $query); + } + } + }); + + return $query; + }; + } + + /** + * Returns an associative array of methods that will transform Expression + * objects to conform with the specific SQL dialect. Keys are class names + * and values a method in this class. + * + * @psalm-return array + * @return array + */ + protected function _expressionTranslators(): array + { + return []; + } + + /** + * Apply translation steps to select queries. + * + * @param \Cake\Database\Query $query The query to translate + * @return \Cake\Database\Query The modified query + */ + protected function _selectQueryTranslator(Query $query): Query + { + return $this->_transformDistinct($query); + } + + /** + * Returns the passed query after rewriting the DISTINCT clause, so that drivers + * that do not support the "ON" part can provide the actual way it should be done + * + * @param \Cake\Database\Query $query The query to be transformed + * @return \Cake\Database\Query + */ + protected function _transformDistinct(Query $query): Query + { + if (is_array($query->clause('distinct'))) { + $query->group($query->clause('distinct'), true); + $query->distinct(false); + } + + return $query; + } + + /** + * Apply translation steps to delete queries. + * + * Chops out aliases on delete query conditions as most database dialects do not + * support aliases in delete queries. This also removes aliases + * in table names as they frequently don't work either. + * + * We are intentionally not supporting deletes with joins as they have even poorer support. + * + * @param \Cake\Database\Query $query The query to translate + * @return \Cake\Database\Query The modified query + */ + protected function _deleteQueryTranslator(Query $query): Query + { + $hadAlias = false; + $tables = []; + foreach ($query->clause('from') as $alias => $table) { + if (is_string($alias)) { + $hadAlias = true; + } + $tables[] = $table; + } + if ($hadAlias) { + $query->from($tables, true); + } + + if (!$hadAlias) { + return $query; + } + + return $this->_removeAliasesFromConditions($query); + } + + /** + * Apply translation steps to update queries. + * + * Chops out aliases on update query conditions as not all database dialects do support + * aliases in update queries. + * + * Just like for delete queries, joins are currently not supported for update queries. + * + * @param \Cake\Database\Query $query The query to translate + * @return \Cake\Database\Query The modified query + */ + protected function _updateQueryTranslator(Query $query): Query + { + return $this->_removeAliasesFromConditions($query); + } + + /** + * Removes aliases from the `WHERE` clause of a query. + * + * @param \Cake\Database\Query $query The query to process. + * @return \Cake\Database\Query The modified query. + * @throws \RuntimeException In case the processed query contains any joins, as removing + * aliases from the conditions can break references to the joined tables. + */ + protected function _removeAliasesFromConditions(Query $query): Query + { + if ($query->clause('join')) { + throw new RuntimeException( + 'Aliases are being removed from conditions for UPDATE/DELETE queries, ' . + 'this can break references to joined tables.' + ); + } + + $conditions = $query->clause('where'); + if ($conditions) { + $conditions->traverse(function ($expression) { + if ($expression instanceof ComparisonExpression) { + $field = $expression->getField(); + if ( + is_string($field) && + strpos($field, '.') !== false + ) { + [, $unaliasedField] = explode('.', $field, 2); + $expression->setField($unaliasedField); + } + + return $expression; + } + + if ($expression instanceof IdentifierExpression) { + $identifier = $expression->getIdentifier(); + if (strpos($identifier, '.') !== false) { + [, $unaliasedIdentifier] = explode('.', $identifier, 2); + $expression->setIdentifier($unaliasedIdentifier); + } + + return $expression; + } + + return $expression; + }); + } + + return $query; + } + + /** + * Apply translation steps to insert queries. + * + * @param \Cake\Database\Query $query The query to translate + * @return \Cake\Database\Query The modified query + */ + protected function _insertQueryTranslator(Query $query): Query + { + return $query; + } + + /** + * Returns a SQL snippet for creating a new transaction savepoint + * + * @param string|int $name save point name + * @return string + */ + public function savePointSQL($name): string + { + return 'SAVEPOINT LEVEL' . $name; + } + + /** + * Returns a SQL snippet for releasing a previously created save point + * + * @param string|int $name save point name + * @return string + */ + public function releaseSavePointSQL($name): string + { + return 'RELEASE SAVEPOINT LEVEL' . $name; + } + + /** + * Returns a SQL snippet for rollbacking a previously created save point + * + * @param string|int $name save point name + * @return string + */ + public function rollbackSavePointSQL($name): string + { + return 'ROLLBACK TO SAVEPOINT LEVEL' . $name; + } +} diff --git a/vendor/cakephp/database/Driver/Sqlite.php b/vendor/cakephp/database/Driver/Sqlite.php new file mode 100644 index 0000000..0b2cdb0 --- /dev/null +++ b/vendor/cakephp/database/Driver/Sqlite.php @@ -0,0 +1,363 @@ + + */ + protected $_baseConfig = [ + 'persistent' => false, + 'username' => null, + 'password' => null, + 'database' => ':memory:', + 'encoding' => 'utf8', + 'mask' => 0644, + 'flags' => [], + 'init' => [], + ]; + + /** + * The schema dialect class for this driver + * + * @var \Cake\Database\Schema\SqliteSchemaDialect|null + */ + protected $_schemaDialect; + + /** + * Whether the connected server supports window functions. + * + * @var bool|null + */ + protected $_supportsWindowFunctions; + + /** + * String used to start a database identifier quoting to make it safe + * + * @var string + */ + protected $_startQuote = '"'; + + /** + * String used to end a database identifier quoting to make it safe + * + * @var string + */ + protected $_endQuote = '"'; + + /** + * Mapping of date parts. + * + * @var array + */ + protected $_dateParts = [ + 'day' => 'd', + 'hour' => 'H', + 'month' => 'm', + 'minute' => 'M', + 'second' => 'S', + 'week' => 'W', + 'year' => 'Y', + ]; + + /** + * Mapping of feature to db server version for feature availability checks. + * + * @var array + */ + protected $featureVersions = [ + 'cte' => '3.8.3', + 'window' => '3.28.0', + ]; + + /** + * Establishes a connection to the database server + * + * @return bool true on success + */ + public function connect(): bool + { + if ($this->_connection) { + return true; + } + $config = $this->_config; + $config['flags'] += [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ]; + if (!is_string($config['database']) || $config['database'] === '') { + $name = $config['name'] ?? 'unknown'; + throw new InvalidArgumentException( + "The `database` key for the `{$name}` SQLite connection needs to be a non-empty string." + ); + } + + $databaseExists = file_exists($config['database']); + + $dsn = "sqlite:{$config['database']}"; + $this->_connect($dsn, $config); + + if (!$databaseExists && $config['database'] !== ':memory:') { + // phpcs:disable + @chmod($config['database'], $config['mask']); + // phpcs:enable + } + + if (!empty($config['init'])) { + foreach ((array)$config['init'] as $command) { + $this->getConnection()->exec($command); + } + } + + return true; + } + + /** + * Returns whether php is able to use this driver for connecting to database + * + * @return bool true if it is valid to use this driver + */ + public function enabled(): bool + { + return in_array('sqlite', PDO::getAvailableDrivers(), true); + } + + /** + * Prepares a sql statement to be executed + * + * @param \Cake\Database\Query|string $query The query to prepare. + * @return \Cake\Database\StatementInterface + */ + public function prepare($query): StatementInterface + { + $this->connect(); + $isObject = $query instanceof Query; + /** + * @psalm-suppress PossiblyInvalidMethodCall + * @psalm-suppress PossiblyInvalidArgument + */ + $statement = $this->_connection->prepare($isObject ? $query->sql() : $query); + $result = new SqliteStatement(new PDOStatement($statement, $this), $this); + /** @psalm-suppress PossiblyInvalidMethodCall */ + if ($isObject && $query->isBufferedResultsEnabled() === false) { + $result->bufferResults(false); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function disableForeignKeySQL(): string + { + return 'PRAGMA foreign_keys = OFF'; + } + + /** + * @inheritDoc + */ + public function enableForeignKeySQL(): string + { + return 'PRAGMA foreign_keys = ON'; + } + + /** + * @inheritDoc + */ + public function supports(string $feature): bool + { + switch ($feature) { + case static::FEATURE_CTE: + case static::FEATURE_WINDOW: + return version_compare( + $this->version(), + $this->featureVersions[$feature], + '>=' + ); + + case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS: + return true; + } + + return parent::supports($feature); + } + + /** + * @inheritDoc + */ + public function supportsDynamicConstraints(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function schemaDialect(): SchemaDialect + { + if ($this->_schemaDialect === null) { + $this->_schemaDialect = new SqliteSchemaDialect($this); + } + + return $this->_schemaDialect; + } + + /** + * @inheritDoc + */ + public function newCompiler(): QueryCompiler + { + return new SqliteCompiler(); + } + + /** + * @inheritDoc + */ + protected function _expressionTranslators(): array + { + return [ + FunctionExpression::class => '_transformFunctionExpression', + TupleComparison::class => '_transformTupleComparison', + ]; + } + + /** + * Receives a FunctionExpression and changes it so that it conforms to this + * SQL dialect. + * + * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL. + * @return void + */ + protected function _transformFunctionExpression(FunctionExpression $expression): void + { + switch ($expression->getName()) { + case 'CONCAT': + // CONCAT function is expressed as exp1 || exp2 + $expression->setName('')->setConjunction(' ||'); + break; + case 'DATEDIFF': + $expression + ->setName('ROUND') + ->setConjunction('-') + ->iterateParts(function ($p) { + return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]); + }); + break; + case 'NOW': + $expression->setName('DATETIME')->add(["'now'" => 'literal']); + break; + case 'RAND': + $expression + ->setName('ABS') + ->add(['RANDOM() % 1' => 'literal'], [], true); + break; + case 'CURRENT_DATE': + $expression->setName('DATE')->add(["'now'" => 'literal']); + break; + case 'CURRENT_TIME': + $expression->setName('TIME')->add(["'now'" => 'literal']); + break; + case 'EXTRACT': + $expression + ->setName('STRFTIME') + ->setConjunction(' ,') + ->iterateParts(function ($p, $key) { + if ($key === 0) { + $value = rtrim(strtolower($p), 's'); + if (isset($this->_dateParts[$value])) { + $p = ['value' => '%' . $this->_dateParts[$value], 'type' => null]; + } + } + + return $p; + }); + break; + case 'DATE_ADD': + $expression + ->setName('DATE') + ->setConjunction(',') + ->iterateParts(function ($p, $key) { + if ($key === 1) { + $p = ['value' => $p, 'type' => null]; + } + + return $p; + }); + break; + case 'DAYOFWEEK': + $expression + ->setName('STRFTIME') + ->setConjunction(' ') + ->add(["'%w', " => 'literal'], [], true) + ->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1 + break; + } + } + + /** + * Returns true if the server supports common table expressions. + * + * @return bool + * @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_CTE)` instead + */ + public function supportsCTEs(): bool + { + deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.'); + + return $this->supports(static::FEATURE_CTE); + } + + /** + * Returns true if the connected server supports window functions. + * + * @return bool + * @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_WINDOW)` instead + */ + public function supportsWindowFunctions(): bool + { + deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.'); + + return $this->supports(static::FEATURE_WINDOW); + } +} diff --git a/vendor/cakephp/database/Driver/Sqlserver.php b/vendor/cakephp/database/Driver/Sqlserver.php new file mode 100644 index 0000000..23e23dd --- /dev/null +++ b/vendor/cakephp/database/Driver/Sqlserver.php @@ -0,0 +1,561 @@ + + */ + protected $_baseConfig = [ + 'host' => 'localhost\SQLEXPRESS', + 'username' => '', + 'password' => '', + 'database' => 'cake', + 'port' => '', + // PDO::SQLSRV_ENCODING_UTF8 + 'encoding' => 65001, + 'flags' => [], + 'init' => [], + 'settings' => [], + 'attributes' => [], + 'app' => null, + 'connectionPooling' => null, + 'failoverPartner' => null, + 'loginTimeout' => null, + 'multiSubnetFailover' => null, + ]; + + /** + * The schema dialect class for this driver + * + * @var \Cake\Database\Schema\SqlserverSchemaDialect|null + */ + protected $_schemaDialect; + + /** + * String used to start a database identifier quoting to make it safe + * + * @var string + */ + protected $_startQuote = '['; + + /** + * String used to end a database identifier quoting to make it safe + * + * @var string + */ + protected $_endQuote = ']'; + + /** + * Establishes a connection to the database server. + * + * Please note that the PDO::ATTR_PERSISTENT attribute is not supported by + * the SQL Server PHP PDO drivers. As a result you cannot use the + * persistent config option when connecting to a SQL Server (for more + * information see: https://github.com/Microsoft/msphpsql/issues/65). + * + * @throws \InvalidArgumentException if an unsupported setting is in the driver config + * @return bool true on success + */ + public function connect(): bool + { + if ($this->_connection) { + return true; + } + $config = $this->_config; + + if (isset($config['persistent']) && $config['persistent']) { + throw new InvalidArgumentException( + 'Config setting "persistent" cannot be set to true, ' + . 'as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT' + ); + } + + $config['flags'] += [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ]; + + if (!empty($config['encoding'])) { + $config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding']; + } + $port = ''; + if ($config['port']) { + $port = ',' . $config['port']; + } + + $dsn = "sqlsrv:Server={$config['host']}{$port};Database={$config['database']};MultipleActiveResultSets=false"; + if ($config['app'] !== null) { + $dsn .= ";APP={$config['app']}"; + } + if ($config['connectionPooling'] !== null) { + $dsn .= ";ConnectionPooling={$config['connectionPooling']}"; + } + if ($config['failoverPartner'] !== null) { + $dsn .= ";Failover_Partner={$config['failoverPartner']}"; + } + if ($config['loginTimeout'] !== null) { + $dsn .= ";LoginTimeout={$config['loginTimeout']}"; + } + if ($config['multiSubnetFailover'] !== null) { + $dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}"; + } + $this->_connect($dsn, $config); + + $connection = $this->getConnection(); + if (!empty($config['init'])) { + foreach ((array)$config['init'] as $command) { + $connection->exec($command); + } + } + if (!empty($config['settings']) && is_array($config['settings'])) { + foreach ($config['settings'] as $key => $value) { + $connection->exec("SET {$key} {$value}"); + } + } + if (!empty($config['attributes']) && is_array($config['attributes'])) { + foreach ($config['attributes'] as $key => $value) { + $connection->setAttribute($key, $value); + } + } + + return true; + } + + /** + * Returns whether PHP is able to use this driver for connecting to database + * + * @return bool true if it is valid to use this driver + */ + public function enabled(): bool + { + return in_array('sqlsrv', PDO::getAvailableDrivers(), true); + } + + /** + * Prepares a sql statement to be executed + * + * @param \Cake\Database\Query|string $query The query to prepare. + * @return \Cake\Database\StatementInterface + */ + public function prepare($query): StatementInterface + { + $this->connect(); + + $sql = $query; + $options = [ + PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, + PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED, + ]; + if ($query instanceof Query) { + $sql = $query->sql(); + if (count($query->getValueBinder()->bindings()) > 2100) { + throw new InvalidArgumentException( + 'Exceeded maximum number of parameters (2100) for prepared statements in Sql Server. ' . + 'This is probably due to a very large WHERE IN () clause which generates a parameter ' . + 'for each value in the array. ' . + 'If using an Association, try changing the `strategy` from select to subquery.' + ); + } + + if (!$query->isBufferedResultsEnabled()) { + $options = []; + } + } + + /** @psalm-suppress PossiblyInvalidArgument */ + $statement = $this->_connection->prepare($sql, $options); + + return new SqlserverStatement($statement, $this); + } + + /** + * @inheritDoc + */ + public function savePointSQL($name): string + { + return 'SAVE TRANSACTION t' . $name; + } + + /** + * @inheritDoc + */ + public function releaseSavePointSQL($name): string + { + // SQLServer has no release save point operation. + return ''; + } + + /** + * @inheritDoc + */ + public function rollbackSavePointSQL($name): string + { + return 'ROLLBACK TRANSACTION t' . $name; + } + + /** + * @inheritDoc + */ + public function disableForeignKeySQL(): string + { + return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"'; + } + + /** + * @inheritDoc + */ + public function enableForeignKeySQL(): string + { + return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"'; + } + + /** + * @inheritDoc + */ + public function supports(string $feature): bool + { + switch ($feature) { + case static::FEATURE_CTE: + case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS: + case static::FEATURE_WINDOW: + return true; + + case static::FEATURE_QUOTE: + $this->connect(); + + return $this->_connection->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'odbc'; + } + + return parent::supports($feature); + } + + /** + * @inheritDoc + */ + public function supportsDynamicConstraints(): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function schemaDialect(): SchemaDialect + { + if ($this->_schemaDialect === null) { + $this->_schemaDialect = new SqlserverSchemaDialect($this); + } + + return $this->_schemaDialect; + } + + /** + * {@inheritDoc} + * + * @return \Cake\Database\SqlserverCompiler + */ + public function newCompiler(): QueryCompiler + { + return new SqlserverCompiler(); + } + + /** + * @inheritDoc + */ + protected function _selectQueryTranslator(Query $query): Query + { + $limit = $query->clause('limit'); + $offset = $query->clause('offset'); + + if ($limit && $offset === null) { + $query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]); + } + + if ($offset !== null && !$query->clause('order')) { + $query->order($query->newExpr()->add('(SELECT NULL)')); + } + + if ($this->version() < 11 && $offset !== null) { + return $this->_pagingSubquery($query, $limit, $offset); + } + + return $this->_transformDistinct($query); + } + + /** + * Generate a paging subquery for older versions of SQLserver. + * + * Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must + * be used. + * + * @param \Cake\Database\Query $original The query to wrap in a subquery. + * @param int|null $limit The number of rows to fetch. + * @param int|null $offset The number of rows to offset. + * @return \Cake\Database\Query Modified query object. + */ + protected function _pagingSubquery(Query $original, ?int $limit, ?int $offset): Query + { + $field = '_cake_paging_._cake_page_rownum_'; + + if ($original->clause('order')) { + // SQL server does not support column aliases in OVER clauses. But + // the only practical way to specify the use of calculated columns + // is with their alias. So substitute the select SQL in place of + // any column aliases for those entries in the order clause. + $select = $original->clause('select'); + $order = new OrderByExpression(); + $original + ->clause('order') + ->iterateParts(function ($direction, $orderBy) use ($select, $order) { + $key = $orderBy; + if ( + isset($select[$orderBy]) && + $select[$orderBy] instanceof ExpressionInterface + ) { + $order->add(new OrderClauseExpression($select[$orderBy], $direction)); + } else { + $order->add([$key => $direction]); + } + + // Leave original order clause unchanged. + return $orderBy; + }); + } else { + $order = new OrderByExpression('(SELECT NULL)'); + } + + $query = clone $original; + $query->select([ + '_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order), + ])->limit(null) + ->offset(null) + ->order([], true); + + $outer = new Query($query->getConnection()); + $outer->select('*') + ->from(['_cake_paging_' => $query]); + + if ($offset) { + $outer->where(["$field > " . $offset]); + } + if ($limit) { + $value = (int)$offset + $limit; + $outer->where(["$field <= $value"]); + } + + // Decorate the original query as that is what the + // end developer will be calling execute() on originally. + $original->decorateResults(function ($row) { + if (isset($row['_cake_page_rownum_'])) { + unset($row['_cake_page_rownum_']); + } + + return $row; + }); + + return $outer; + } + + /** + * @inheritDoc + */ + protected function _transformDistinct(Query $query): Query + { + if (!is_array($query->clause('distinct'))) { + return $query; + } + + $original = $query; + $query = clone $original; + + $distinct = $query->clause('distinct'); + $query->distinct(false); + + $order = new OrderByExpression($distinct); + $query + ->select(function ($q) use ($distinct, $order) { + $over = $q->newExpr('ROW_NUMBER() OVER') + ->add('(PARTITION BY') + ->add($q->newExpr()->add($distinct)->setConjunction(',')) + ->add($order) + ->add(')') + ->setConjunction(' '); + + return [ + '_cake_distinct_pivot_' => $over, + ]; + }) + ->limit(null) + ->offset(null) + ->order([], true); + + $outer = new Query($query->getConnection()); + $outer->select('*') + ->from(['_cake_distinct_' => $query]) + ->where(['_cake_distinct_pivot_' => 1]); + + // Decorate the original query as that is what the + // end developer will be calling execute() on originally. + $original->decorateResults(function ($row) { + if (isset($row['_cake_distinct_pivot_'])) { + unset($row['_cake_distinct_pivot_']); + } + + return $row; + }); + + return $outer; + } + + /** + * @inheritDoc + */ + protected function _expressionTranslators(): array + { + return [ + FunctionExpression::class => '_transformFunctionExpression', + TupleComparison::class => '_transformTupleComparison', + ]; + } + + /** + * Receives a FunctionExpression and changes it so that it conforms to this + * SQL dialect. + * + * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL. + * @return void + */ + protected function _transformFunctionExpression(FunctionExpression $expression): void + { + switch ($expression->getName()) { + case 'CONCAT': + // CONCAT function is expressed as exp1 + exp2 + $expression->setName('')->setConjunction(' +'); + break; + case 'DATEDIFF': + /** @var bool $hasDay */ + $hasDay = false; + $visitor = function ($value) use (&$hasDay) { + if ($value === 'day') { + $hasDay = true; + } + + return $value; + }; + $expression->iterateParts($visitor); + + if (!$hasDay) { + $expression->add(['day' => 'literal'], [], true); + } + break; + case 'CURRENT_DATE': + $time = new FunctionExpression('GETUTCDATE'); + $expression->setName('CONVERT')->add(['date' => 'literal', $time]); + break; + case 'CURRENT_TIME': + $time = new FunctionExpression('GETUTCDATE'); + $expression->setName('CONVERT')->add(['time' => 'literal', $time]); + break; + case 'NOW': + $expression->setName('GETUTCDATE'); + break; + case 'EXTRACT': + $expression->setName('DATEPART')->setConjunction(' ,'); + break; + case 'DATE_ADD': + $params = []; + $visitor = function ($p, $key) use (&$params) { + if ($key === 0) { + $params[2] = $p; + } else { + $valueUnit = explode(' ', $p); + $params[0] = rtrim($valueUnit[1], 's'); + $params[1] = $valueUnit[0]; + } + + return $p; + }; + $manipulator = function ($p, $key) use (&$params) { + return $params[$key]; + }; + + $expression + ->setName('DATEADD') + ->setConjunction(',') + ->iterateParts($visitor) + ->iterateParts($manipulator) + ->add([$params[2] => 'literal']); + break; + case 'DAYOFWEEK': + $expression + ->setName('DATEPART') + ->setConjunction(' ') + ->add(['weekday, ' => 'literal'], [], true); + break; + case 'SUBSTR': + $expression->setName('SUBSTRING'); + if (count($expression) < 4) { + $params = []; + $expression + ->iterateParts(function ($p) use (&$params) { + return $params[] = $p; + }) + ->add([new FunctionExpression('LEN', [$params[0]]), ['string']]); + } + + break; + } + } +} diff --git a/vendor/cakephp/database/Driver/TupleComparisonTranslatorTrait.php b/vendor/cakephp/database/Driver/TupleComparisonTranslatorTrait.php new file mode 100644 index 0000000..e94b268 --- /dev/null +++ b/vendor/cakephp/database/Driver/TupleComparisonTranslatorTrait.php @@ -0,0 +1,114 @@ +getField(); + + if (!is_array($fields)) { + return; + } + + $operator = strtoupper($expression->getOperator()); + if (!in_array($operator, ['IN', '='])) { + throw new RuntimeException( + sprintf( + 'Tuple comparison transform only supports the `IN` and `=` operators, `%s` given.', + $operator + ) + ); + } + + $value = $expression->getValue(); + $true = new QueryExpression('1'); + + if ($value instanceof Query) { + $selected = array_values($value->clause('select')); + foreach ($fields as $i => $field) { + $value->andWhere([$field => new IdentifierExpression($selected[$i])]); + } + $value->select($true, true); + $expression->setField($true); + $expression->setOperator('='); + + return; + } + + $type = $expression->getType(); + if ($type) { + /** @var array $typeMap */ + $typeMap = array_combine($fields, $type) ?: []; + } else { + $typeMap = []; + } + + $surrogate = $query->getConnection() + ->newQuery() + ->select($true); + + if (!is_array(current($value))) { + $value = [$value]; + } + + $conditions = ['OR' => []]; + foreach ($value as $tuple) { + $item = []; + foreach (array_values($tuple) as $i => $value2) { + $item[] = [$fields[$i] => $value2]; + } + $conditions['OR'][] = $item; + } + $surrogate->where($conditions, $typeMap); + + $expression->setField($true); + $expression->setValue($surrogate); + $expression->setOperator('='); + } +} diff --git a/vendor/cakephp/database/DriverInterface.php b/vendor/cakephp/database/DriverInterface.php new file mode 100644 index 0000000..e2ccc4d --- /dev/null +++ b/vendor/cakephp/database/DriverInterface.php @@ -0,0 +1,334 @@ + $types Associative array of type names used to bind values to query + * @return $this + * @see \Cake\Database\Query::where() + */ + public function filter($conditions, array $types = []) + { + if ($this->filter === null) { + $this->filter = new QueryExpression(); + } + + if ($conditions instanceof Closure) { + $conditions = $conditions(new QueryExpression()); + } + + $this->filter->add($conditions, $types); + + return $this; + } + + /** + * Adds an empty `OVER()` window expression or a named window epression. + * + * @param string|null $name Window name + * @return $this + */ + public function over(?string $name = null) + { + if ($this->window === null) { + $this->window = new WindowExpression(); + } + if ($name) { + // Set name manually in case this was chained from FunctionsBuilder wrapper + $this->window->name($name); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function partition($partitions) + { + $this->over(); + $this->window->partition($partitions); + + return $this; + } + + /** + * @inheritDoc + */ + public function order($fields) + { + $this->over(); + $this->window->order($fields); + + return $this; + } + + /** + * @inheritDoc + */ + public function range($start, $end = 0) + { + $this->over(); + $this->window->range($start, $end); + + return $this; + } + + /** + * @inheritDoc + */ + public function rows(?int $start, ?int $end = 0) + { + $this->over(); + $this->window->rows($start, $end); + + return $this; + } + + /** + * @inheritDoc + */ + public function groups(?int $start, ?int $end = 0) + { + $this->over(); + $this->window->groups($start, $end); + + return $this; + } + + /** + * @inheritDoc + */ + public function frame( + string $type, + $startOffset, + string $startDirection, + $endOffset, + string $endDirection + ) { + $this->over(); + $this->window->frame($type, $startOffset, $startDirection, $endOffset, $endDirection); + + return $this; + } + + /** + * @inheritDoc + */ + public function excludeCurrent() + { + $this->over(); + $this->window->excludeCurrent(); + + return $this; + } + + /** + * @inheritDoc + */ + public function excludeGroup() + { + $this->over(); + $this->window->excludeGroup(); + + return $this; + } + + /** + * @inheritDoc + */ + public function excludeTies() + { + $this->over(); + $this->window->excludeTies(); + + return $this; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $sql = parent::sql($binder); + if ($this->filter !== null) { + $sql .= ' FILTER (WHERE ' . $this->filter->sql($binder) . ')'; + } + if ($this->window !== null) { + if ($this->window->isNamedOnly()) { + $sql .= ' OVER ' . $this->window->sql($binder); + } else { + $sql .= ' OVER (' . $this->window->sql($binder) . ')'; + } + } + + return $sql; + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + parent::traverse($callback); + if ($this->filter !== null) { + $callback($this->filter); + $this->filter->traverse($callback); + } + if ($this->window !== null) { + $callback($this->window); + $this->window->traverse($callback); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function count(): int + { + $count = parent::count(); + if ($this->window !== null) { + $count = $count + 1; + } + + return $count; + } + + /** + * Clone this object and its subtree of expressions. + * + * @return void + */ + public function __clone() + { + parent::__clone(); + if ($this->filter !== null) { + $this->filter = clone $this->filter; + } + if ($this->window !== null) { + $this->window = clone $this->window; + } + } +} diff --git a/vendor/cakephp/database/Expression/BetweenExpression.php b/vendor/cakephp/database/Expression/BetweenExpression.php new file mode 100644 index 0000000..3466147 --- /dev/null +++ b/vendor/cakephp/database/Expression/BetweenExpression.php @@ -0,0 +1,144 @@ +_castToExpression($from, $type); + $to = $this->_castToExpression($to, $type); + } + + $this->_field = $field; + $this->_from = $from; + $this->_to = $to; + $this->_type = $type; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $parts = [ + 'from' => $this->_from, + 'to' => $this->_to, + ]; + + /** @var \Cake\Database\ExpressionInterface|string $field */ + $field = $this->_field; + if ($field instanceof ExpressionInterface) { + $field = $field->sql($binder); + } + + foreach ($parts as $name => $part) { + if ($part instanceof ExpressionInterface) { + $parts[$name] = $part->sql($binder); + continue; + } + $parts[$name] = $this->_bindValue($part, $binder, $this->_type); + } + + return sprintf('%s BETWEEN %s AND %s', $field, $parts['from'], $parts['to']); + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + foreach ([$this->_field, $this->_from, $this->_to] as $part) { + if ($part instanceof ExpressionInterface) { + $callback($part); + } + } + + return $this; + } + + /** + * Registers a value in the placeholder generator and returns the generated placeholder + * + * @param mixed $value The value to bind + * @param \Cake\Database\ValueBinder $binder The value binder to use + * @param string $type The type of $value + * @return string generated placeholder + */ + protected function _bindValue($value, $binder, $type): string + { + $placeholder = $binder->placeholder('c'); + $binder->bind($placeholder, $value, $type); + + return $placeholder; + } + + /** + * Do a deep clone of this expression. + * + * @return void + */ + public function __clone() + { + foreach (['_field', '_from', '_to'] as $part) { + if ($this->{$part} instanceof ExpressionInterface) { + $this->{$part} = clone $this->{$part}; + } + } + } +} diff --git a/vendor/cakephp/database/Expression/CaseExpression.php b/vendor/cakephp/database/Expression/CaseExpression.php new file mode 100644 index 0000000..864a3bc --- /dev/null +++ b/vendor/cakephp/database/Expression/CaseExpression.php @@ -0,0 +1,251 @@ + :value" + * + * @var array + */ + protected $_conditions = []; + + /** + * Values that are associated with the conditions in the $_conditions array. + * Each value represents the 'true' value for the condition with the corresponding key. + * + * @var array + */ + protected $_values = []; + + /** + * The `ELSE` value for the case statement. If null then no `ELSE` will be included. + * + * @var \Cake\Database\ExpressionInterface|array|string|null + */ + protected $_elseValue; + + /** + * Constructs the case expression + * + * @param \Cake\Database\ExpressionInterface|array $conditions The conditions to test. Must be a ExpressionInterface + * instance, or an array of ExpressionInterface instances. + * @param \Cake\Database\ExpressionInterface|array $values Associative array of values to be associated with the + * conditions passed in $conditions. If there are more $values than $conditions, + * the last $value is used as the `ELSE` value. + * @param array $types Associative array of types to be associated with the values + * passed in $values + */ + public function __construct($conditions = [], $values = [], $types = []) + { + $conditions = is_array($conditions) ? $conditions : [$conditions]; + $values = is_array($values) ? $values : [$values]; + $types = is_array($types) ? $types : [$types]; + + if (!empty($conditions)) { + $this->add($conditions, $values, $types); + } + + if (count($values) > count($conditions)) { + end($values); + $key = key($values); + $this->elseValue($values[$key], $types[$key] ?? null); + } + } + + /** + * Adds one or more conditions and their respective true values to the case object. + * Conditions must be a one dimensional array or a QueryExpression. + * The trueValues must be a similar structure, but may contain a string value. + * + * @param \Cake\Database\ExpressionInterface|array $conditions Must be a ExpressionInterface instance, + * or an array of ExpressionInterface instances. + * @param \Cake\Database\ExpressionInterface|array $values Associative array of values of each condition + * @param array $types Associative array of types to be associated with the values + * @return $this + */ + public function add($conditions = [], $values = [], $types = []) + { + $conditions = is_array($conditions) ? $conditions : [$conditions]; + $values = is_array($values) ? $values : [$values]; + $types = is_array($types) ? $types : [$types]; + + $this->_addExpressions($conditions, $values, $types); + + return $this; + } + + /** + * Iterates over the passed in conditions and ensures that there is a matching true value for each. + * If no matching true value, then it is defaulted to '1'. + * + * @param array $conditions Array of ExpressionInterface instances. + * @param array $values Associative array of values of each condition + * @param array $types Associative array of types to be associated with the values + * @return void + */ + protected function _addExpressions(array $conditions, array $values, array $types): void + { + $rawValues = array_values($values); + $keyValues = array_keys($values); + + foreach ($conditions as $k => $c) { + $numericKey = is_numeric($k); + + if ($numericKey && empty($c)) { + continue; + } + + if (!$c instanceof ExpressionInterface) { + continue; + } + + $this->_conditions[] = $c; + $value = $rawValues[$k] ?? 1; + + if ($value === 'literal') { + $value = $keyValues[$k]; + $this->_values[] = $value; + continue; + } + + if ($value === 'identifier') { + /** @var string $identifier */ + $identifier = $keyValues[$k]; + $value = new IdentifierExpression($identifier); + $this->_values[] = $value; + continue; + } + + $type = $types[$k] ?? null; + + if ($type !== null && !$value instanceof ExpressionInterface) { + $value = $this->_castToExpression($value, $type); + } + + if ($value instanceof ExpressionInterface) { + $this->_values[] = $value; + continue; + } + + $this->_values[] = ['value' => $value, 'type' => $type]; + } + } + + /** + * Sets the default value + * + * @param \Cake\Database\ExpressionInterface|array|string|null $value Value to set + * @param string|null $type Type of value + * @return void + */ + public function elseValue($value = null, ?string $type = null): void + { + if (is_array($value)) { + end($value); + $value = key($value); + } + + if ($value !== null && !$value instanceof ExpressionInterface) { + $value = $this->_castToExpression($value, $type); + } + + if (!$value instanceof ExpressionInterface) { + $value = ['value' => $value, 'type' => $type]; + } + + $this->_elseValue = $value; + } + + /** + * Compiles the relevant parts into sql + * + * @param \Cake\Database\ExpressionInterface|array|string $part The part to compile + * @param \Cake\Database\ValueBinder $binder Sql generator + * @return string + */ + protected function _compile($part, ValueBinder $binder): string + { + if ($part instanceof ExpressionInterface) { + $part = $part->sql($binder); + } elseif (is_array($part)) { + $placeholder = $binder->placeholder('param'); + $binder->bind($placeholder, $part['value'], $part['type']); + $part = $placeholder; + } + + return $part; + } + + /** + * Converts the Node into a SQL string fragment. + * + * @param \Cake\Database\ValueBinder $binder Placeholder generator object + * @return string + */ + public function sql(ValueBinder $binder): string + { + $parts = []; + $parts[] = 'CASE'; + foreach ($this->_conditions as $k => $part) { + $value = $this->_values[$k]; + $parts[] = 'WHEN ' . $this->_compile($part, $binder) . ' THEN ' . $this->_compile($value, $binder); + } + if ($this->_elseValue !== null) { + $parts[] = 'ELSE'; + $parts[] = $this->_compile($this->_elseValue, $binder); + } + $parts[] = 'END'; + + return implode(' ', $parts); + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + foreach (['_conditions', '_values'] as $part) { + foreach ($this->{$part} as $c) { + if ($c instanceof ExpressionInterface) { + $callback($c); + $c->traverse($callback); + } + } + } + if ($this->_elseValue instanceof ExpressionInterface) { + $callback($this->_elseValue); + $this->_elseValue->traverse($callback); + } + + return $this; + } +} diff --git a/vendor/cakephp/database/Expression/CaseExpressionTrait.php b/vendor/cakephp/database/Expression/CaseExpressionTrait.php new file mode 100644 index 0000000..e486a19 --- /dev/null +++ b/vendor/cakephp/database/Expression/CaseExpressionTrait.php @@ -0,0 +1,106 @@ +_typeMap !== null && + $value instanceof IdentifierExpression + ) { + $type = $this->_typeMap->type($value->getIdentifier()); + } + + return $type; + } + + /** + * Compiles a nullable value to SQL. + * + * @param \Cake\Database\ValueBinder $binder The value binder to use. + * @param \Cake\Database\ExpressionInterface|object|scalar|null $value The value to compile. + * @param string|null $type The value type. + * @return string + */ + protected function compileNullableValue(ValueBinder $binder, $value, ?string $type = null): string + { + if ( + $type !== null && + !($value instanceof ExpressionInterface) + ) { + $value = $this->_castToExpression($value, $type); + } + + if ($value === null) { + $value = 'NULL'; + } elseif ($value instanceof Query) { + $value = sprintf('(%s)', $value->sql($binder)); + } elseif ($value instanceof ExpressionInterface) { + $value = $value->sql($binder); + } else { + $placeholder = $binder->placeholder('c'); + $binder->bind($placeholder, $value, $type); + $value = $placeholder; + } + + return $value; + } +} diff --git a/vendor/cakephp/database/Expression/CaseStatementExpression.php b/vendor/cakephp/database/Expression/CaseStatementExpression.php new file mode 100644 index 0000000..40bacf9 --- /dev/null +++ b/vendor/cakephp/database/Expression/CaseStatementExpression.php @@ -0,0 +1,596 @@ + + */ + protected $validClauseNames = [ + 'value', + 'when', + 'else', + ]; + + /** + * Whether this is a simple case expression. + * + * @var bool + */ + protected $isSimpleVariant = false; + + /** + * The case value. + * + * @var \Cake\Database\ExpressionInterface|object|scalar|null + */ + protected $value = null; + + /** + * The case value type. + * + * @var string|null + */ + protected $valueType = null; + + /** + * The `WHEN ... THEN ...` expressions. + * + * @var array<\Cake\Database\Expression\WhenThenExpression> + */ + protected $when = []; + + /** + * Buffer that holds values and types for use with `then()`. + * + * @var array|null + */ + protected $whenBuffer = null; + + /** + * The else part result value. + * + * @var \Cake\Database\ExpressionInterface|object|scalar|null + */ + protected $else = null; + + /** + * The else part result type. + * + * @var string|null + */ + protected $elseType = null; + + /** + * The return type. + * + * @var string|null + */ + protected $returnType = null; + + /** + * Constructor. + * + * When a value is set, the syntax generated is + * `CASE case_value WHEN when_value ... END` (simple case), + * where the `when_value`'s are compared against the + * `case_value`. + * + * When no value is set, the syntax generated is + * `CASE WHEN when_conditions ... END` (searched case), + * where the conditions hold the comparisons. + * + * Note that `null` is a valid case value, and thus should + * only be passed if you actually want to create the simple + * case expression variant! + * + * @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value. + * @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred + * from the value. + */ + public function __construct($value = null, ?string $type = null) + { + if (func_num_args() > 0) { + if ( + $value !== null && + !is_scalar($value) && + !(is_object($value) && !($value instanceof Closure)) + ) { + throw new InvalidArgumentException(sprintf( + 'The `$value` argument must be either `null`, a scalar value, an object, ' . + 'or an instance of `\%s`, `%s` given.', + ExpressionInterface::class, + getTypeName($value) + )); + } + + $this->value = $value; + + if ( + $value !== null && + $type === null && + !($value instanceof ExpressionInterface) + ) { + $type = $this->inferType($value); + } + $this->valueType = $type; + + $this->isSimpleVariant = true; + } + } + + /** + * Sets the `WHEN` value for a `WHEN ... THEN ...` expression, or a + * self-contained expression that holds both the value for `WHEN` + * and the value for `THEN`. + * + * ### Order based syntax + * + * When passing a value other than a self-contained + * `\Cake\Database\Expression\WhenThenExpression`, + * instance, the `WHEN ... THEN ...` statement must be closed off with + * a call to `then()` before invoking `when()` again or `else()`: + * + * ``` + * $queryExpression + * ->case($query->identifier('Table.column')) + * ->when(true) + * ->then('Yes') + * ->when(false) + * ->then('No') + * ->else('Maybe'); + * ``` + * + * ### Self-contained expressions + * + * When passing an instance of `\Cake\Database\Expression\WhenThenExpression`, + * being it directly, or via a callable, then there is no need to close + * using `then()` on this object, instead the statement will be closed + * on the `\Cake\Database\Expression\WhenThenExpression` + * object using + * `\Cake\Database\Expression\WhenThenExpression::then()`. + * + * Callables will receive an instance of `\Cake\Database\Expression\WhenThenExpression`, + * and must return one, being it the same object, or a custom one: + * + * ``` + * $queryExpression + * ->case() + * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) { + * return $whenThen + * ->when(['Table.column' => true]) + * ->then('Yes'); + * }) + * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) { + * return $whenThen + * ->when(['Table.column' => false]) + * ->then('No'); + * }) + * ->else('Maybe'); + * ``` + * + * ### Type handling + * + * The types provided via the `$type` argument will be merged with the + * type map set for this expression. When using callables for `$when`, + * the `\Cake\Database\Expression\WhenThenExpression` + * instance received by the callables will inherit that type map, however + * the types passed here will _not_ be merged in case of using callables, + * instead the types must be passed in + * `\Cake\Database\Expression\WhenThenExpression::when()`: + * + * ``` + * $queryExpression + * ->case() + * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) { + * return $whenThen + * ->when(['unmapped_column' => true], ['unmapped_column' => 'bool']) + * ->then('Yes'); + * }) + * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) { + * return $whenThen + * ->when(['unmapped_column' => false], ['unmapped_column' => 'bool']) + * ->then('No'); + * }) + * ->else('Maybe'); + * ``` + * + * ### User data safety + * + * When passing user data, be aware that allowing a user defined array + * to be passed, is a potential SQL injection vulnerability, as it + * allows for raw SQL to slip in! + * + * The following is _unsafe_ usage that must be avoided: + * + * ``` + * $case + * ->when($userData) + * ``` + * + * A safe variant for the above would be to define a single type for + * the value: + * + * ``` + * $case + * ->when($userData, 'integer') + * ``` + * + * This way an exception would be triggered when an array is passed for + * the value, thus preventing raw SQL from slipping in, and all other + * types of values would be forced to be bound as an integer. + * + * Another way to safely pass user data is when using a conditions + * array, and passing user data only on the value side of the array + * entries, which will cause them to be bound: + * + * ``` + * $case + * ->when([ + * 'Table.column' => $userData, + * ]) + * ``` + * + * Lastly, data can also be bound manually: + * + * ``` + * $query + * ->select([ + * 'val' => $query->newExpr() + * ->case() + * ->when($query->newExpr(':userData')) + * ->then(123) + * ]) + * ->bind(':userData', $userData, 'integer') + * ``` + * + * @param \Cake\Database\ExpressionInterface|\Closure|object|array|scalar $when The `WHEN` value. When using an + * array of conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is + * _not_ completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If + * you plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to + * be a non-array, and then always binds the data), use a conditions array where the user data is only passed on + * the value side of the array entries, or custom bindings! + * @param array|string|null $type The when value type. Either an associative array when using array style + * conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value. + * @return $this + * @throws \LogicException In case this a closing `then()` call is required before calling this method. + * @throws \LogicException In case the callable doesn't return an instance of + * `\Cake\Database\Expression\WhenThenExpression`. + */ + public function when($when, $type = null) + { + if ($this->whenBuffer !== null) { + throw new LogicException('Cannot call `when()` between `when()` and `then()`.'); + } + + if ($when instanceof Closure) { + $when = $when(new WhenThenExpression($this->getTypeMap())); + if (!($when instanceof WhenThenExpression)) { + throw new LogicException(sprintf( + '`when()` callables must return an instance of `\%s`, `%s` given.', + WhenThenExpression::class, + getTypeName($when) + )); + } + } + + if ($when instanceof WhenThenExpression) { + $this->when[] = $when; + } else { + $this->whenBuffer = ['when' => $when, 'type' => $type]; + } + + return $this; + } + + /** + * Sets the `THEN` result value for the last `WHEN ... THEN ...` + * statement that was opened using `when()`. + * + * ### Order based syntax + * + * This method can only be invoked in case `when()` was previously + * used with a value other than a closure or an instance of + * `\Cake\Database\Expression\WhenThenExpression`: + * + * ``` + * $case + * ->when(['Table.column' => true]) + * ->then('Yes') + * ->when(['Table.column' => false]) + * ->then('No') + * ->else('Maybe'); + * ``` + * + * The following would all fail with an exception: + * + * ``` + * $case + * ->when(['Table.column' => true]) + * ->when(['Table.column' => false]) + * // ... + * ``` + * + * ``` + * $case + * ->when(['Table.column' => true]) + * ->else('Maybe') + * // ... + * ``` + * + * ``` + * $case + * ->then('Yes') + * // ... + * ``` + * + * ``` + * $case + * ->when(['Table.column' => true]) + * ->then('Yes') + * ->then('No') + * // ... + * ``` + * + * @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value. + * @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the + * value. + * @return $this + * @throws \LogicException In case `when()` wasn't previously called with a value other than a closure or an + * instance of `\Cake\Database\Expression\WhenThenExpression`. + */ + public function then($result, ?string $type = null) + { + if ($this->whenBuffer === null) { + throw new LogicException('Cannot call `then()` before `when()`.'); + } + + $whenThen = (new WhenThenExpression($this->getTypeMap())) + ->when($this->whenBuffer['when'], $this->whenBuffer['type']) + ->then($result, $type); + + $this->whenBuffer = null; + + $this->when[] = $whenThen; + + return $this; + } + + /** + * Sets the `ELSE` result value. + * + * @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value. + * @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the + * value. + * @return $this + * @throws \LogicException In case a closing `then()` call is required before calling this method. + * @throws \InvalidArgumentException In case the `$result` argument is neither a scalar value, nor an object, an + * instance of `\Cake\Database\ExpressionInterface`, or `null`. + */ + public function else($result, ?string $type = null) + { + if ($this->whenBuffer !== null) { + throw new LogicException('Cannot call `else()` between `when()` and `then()`.'); + } + + if ( + $result !== null && + !is_scalar($result) && + !(is_object($result) && !($result instanceof Closure)) + ) { + throw new InvalidArgumentException(sprintf( + 'The `$result` argument must be either `null`, a scalar value, an object, ' . + 'or an instance of `\%s`, `%s` given.', + ExpressionInterface::class, + getTypeName($result) + )); + } + + if ($type === null) { + $type = $this->inferType($result); + } + + $this->else = $result; + $this->elseType = $type; + + return $this; + } + + /** + * Returns the abstract type that this expression will return. + * + * If no type has been explicitly set via `setReturnType()`, this + * method will try to obtain the type from the result types of the + * `then()` and `else() `calls. All types must be identical in order + * for this to work, otherwise the type will default to `string`. + * + * @return string + * @see CaseStatementExpression::then() + */ + public function getReturnType(): string + { + if ($this->returnType !== null) { + return $this->returnType; + } + + $types = []; + foreach ($this->when as $when) { + $type = $when->getResultType(); + if ($type !== null) { + $types[] = $type; + } + } + + if ($this->elseType !== null) { + $types[] = $this->elseType; + } + + $types = array_unique($types); + if (count($types) === 1) { + return $types[0]; + } + + return 'string'; + } + + /** + * Sets the abstract type that this expression will return. + * + * If no type is being explicitly set via this method, then the + * `getReturnType()` method will try to infer the type from the + * result types of the `then()` and `else() `calls. + * + * @param string $type The type name to use. + * @return $this + */ + public function setReturnType(string $type) + { + $this->returnType = $type; + + return $this; + } + + /** + * Returns the available data for the given clause. + * + * ### Available clauses + * + * The following clause names are available: + * + * * `value`: The case value for a `CASE case_value WHEN ...` expression. + * * `when`: An array of `WHEN ... THEN ...` expressions. + * * `else`: The `ELSE` result value. + * + * @param string $clause The name of the clause to obtain. + * @return \Cake\Database\ExpressionInterface|object|array<\Cake\Database\Expression\WhenThenExpression>|scalar|null + * @throws \InvalidArgumentException In case the given clause name is invalid. + */ + public function clause(string $clause) + { + if (!in_array($clause, $this->validClauseNames, true)) { + throw new InvalidArgumentException( + sprintf( + 'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.', + implode('`, `', $this->validClauseNames), + $clause + ) + ); + } + + return $this->{$clause}; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + if ($this->whenBuffer !== null) { + throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.'); + } + + if (empty($this->when)) { + throw new LogicException('Case expression must have at least one when statement.'); + } + + $value = ''; + if ($this->isSimpleVariant) { + $value = $this->compileNullableValue($binder, $this->value, $this->valueType) . ' '; + } + + $whenThenExpressions = []; + foreach ($this->when as $whenThen) { + $whenThenExpressions[] = $whenThen->sql($binder); + } + $whenThen = implode(' ', $whenThenExpressions); + + $else = $this->compileNullableValue($binder, $this->else, $this->elseType); + + return "CASE {$value}{$whenThen} ELSE $else END"; + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + if ($this->whenBuffer !== null) { + throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.'); + } + + if ($this->value instanceof ExpressionInterface) { + $callback($this->value); + $this->value->traverse($callback); + } + + foreach ($this->when as $when) { + $callback($when); + $when->traverse($callback); + } + + if ($this->else instanceof ExpressionInterface) { + $callback($this->else); + $this->else->traverse($callback); + } + + return $this; + } + + /** + * Clones the inner expression objects. + * + * @return void + */ + public function __clone() + { + if ($this->whenBuffer !== null) { + throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.'); + } + + if ($this->value instanceof ExpressionInterface) { + $this->value = clone $this->value; + } + + foreach ($this->when as $key => $when) { + $this->when[$key] = clone $this->when[$key]; + } + + if ($this->else instanceof ExpressionInterface) { + $this->else = clone $this->else; + } + } +} diff --git a/vendor/cakephp/database/Expression/CommonTableExpression.php b/vendor/cakephp/database/Expression/CommonTableExpression.php new file mode 100644 index 0000000..f3d359e --- /dev/null +++ b/vendor/cakephp/database/Expression/CommonTableExpression.php @@ -0,0 +1,239 @@ + + */ + protected $fields = []; + + /** + * The CTE query definition. + * + * @var \Cake\Database\ExpressionInterface|null + */ + protected $query; + + /** + * Whether the CTE is materialized or not materialized. + * + * @var string|null + */ + protected $materialized = null; + + /** + * Whether the CTE is recursive. + * + * @var bool + */ + protected $recursive = false; + + /** + * Constructor. + * + * @param string $name The CTE name. + * @param \Cake\Database\ExpressionInterface|\Closure $query CTE query + */ + public function __construct(string $name = '', $query = null) + { + $this->name = new IdentifierExpression($name); + if ($query) { + $this->query($query); + } + } + + /** + * Sets the name of this CTE. + * + * This is the named you used to reference the expression + * in select, insert, etc queries. + * + * @param string $name The CTE name. + * @return $this + */ + public function name(string $name) + { + $this->name = new IdentifierExpression($name); + + return $this; + } + + /** + * Sets the query for this CTE. + * + * @param \Cake\Database\ExpressionInterface|\Closure $query CTE query + * @return $this + */ + public function query($query) + { + if ($query instanceof Closure) { + $query = $query(); + if (!($query instanceof ExpressionInterface)) { + throw new RuntimeException( + 'You must return an `ExpressionInterface` from a Closure passed to `query()`.' + ); + } + } + $this->query = $query; + + return $this; + } + + /** + * Adds one or more fields (arguments) to the CTE. + * + * @param \Cake\Database\Expression\IdentifierExpression|array<\Cake\Database\Expression\IdentifierExpression>|array|string $fields Field names + * @return $this + */ + public function field($fields) + { + $fields = (array)$fields; + foreach ($fields as &$field) { + if (!($field instanceof IdentifierExpression)) { + $field = new IdentifierExpression($field); + } + } + $this->fields = array_merge($this->fields, $fields); + + return $this; + } + + /** + * Sets this CTE as materialized. + * + * @return $this + */ + public function materialized() + { + $this->materialized = 'MATERIALIZED'; + + return $this; + } + + /** + * Sets this CTE as not materialized. + * + * @return $this + */ + public function notMaterialized() + { + $this->materialized = 'NOT MATERIALIZED'; + + return $this; + } + + /** + * Gets whether this CTE is recursive. + * + * @return bool + */ + public function isRecursive(): bool + { + return $this->recursive; + } + + /** + * Sets this CTE as recursive. + * + * @return $this + */ + public function recursive() + { + $this->recursive = true; + + return $this; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $fields = ''; + if ($this->fields) { + $expressions = array_map(function (IdentifierExpression $e) use ($binder) { + return $e->sql($binder); + }, $this->fields); + $fields = sprintf('(%s)', implode(', ', $expressions)); + } + + $suffix = $this->materialized ? $this->materialized . ' ' : ''; + + return sprintf( + '%s%s AS %s(%s)', + $this->name->sql($binder), + $fields, + $suffix, + $this->query ? $this->query->sql($binder) : '' + ); + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + $callback($this->name); + foreach ($this->fields as $field) { + $callback($field); + $field->traverse($callback); + } + + if ($this->query) { + $callback($this->query); + $this->query->traverse($callback); + } + + return $this; + } + + /** + * Clones the inner expression objects. + * + * @return void + */ + public function __clone() + { + $this->name = clone $this->name; + if ($this->query) { + $this->query = clone $this->query; + } + + foreach ($this->fields as $key => $field) { + $this->fields[$key] = clone $field; + } + } +} diff --git a/vendor/cakephp/database/Expression/Comparison.php b/vendor/cakephp/database/Expression/Comparison.php new file mode 100644 index 0000000..11cdb98 --- /dev/null +++ b/vendor/cakephp/database/Expression/Comparison.php @@ -0,0 +1,6 @@ + + */ + protected $_valueExpressions = []; + + /** + * Constructor + * + * @param \Cake\Database\ExpressionInterface|string $field the field name to compare to a value + * @param mixed $value The value to be used in comparison + * @param string|null $type the type name used to cast the value + * @param string $operator the operator used for comparing field and value + */ + public function __construct($field, $value, ?string $type = null, string $operator = '=') + { + $this->_type = $type; + $this->setField($field); + $this->setValue($value); + $this->_operator = $operator; + } + + /** + * Sets the value + * + * @param mixed $value The value to compare + * @return void + */ + public function setValue($value): void + { + $value = $this->_castToExpression($value, $this->_type); + + $isMultiple = $this->_type && strpos($this->_type, '[]') !== false; + if ($isMultiple) { + [$value, $this->_valueExpressions] = $this->_collectExpressions($value); + } + + $this->_isMultiple = $isMultiple; + $this->_value = $value; + } + + /** + * Returns the value used for comparison + * + * @return mixed + */ + public function getValue() + { + return $this->_value; + } + + /** + * Sets the operator to use for the comparison + * + * @param string $operator The operator to be used for the comparison. + * @return void + */ + public function setOperator(string $operator): void + { + $this->_operator = $operator; + } + + /** + * Returns the operator used for comparison + * + * @return string + */ + public function getOperator(): string + { + return $this->_operator; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + /** @var \Cake\Database\ExpressionInterface|string $field */ + $field = $this->_field; + + if ($field instanceof ExpressionInterface) { + $field = $field->sql($binder); + } + + if ($this->_value instanceof IdentifierExpression) { + $template = '%s %s %s'; + $value = $this->_value->sql($binder); + } elseif ($this->_value instanceof ExpressionInterface) { + $template = '%s %s (%s)'; + $value = $this->_value->sql($binder); + } else { + [$template, $value] = $this->_stringExpression($binder); + } + + return sprintf($template, $field, $this->_operator, $value); + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + if ($this->_field instanceof ExpressionInterface) { + $callback($this->_field); + $this->_field->traverse($callback); + } + + if ($this->_value instanceof ExpressionInterface) { + $callback($this->_value); + $this->_value->traverse($callback); + } + + foreach ($this->_valueExpressions as $v) { + $callback($v); + $v->traverse($callback); + } + + return $this; + } + + /** + * Create a deep clone. + * + * Clones the field and value if they are expression objects. + * + * @return void + */ + public function __clone() + { + foreach (['_value', '_field'] as $prop) { + if ($this->{$prop} instanceof ExpressionInterface) { + $this->{$prop} = clone $this->{$prop}; + } + } + } + + /** + * Returns a template and a placeholder for the value after registering it + * with the placeholder $binder + * + * @param \Cake\Database\ValueBinder $binder The value binder to use. + * @return array First position containing the template and the second a placeholder + */ + protected function _stringExpression(ValueBinder $binder): array + { + $template = '%s '; + + if ($this->_field instanceof ExpressionInterface && !$this->_field instanceof IdentifierExpression) { + $template = '(%s) '; + } + + if ($this->_isMultiple) { + $template .= '%s (%s)'; + $type = $this->_type; + if ($type !== null) { + $type = str_replace('[]', '', $type); + } + $value = $this->_flattenValue($this->_value, $binder, $type); + + // To avoid SQL errors when comparing a field to a list of empty values, + // better just throw an exception here + if ($value === '') { + $field = $this->_field instanceof ExpressionInterface ? $this->_field->sql($binder) : $this->_field; + /** @psalm-suppress PossiblyInvalidCast */ + throw new DatabaseException( + "Impossible to generate condition with empty list of values for field ($field)" + ); + } + } else { + $template .= '%s %s'; + $value = $this->_bindValue($this->_value, $binder, $this->_type); + } + + return [$template, $value]; + } + + /** + * Registers a value in the placeholder generator and returns the generated placeholder + * + * @param mixed $value The value to bind + * @param \Cake\Database\ValueBinder $binder The value binder to use + * @param string|null $type The type of $value + * @return string generated placeholder + */ + protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string + { + $placeholder = $binder->placeholder('c'); + $binder->bind($placeholder, $value, $type); + + return $placeholder; + } + + /** + * Converts a traversable value into a set of placeholders generated by + * $binder and separated by `,` + * + * @param iterable $value the value to flatten + * @param \Cake\Database\ValueBinder $binder The value binder to use + * @param string|null $type the type to cast values to + * @return string + */ + protected function _flattenValue(iterable $value, ValueBinder $binder, ?string $type = null): string + { + $parts = []; + if (is_array($value)) { + foreach ($this->_valueExpressions as $k => $v) { + $parts[$k] = $v->sql($binder); + unset($value[$k]); + } + } + + if (!empty($value)) { + $parts += $binder->generateManyNamed($value, $type); + } + + return implode(',', $parts); + } + + /** + * Returns an array with the original $values in the first position + * and all ExpressionInterface objects that could be found in the second + * position. + * + * @param \Cake\Database\ExpressionInterface|iterable $values The rows to insert + * @return array + */ + protected function _collectExpressions($values): array + { + if ($values instanceof ExpressionInterface) { + return [$values, []]; + } + + $expressions = $result = []; + $isArray = is_array($values); + + if ($isArray) { + /** @var array $result */ + $result = $values; + } + + foreach ($values as $k => $v) { + if ($v instanceof ExpressionInterface) { + $expressions[$k] = $v; + } + + if ($isArray) { + $result[$k] = $v; + } + } + + return [$result, $expressions]; + } +} + +// phpcs:disable +// Comparison will not load during instanceof checks so ensure it's loaded here +// @deprecated 4.1.0 Add backwards compatible alias. +class_alias('Cake\Database\Expression\ComparisonExpression', 'Cake\Database\Expression\Comparison'); +// phpcs:enable diff --git a/vendor/cakephp/database/Expression/FieldInterface.php b/vendor/cakephp/database/Expression/FieldInterface.php new file mode 100644 index 0000000..f0554d9 --- /dev/null +++ b/vendor/cakephp/database/Expression/FieldInterface.php @@ -0,0 +1,39 @@ +_field = $field; + } + + /** + * Returns the field name + * + * @return \Cake\Database\ExpressionInterface|array|string + */ + public function getField() + { + return $this->_field; + } +} diff --git a/vendor/cakephp/database/Expression/FunctionExpression.php b/vendor/cakephp/database/Expression/FunctionExpression.php new file mode 100644 index 0000000..105655b --- /dev/null +++ b/vendor/cakephp/database/Expression/FunctionExpression.php @@ -0,0 +1,178 @@ + 'literal', ' rules']);` + * + * Will produce `CONCAT(name, ' rules')` + * + * @param string $name the name of the function to be constructed + * @param array $params list of arguments to be passed to the function + * If associative the key would be used as argument when value is 'literal' + * @param array|array $types Associative array of types to be associated with the + * passed arguments + * @param string $returnType The return type of this expression + */ + public function __construct(string $name, array $params = [], array $types = [], string $returnType = 'string') + { + $this->_name = $name; + $this->_returnType = $returnType; + parent::__construct($params, $types, ','); + } + + /** + * Sets the name of the SQL function to be invoke in this expression. + * + * @param string $name The name of the function + * @return $this + */ + public function setName(string $name) + { + $this->_name = $name; + + return $this; + } + + /** + * Gets the name of the SQL function to be invoke in this expression. + * + * @return string + */ + public function getName(): string + { + return $this->_name; + } + + /** + * Adds one or more arguments for the function call. + * + * @param array $conditions list of arguments to be passed to the function + * If associative the key would be used as argument when value is 'literal' + * @param array $types Associative array of types to be associated with the + * passed arguments + * @param bool $prepend Whether to prepend or append to the list of arguments + * @see \Cake\Database\Expression\FunctionExpression::__construct() for more details. + * @return $this + * @psalm-suppress MoreSpecificImplementedParamType + */ + public function add($conditions, array $types = [], bool $prepend = false) + { + $put = $prepend ? 'array_unshift' : 'array_push'; + $typeMap = $this->getTypeMap()->setTypes($types); + foreach ($conditions as $k => $p) { + if ($p === 'literal') { + $put($this->_conditions, $k); + continue; + } + + if ($p === 'identifier') { + $put($this->_conditions, new IdentifierExpression($k)); + continue; + } + + $type = $typeMap->type($k); + + if ($type !== null && !$p instanceof ExpressionInterface) { + $p = $this->_castToExpression($p, $type); + } + + if ($p instanceof ExpressionInterface) { + $put($this->_conditions, $p); + continue; + } + + $put($this->_conditions, ['value' => $p, 'type' => $type]); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $parts = []; + foreach ($this->_conditions as $condition) { + if ($condition instanceof Query) { + $condition = sprintf('(%s)', $condition->sql($binder)); + } elseif ($condition instanceof ExpressionInterface) { + $condition = $condition->sql($binder); + } elseif (is_array($condition)) { + $p = $binder->placeholder('param'); + $binder->bind($p, $condition['value'], $condition['type']); + $condition = $p; + } + $parts[] = $condition; + } + + return $this->_name . sprintf('(%s)', implode( + $this->_conjunction . ' ', + $parts + )); + } + + /** + * The name of the function is in itself an expression to generate, thus + * always adding 1 to the amount of expressions stored in this object. + * + * @return int + */ + public function count(): int + { + return 1 + count($this->_conditions); + } +} diff --git a/vendor/cakephp/database/Expression/IdentifierExpression.php b/vendor/cakephp/database/Expression/IdentifierExpression.php new file mode 100644 index 0000000..69486a4 --- /dev/null +++ b/vendor/cakephp/database/Expression/IdentifierExpression.php @@ -0,0 +1,119 @@ +_identifier = $identifier; + $this->collation = $collation; + } + + /** + * Sets the identifier this expression represents + * + * @param string $identifier The identifier + * @return void + */ + public function setIdentifier(string $identifier): void + { + $this->_identifier = $identifier; + } + + /** + * Returns the identifier this expression represents + * + * @return string + */ + public function getIdentifier(): string + { + return $this->_identifier; + } + + /** + * Sets the collation. + * + * @param string $collation Identifier collation + * @return void + */ + public function setCollation(string $collation): void + { + $this->collation = $collation; + } + + /** + * Returns the collation. + * + * @return string|null + */ + public function getCollation(): ?string + { + return $this->collation; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $sql = $this->_identifier; + if ($this->collation) { + $sql .= ' COLLATE ' . $this->collation; + } + + return $sql; + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + return $this; + } +} diff --git a/vendor/cakephp/database/Expression/OrderByExpression.php b/vendor/cakephp/database/Expression/OrderByExpression.php new file mode 100644 index 0000000..74fbf21 --- /dev/null +++ b/vendor/cakephp/database/Expression/OrderByExpression.php @@ -0,0 +1,88 @@ + $types The types for each column. + * @param string $conjunction The glue used to join conditions together. + */ + public function __construct($conditions = [], $types = [], $conjunction = '') + { + parent::__construct($conditions, $types, $conjunction); + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $order = []; + foreach ($this->_conditions as $k => $direction) { + if ($direction instanceof ExpressionInterface) { + $direction = $direction->sql($binder); + } + $order[] = is_numeric($k) ? $direction : sprintf('%s %s', $k, $direction); + } + + return sprintf('ORDER BY %s', implode(', ', $order)); + } + + /** + * Auxiliary function used for decomposing a nested array of conditions and + * building a tree structure inside this object to represent the full SQL expression. + * + * New order by expressions are merged to existing ones + * + * @param array $conditions list of order by expressions + * @param array $types list of types associated on fields referenced in $conditions + * @return void + */ + protected function _addConditions(array $conditions, array $types): void + { + foreach ($conditions as $key => $val) { + if ( + is_string($key) && + is_string($val) && + !in_array(strtoupper($val), ['ASC', 'DESC'], true) + ) { + throw new RuntimeException( + sprintf( + 'Passing extra expressions by associative array (`\'%s\' => \'%s\'`) ' . + 'is not allowed to avoid potential SQL injection. ' . + 'Use QueryExpression or numeric array instead.', + $key, + $val + ) + ); + } + } + + $this->_conditions = array_merge($this->_conditions, $conditions); + } +} diff --git a/vendor/cakephp/database/Expression/OrderClauseExpression.php b/vendor/cakephp/database/Expression/OrderClauseExpression.php new file mode 100644 index 0000000..dfab8b9 --- /dev/null +++ b/vendor/cakephp/database/Expression/OrderClauseExpression.php @@ -0,0 +1,90 @@ +_field = $field; + $this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC'; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + /** @var \Cake\Database\ExpressionInterface|string $field */ + $field = $this->_field; + if ($field instanceof Query) { + $field = sprintf('(%s)', $field->sql($binder)); + } elseif ($field instanceof ExpressionInterface) { + $field = $field->sql($binder); + } + + return sprintf('%s %s', $field, $this->_direction); + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + if ($this->_field instanceof ExpressionInterface) { + $callback($this->_field); + $this->_field->traverse($callback); + } + + return $this; + } + + /** + * Create a deep clone of the order clause. + * + * @return void + */ + public function __clone() + { + if ($this->_field instanceof ExpressionInterface) { + $this->_field = clone $this->_field; + } + } +} diff --git a/vendor/cakephp/database/Expression/QueryExpression.php b/vendor/cakephp/database/Expression/QueryExpression.php new file mode 100644 index 0000000..35582dd --- /dev/null +++ b/vendor/cakephp/database/Expression/QueryExpression.php @@ -0,0 +1,878 @@ + :value" + * + * @var array + */ + protected $_conditions = []; + + /** + * Constructor. A new expression object can be created without any params and + * be built dynamically. Otherwise it is possible to pass an array of conditions + * containing either a tree-like array structure to be parsed and/or other + * expression objects. Optionally, you can set the conjunction keyword to be used + * for joining each part of this level of the expression tree. + * + * @param \Cake\Database\ExpressionInterface|array|string $conditions Tree like array structure + * containing all the conditions to be added or nested inside this expression object. + * @param \Cake\Database\TypeMap|array $types Associative array of types to be associated with the values + * passed in $conditions. + * @param string $conjunction the glue that will join all the string conditions at this + * level of the expression tree. For example "AND", "OR", "XOR"... + * @see \Cake\Database\Expression\QueryExpression::add() for more details on $conditions and $types + */ + public function __construct($conditions = [], $types = [], $conjunction = 'AND') + { + $this->setTypeMap($types); + $this->setConjunction(strtoupper($conjunction)); + if (!empty($conditions)) { + $this->add($conditions, $this->getTypeMap()->getTypes()); + } + } + + /** + * Changes the conjunction for the conditions at this level of the expression tree. + * + * @param string $conjunction Value to be used for joining conditions + * @return $this + */ + public function setConjunction(string $conjunction) + { + $this->_conjunction = strtoupper($conjunction); + + return $this; + } + + /** + * Gets the currently configured conjunction for the conditions at this level of the expression tree. + * + * @return string + */ + public function getConjunction(): string + { + return $this->_conjunction; + } + + /** + * Adds one or more conditions to this expression object. Conditions can be + * expressed in a one dimensional array, that will cause all conditions to + * be added directly at this level of the tree or they can be nested arbitrarily + * making it create more expression objects that will be nested inside and + * configured to use the specified conjunction. + * + * If the type passed for any of the fields is expressed "type[]" (note braces) + * then it will cause the placeholder to be re-written dynamically so if the + * value is an array, it will create as many placeholders as values are in it. + * + * @param \Cake\Database\ExpressionInterface|array|string $conditions single or multiple conditions to + * be added. When using an array and the key is 'OR' or 'AND' a new expression + * object will be created with that conjunction and internal array value passed + * as conditions. + * @param array $types Associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @see \Cake\Database\Query::where() for examples on conditions + * @return $this + */ + public function add($conditions, array $types = []) + { + if (is_string($conditions)) { + $this->_conditions[] = $conditions; + + return $this; + } + + if ($conditions instanceof ExpressionInterface) { + $this->_conditions[] = $conditions; + + return $this; + } + + $this->_addConditions($conditions, $types); + + return $this; + } + + /** + * Adds a new condition to the expression object in the form "field = value". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * If it is suffixed with "[]" and the value is an array then multiple placeholders + * will be created, one per each value in the array. + * @return $this + */ + public function eq($field, $value, ?string $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new ComparisonExpression($field, $value, $type, '=')); + } + + /** + * Adds a new condition to the expression object in the form "field != value". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * If it is suffixed with "[]" and the value is an array then multiple placeholders + * will be created, one per each value in the array. + * @return $this + */ + public function notEq($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new ComparisonExpression($field, $value, $type, '!=')); + } + + /** + * Adds a new condition to the expression object in the form "field > value". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function gt($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new ComparisonExpression($field, $value, $type, '>')); + } + + /** + * Adds a new condition to the expression object in the form "field < value". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function lt($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new ComparisonExpression($field, $value, $type, '<')); + } + + /** + * Adds a new condition to the expression object in the form "field >= value". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function gte($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new ComparisonExpression($field, $value, $type, '>=')); + } + + /** + * Adds a new condition to the expression object in the form "field <= value". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function lte($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new ComparisonExpression($field, $value, $type, '<=')); + } + + /** + * Adds a new condition to the expression object in the form "field IS NULL". + * + * @param \Cake\Database\ExpressionInterface|string $field database field to be + * tested for null + * @return $this + */ + public function isNull($field) + { + if (!($field instanceof ExpressionInterface)) { + $field = new IdentifierExpression($field); + } + + return $this->add(new UnaryExpression('IS NULL', $field, UnaryExpression::POSTFIX)); + } + + /** + * Adds a new condition to the expression object in the form "field IS NOT NULL". + * + * @param \Cake\Database\ExpressionInterface|string $field database field to be + * tested for not null + * @return $this + */ + public function isNotNull($field) + { + if (!($field instanceof ExpressionInterface)) { + $field = new IdentifierExpression($field); + } + + return $this->add(new UnaryExpression('IS NOT NULL', $field, UnaryExpression::POSTFIX)); + } + + /** + * Adds a new condition to the expression object in the form "field LIKE value". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function like($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new ComparisonExpression($field, $value, $type, 'LIKE')); + } + + /** + * Adds a new condition to the expression object in the form "field NOT LIKE value". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function notLike($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new ComparisonExpression($field, $value, $type, 'NOT LIKE')); + } + + /** + * Adds a new condition to the expression object in the form + * "field IN (value1, value2)". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function in($field, $values, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + $type = $type ?: 'string'; + $type .= '[]'; + $values = $values instanceof ExpressionInterface ? $values : (array)$values; + + return $this->add(new ComparisonExpression($field, $values, $type, 'IN')); + } + + /** + * Adds a new case expression to the expression object + * + * @param \Cake\Database\ExpressionInterface|array $conditions The conditions to test. Must be a ExpressionInterface + * instance, or an array of ExpressionInterface instances. + * @param \Cake\Database\ExpressionInterface|array $values Associative array of values to be associated with the + * conditions passed in $conditions. If there are more $values than $conditions, + * the last $value is used as the `ELSE` value. + * @param array $types Associative array of types to be associated with the values + * passed in $values + * @return $this + * @deprecated 4.3.0 Use QueryExpression::case() or CaseStatementExpression instead + */ + public function addCase($conditions, $values = [], $types = []) + { + deprecationWarning('QueryExpression::addCase() is deprecated, use case() instead.'); + + return $this->add(new CaseExpression($conditions, $values, $types)); + } + + /** + * Returns a new case expression object. + * + * When a value is set, the syntax generated is + * `CASE case_value WHEN when_value ... END` (simple case), + * where the `when_value`'s are compared against the + * `case_value`. + * + * When no value is set, the syntax generated is + * `CASE WHEN when_conditions ... END` (searched case), + * where the conditions hold the comparisons. + * + * Note that `null` is a valid case value, and thus should + * only be passed if you actually want to create the simple + * case expression variant! + * + * @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value. + * @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred + * from the value. + * @return \Cake\Database\Expression\CaseStatementExpression + */ + public function case($value = null, ?string $type = null): CaseStatementExpression + { + if (func_num_args() > 0) { + $expression = new CaseStatementExpression($value, $type); + } else { + $expression = new CaseStatementExpression(); + } + + return $expression->setTypeMap($this->getTypeMap()); + } + + /** + * Adds a new condition to the expression object in the form + * "field NOT IN (value1, value2)". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function notIn($field, $values, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + $type = $type ?: 'string'; + $type .= '[]'; + $values = $values instanceof ExpressionInterface ? $values : (array)$values; + + return $this->add(new ComparisonExpression($field, $values, $type, 'NOT IN')); + } + + /** + * Adds a new condition to the expression object in the form + * "(field NOT IN (value1, value2) OR field IS NULL". + * + * @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value + * @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function notInOrNull($field, $values, ?string $type = null) + { + $or = new static([], [], 'OR'); + $or + ->notIn($field, $values, $type) + ->isNull($field); + + return $this->add($or); + } + + /** + * Adds a new condition to the expression object in the form "EXISTS (...)". + * + * @param \Cake\Database\ExpressionInterface $expression the inner query + * @return $this + */ + public function exists(ExpressionInterface $expression) + { + return $this->add(new UnaryExpression('EXISTS', $expression, UnaryExpression::PREFIX)); + } + + /** + * Adds a new condition to the expression object in the form "NOT EXISTS (...)". + * + * @param \Cake\Database\ExpressionInterface $expression the inner query + * @return $this + */ + public function notExists(ExpressionInterface $expression) + { + return $this->add(new UnaryExpression('NOT EXISTS', $expression, UnaryExpression::PREFIX)); + } + + /** + * Adds a new condition to the expression object in the form + * "field BETWEEN from AND to". + * + * @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values inbetween the range. + * @param mixed $from The initial value of the range. + * @param mixed $to The ending value in the comparison range. + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function between($field, $from, $to, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new BetweenExpression($field, $from, $to, $type)); + } + + /** + * Returns a new QueryExpression object containing all the conditions passed + * and set up the conjunction to be "AND" + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with AND + * @param array $types Associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @return \Cake\Database\Expression\QueryExpression + */ + public function and($conditions, $types = []) + { + if ($conditions instanceof Closure) { + return $conditions(new static([], $this->getTypeMap()->setTypes($types))); + } + + return new static($conditions, $this->getTypeMap()->setTypes($types)); + } + + /** + * Returns a new QueryExpression object containing all the conditions passed + * and set up the conjunction to be "OR" + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with OR + * @param array $types Associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @return \Cake\Database\Expression\QueryExpression + */ + public function or($conditions, $types = []) + { + if ($conditions instanceof Closure) { + return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR')); + } + + return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR'); + } + + // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps + + /** + * Returns a new QueryExpression object containing all the conditions passed + * and set up the conjunction to be "AND" + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with AND + * @param array $types Associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @return \Cake\Database\Expression\QueryExpression + * @deprecated 4.0.0 Use {@link and()} instead. + */ + public function and_($conditions, $types = []) + { + deprecationWarning('QueryExpression::and_() is deprecated use and() instead.'); + + return $this->and($conditions, $types); + } + + /** + * Returns a new QueryExpression object containing all the conditions passed + * and set up the conjunction to be "OR" + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with OR + * @param array $types Associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @return \Cake\Database\Expression\QueryExpression + * @deprecated 4.0.0 Use {@link or()} instead. + */ + public function or_($conditions, $types = []) + { + deprecationWarning('QueryExpression::or_() is deprecated use or() instead.'); + + return $this->or($conditions, $types); + } + + // phpcs:enable + + /** + * Adds a new set of conditions to this level of the tree and negates + * the final result by prepending a NOT, it will look like + * "NOT ( (condition1) AND (conditions2) )" conjunction depends on the one + * currently configured for this object. + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be added and negated + * @param array $types Associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @return $this + */ + public function not($conditions, $types = []) + { + return $this->add(['NOT' => $conditions], $types); + } + + /** + * Returns the number of internal conditions that are stored in this expression. + * Useful to determine if this expression object is void or it will generate + * a non-empty string when compiled + * + * @return int + */ + public function count(): int + { + return count($this->_conditions); + } + + /** + * Builds equal condition or assignment with identifier wrapping. + * + * @param string $leftField Left join condition field name. + * @param string $rightField Right join condition field name. + * @return $this + */ + public function equalFields(string $leftField, string $rightField) + { + $wrapIdentifier = function ($field) { + if ($field instanceof ExpressionInterface) { + return $field; + } + + return new IdentifierExpression($field); + }; + + return $this->eq($wrapIdentifier($leftField), $wrapIdentifier($rightField)); + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $len = $this->count(); + if ($len === 0) { + return ''; + } + $conjunction = $this->_conjunction; + $template = $len === 1 ? '%s' : '(%s)'; + $parts = []; + foreach ($this->_conditions as $part) { + if ($part instanceof Query) { + $part = '(' . $part->sql($binder) . ')'; + } elseif ($part instanceof ExpressionInterface) { + $part = $part->sql($binder); + } + if ($part !== '') { + $parts[] = $part; + } + } + + return sprintf($template, implode(" $conjunction ", $parts)); + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + foreach ($this->_conditions as $c) { + if ($c instanceof ExpressionInterface) { + $callback($c); + $c->traverse($callback); + } + } + + return $this; + } + + /** + * Executes a callable function for each of the parts that form this expression. + * + * The callable function is required to return a value with which the currently + * visited part will be replaced. If the callable function returns null then + * the part will be discarded completely from this expression. + * + * The callback function will receive each of the conditions as first param and + * the key as second param. It is possible to declare the second parameter as + * passed by reference, this will enable you to change the key under which the + * modified part is stored. + * + * @param callable $callback The callable to apply to each part. + * @return $this + */ + public function iterateParts(callable $callback) + { + $parts = []; + foreach ($this->_conditions as $k => $c) { + $key = &$k; + $part = $callback($c, $key); + if ($part !== null) { + $parts[$key] = $part; + } + } + $this->_conditions = $parts; + + return $this; + } + + /** + * Check whether a callable is acceptable. + * + * We don't accept ['class', 'method'] style callbacks, + * as they often contain user input and arrays of strings + * are easy to sneak in. + * + * @param \Cake\Database\ExpressionInterface|callable|array|string $callable The callable to check. + * @return bool Valid callable. + * @deprecated 4.2.0 This method is unused. + * @codeCoverageIgnore + */ + public function isCallable($callable): bool + { + if (is_string($callable)) { + return false; + } + if (is_object($callable) && is_callable($callable)) { + return true; + } + + return is_array($callable) && isset($callable[0]) && is_object($callable[0]) && is_callable($callable); + } + + /** + * Returns true if this expression contains any other nested + * ExpressionInterface objects + * + * @return bool + */ + public function hasNestedExpression(): bool + { + foreach ($this->_conditions as $c) { + if ($c instanceof ExpressionInterface) { + return true; + } + } + + return false; + } + + /** + * Auxiliary function used for decomposing a nested array of conditions and build + * a tree structure inside this object to represent the full SQL expression. + * String conditions are stored directly in the conditions, while any other + * representation is wrapped around an adequate instance or of this class. + * + * @param array $conditions list of conditions to be stored in this object + * @param array $types list of types associated on fields referenced in $conditions + * @return void + */ + protected function _addConditions(array $conditions, array $types): void + { + $operators = ['and', 'or', 'xor']; + + $typeMap = $this->getTypeMap()->setTypes($types); + + foreach ($conditions as $k => $c) { + $numericKey = is_numeric($k); + + if ($c instanceof Closure) { + $expr = new static([], $typeMap); + $c = $c($expr, $this); + } + + if ($numericKey && empty($c)) { + continue; + } + + $isArray = is_array($c); + $isOperator = $isNot = false; + if (!$numericKey) { + $normalizedKey = strtolower($k); + $isOperator = in_array($normalizedKey, $operators); + $isNot = $normalizedKey === 'not'; + } + + if (($isOperator || $isNot) && ($isArray || $c instanceof Countable) && count($c) === 0) { + continue; + } + + if ($numericKey && $c instanceof ExpressionInterface) { + $this->_conditions[] = $c; + continue; + } + + if ($numericKey && is_string($c)) { + $this->_conditions[] = $c; + continue; + } + + if ($numericKey && $isArray || $isOperator) { + $this->_conditions[] = new static($c, $typeMap, $numericKey ? 'AND' : $k); + continue; + } + + if ($isNot) { + $this->_conditions[] = new UnaryExpression('NOT', new static($c, $typeMap)); + continue; + } + + if (!$numericKey) { + $this->_conditions[] = $this->_parseCondition($k, $c); + } + } + } + + /** + * Parses a string conditions by trying to extract the operator inside it if any + * and finally returning either an adequate QueryExpression object or a plain + * string representation of the condition. This function is responsible for + * generating the placeholders and replacing the values by them, while storing + * the value elsewhere for future binding. + * + * @param string $field The value from which the actual field and operator will + * be extracted. + * @param mixed $value The value to be bound to a placeholder for the field + * @return \Cake\Database\ExpressionInterface + * @throws \InvalidArgumentException If operator is invalid or missing on NULL usage. + */ + protected function _parseCondition(string $field, $value) + { + $field = trim($field); + $operator = '='; + $expression = $field; + + $spaces = substr_count($field, ' '); + // Handle operators with a space in them like `is not` and `not like` + if ($spaces > 1) { + $parts = explode(' ', $field); + if (preg_match('/(is not|not \w+)$/i', $field)) { + $last = array_pop($parts); + $second = array_pop($parts); + array_push($parts, strtolower("{$second} {$last}")); + } + $operator = array_pop($parts); + $expression = implode(' ', $parts); + } elseif ($spaces == 1) { + $parts = explode(' ', $field, 2); + [$expression, $operator] = $parts; + $operator = strtolower(trim($operator)); + } + $type = $this->getTypeMap()->type($expression); + + $typeMultiple = (is_string($type) && strpos($type, '[]') !== false); + if (in_array($operator, ['in', 'not in']) || $typeMultiple) { + $type = $type ?: 'string'; + if (!$typeMultiple) { + $type .= '[]'; + } + $operator = $operator === '=' ? 'IN' : $operator; + $operator = $operator === '!=' ? 'NOT IN' : $operator; + $typeMultiple = true; + } + + if ($typeMultiple) { + $value = $value instanceof ExpressionInterface ? $value : (array)$value; + } + + if ($operator === 'is' && $value === null) { + return new UnaryExpression( + 'IS NULL', + new IdentifierExpression($expression), + UnaryExpression::POSTFIX + ); + } + + if ($operator === 'is not' && $value === null) { + return new UnaryExpression( + 'IS NOT NULL', + new IdentifierExpression($expression), + UnaryExpression::POSTFIX + ); + } + + if ($operator === 'is' && $value !== null) { + $operator = '='; + } + + if ($operator === 'is not' && $value !== null) { + $operator = '!='; + } + + if ($value === null && $this->_conjunction !== ',') { + throw new InvalidArgumentException( + sprintf('Expression `%s` is missing operator (IS, IS NOT) with `null` value.', $expression) + ); + } + + return new ComparisonExpression($expression, $value, $type, $operator); + } + + /** + * Returns the type name for the passed field if it was stored in the typeMap + * + * @param \Cake\Database\ExpressionInterface|string $field The field name to get a type for. + * @return string|null The computed type or null, if the type is unknown. + */ + protected function _calculateType($field): ?string + { + $field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field; + if (is_string($field)) { + return $this->getTypeMap()->type($field); + } + + return null; + } + + /** + * Clone this object and its subtree of expressions. + * + * @return void + */ + public function __clone() + { + foreach ($this->_conditions as $i => $condition) { + if ($condition instanceof ExpressionInterface) { + $this->_conditions[$i] = clone $condition; + } + } + } +} diff --git a/vendor/cakephp/database/Expression/StringExpression.php b/vendor/cakephp/database/Expression/StringExpression.php new file mode 100644 index 0000000..e438074 --- /dev/null +++ b/vendor/cakephp/database/Expression/StringExpression.php @@ -0,0 +1,87 @@ +string = $string; + $this->collation = $collation; + } + + /** + * Sets the string collation. + * + * @param string $collation String collation + * @return void + */ + public function setCollation(string $collation): void + { + $this->collation = $collation; + } + + /** + * Returns the string collation. + * + * @return string + */ + public function getCollation(): string + { + return $this->collation; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $placeholder = $binder->placeholder('c'); + $binder->bind($placeholder, $this->string, 'string'); + + return $placeholder . ' COLLATE ' . $this->collation; + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + return $this; + } +} diff --git a/vendor/cakephp/database/Expression/TupleComparison.php b/vendor/cakephp/database/Expression/TupleComparison.php new file mode 100644 index 0000000..b5c36f0 --- /dev/null +++ b/vendor/cakephp/database/Expression/TupleComparison.php @@ -0,0 +1,231 @@ + + * @psalm-suppress NonInvariantDocblockPropertyType + */ + protected $_type; + + /** + * Constructor + * + * @param \Cake\Database\ExpressionInterface|array|string $fields the fields to use to form a tuple + * @param \Cake\Database\ExpressionInterface|array $values the values to use to form a tuple + * @param array $types the types names to use for casting each of the values, only + * one type per position in the value array in needed + * @param string $conjunction the operator used for comparing field and value + */ + public function __construct($fields, $values, array $types = [], string $conjunction = '=') + { + $this->_type = $types; + $this->setField($fields); + $this->_operator = $conjunction; + $this->setValue($values); + } + + /** + * Returns the type to be used for casting the value to a database representation + * + * @return array + */ + public function getType(): array + { + return $this->_type; + } + + /** + * Sets the value + * + * @param mixed $value The value to compare + * @return void + */ + public function setValue($value): void + { + if ($this->isMulti()) { + if (is_array($value) && !is_array(current($value))) { + throw new InvalidArgumentException( + 'Multi-tuple comparisons require a multi-tuple value, single-tuple given.' + ); + } + } else { + if (is_array($value) && is_array(current($value))) { + throw new InvalidArgumentException( + 'Single-tuple comparisons require a single-tuple value, multi-tuple given.' + ); + } + } + + $this->_value = $value; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $template = '(%s) %s (%s)'; + $fields = []; + $originalFields = $this->getField(); + + if (!is_array($originalFields)) { + $originalFields = [$originalFields]; + } + + foreach ($originalFields as $field) { + $fields[] = $field instanceof ExpressionInterface ? $field->sql($binder) : $field; + } + + $values = $this->_stringifyValues($binder); + + $field = implode(', ', $fields); + + return sprintf($template, $field, $this->_operator, $values); + } + + /** + * Returns a string with the values as placeholders in a string to be used + * for the SQL version of this expression + * + * @param \Cake\Database\ValueBinder $binder The value binder to convert expressions with. + * @return string + */ + protected function _stringifyValues(ValueBinder $binder): string + { + $values = []; + $parts = $this->getValue(); + + if ($parts instanceof ExpressionInterface) { + return $parts->sql($binder); + } + + foreach ($parts as $i => $value) { + if ($value instanceof ExpressionInterface) { + $values[] = $value->sql($binder); + continue; + } + + $type = $this->_type; + $isMultiOperation = $this->isMulti(); + if (empty($type)) { + $type = null; + } + + if ($isMultiOperation) { + $bound = []; + foreach ($value as $k => $val) { + /** @var string $valType */ + $valType = $type && isset($type[$k]) ? $type[$k] : $type; + $bound[] = $this->_bindValue($val, $binder, $valType); + } + + $values[] = sprintf('(%s)', implode(',', $bound)); + continue; + } + + /** @var string $valType */ + $valType = $type && isset($type[$i]) ? $type[$i] : $type; + $values[] = $this->_bindValue($value, $binder, $valType); + } + + return implode(', ', $values); + } + + /** + * @inheritDoc + */ + protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string + { + $placeholder = $binder->placeholder('tuple'); + $binder->bind($placeholder, $value, $type); + + return $placeholder; + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + /** @var array $fields */ + $fields = $this->getField(); + foreach ($fields as $field) { + $this->_traverseValue($field, $callback); + } + + $value = $this->getValue(); + if ($value instanceof ExpressionInterface) { + $callback($value); + $value->traverse($callback); + + return $this; + } + + foreach ($value as $val) { + if ($this->isMulti()) { + foreach ($val as $v) { + $this->_traverseValue($v, $callback); + } + } else { + $this->_traverseValue($val, $callback); + } + } + + return $this; + } + + /** + * Conditionally executes the callback for the passed value if + * it is an ExpressionInterface + * + * @param mixed $value The value to traverse + * @param \Closure $callback The callable to use when traversing + * @return void + */ + protected function _traverseValue($value, Closure $callback): void + { + if ($value instanceof ExpressionInterface) { + $callback($value); + $value->traverse($callback); + } + } + + /** + * Determines if each of the values in this expressions is a tuple in + * itself + * + * @return bool + */ + public function isMulti(): bool + { + return in_array(strtolower($this->_operator), ['in', 'not in']); + } +} diff --git a/vendor/cakephp/database/Expression/UnaryExpression.php b/vendor/cakephp/database/Expression/UnaryExpression.php new file mode 100644 index 0000000..1c40f91 --- /dev/null +++ b/vendor/cakephp/database/Expression/UnaryExpression.php @@ -0,0 +1,118 @@ +_operator = $operator; + $this->_value = $value; + $this->position = $position; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $operand = $this->_value; + if ($operand instanceof ExpressionInterface) { + $operand = $operand->sql($binder); + } + + if ($this->position === self::POSTFIX) { + return '(' . $operand . ') ' . $this->_operator; + } + + return $this->_operator . ' (' . $operand . ')'; + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + if ($this->_value instanceof ExpressionInterface) { + $callback($this->_value); + $this->_value->traverse($callback); + } + + return $this; + } + + /** + * Perform a deep clone of the inner expression. + * + * @return void + */ + public function __clone() + { + if ($this->_value instanceof ExpressionInterface) { + $this->_value = clone $this->_value; + } + } +} diff --git a/vendor/cakephp/database/Expression/ValuesExpression.php b/vendor/cakephp/database/Expression/ValuesExpression.php new file mode 100644 index 0000000..68dbf25 --- /dev/null +++ b/vendor/cakephp/database/Expression/ValuesExpression.php @@ -0,0 +1,325 @@ + type names + */ + public function __construct(array $columns, TypeMap $typeMap) + { + $this->_columns = $columns; + $this->setTypeMap($typeMap); + } + + /** + * Add a row of data to be inserted. + * + * @param \Cake\Database\Query|array $values Array of data to append into the insert, or + * a query for doing INSERT INTO .. SELECT style commands + * @return void + * @throws \Cake\Database\Exception\DatabaseException When mixing array + Query data types. + */ + public function add($values): void + { + if ( + ( + count($this->_values) && + $values instanceof Query + ) || + ( + $this->_query && + is_array($values) + ) + ) { + throw new DatabaseException( + 'You cannot mix subqueries and array values in inserts.' + ); + } + if ($values instanceof Query) { + $this->setQuery($values); + + return; + } + $this->_values[] = $values; + $this->_castedExpressions = false; + } + + /** + * Sets the columns to be inserted. + * + * @param array $columns Array with columns to be inserted. + * @return $this + */ + public function setColumns(array $columns) + { + $this->_columns = $columns; + $this->_castedExpressions = false; + + return $this; + } + + /** + * Gets the columns to be inserted. + * + * @return array + */ + public function getColumns(): array + { + return $this->_columns; + } + + /** + * Get the bare column names. + * + * Because column names could be identifier quoted, we + * need to strip the identifiers off of the columns. + * + * @return array + */ + protected function _columnNames(): array + { + $columns = []; + foreach ($this->_columns as $col) { + if (is_string($col)) { + $col = trim($col, '`[]"'); + } + $columns[] = $col; + } + + return $columns; + } + + /** + * Sets the values to be inserted. + * + * @param array $values Array with values to be inserted. + * @return $this + */ + public function setValues(array $values) + { + $this->_values = $values; + $this->_castedExpressions = false; + + return $this; + } + + /** + * Gets the values to be inserted. + * + * @return array + */ + public function getValues(): array + { + if (!$this->_castedExpressions) { + $this->_processExpressions(); + } + + return $this->_values; + } + + /** + * Sets the query object to be used as the values expression to be evaluated + * to insert records in the table. + * + * @param \Cake\Database\Query $query The query to set + * @return $this + */ + public function setQuery(Query $query) + { + $this->_query = $query; + + return $this; + } + + /** + * Gets the query object to be used as the values expression to be evaluated + * to insert records in the table. + * + * @return \Cake\Database\Query|null + */ + public function getQuery(): ?Query + { + return $this->_query; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + if (empty($this->_values) && empty($this->_query)) { + return ''; + } + + if (!$this->_castedExpressions) { + $this->_processExpressions(); + } + + $columns = $this->_columnNames(); + $defaults = array_fill_keys($columns, null); + $placeholders = []; + + $types = []; + $typeMap = $this->getTypeMap(); + foreach ($defaults as $col => $v) { + $types[$col] = $typeMap->type($col); + } + + foreach ($this->_values as $row) { + $row += $defaults; + $rowPlaceholders = []; + + foreach ($columns as $column) { + $value = $row[$column]; + + if ($value instanceof ExpressionInterface) { + $rowPlaceholders[] = '(' . $value->sql($binder) . ')'; + continue; + } + + $placeholder = $binder->placeholder('c'); + $rowPlaceholders[] = $placeholder; + $binder->bind($placeholder, $value, $types[$column]); + } + + $placeholders[] = implode(', ', $rowPlaceholders); + } + + $query = $this->getQuery(); + if ($query) { + return ' ' . $query->sql($binder); + } + + return sprintf(' VALUES (%s)', implode('), (', $placeholders)); + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + if ($this->_query) { + return $this; + } + + if (!$this->_castedExpressions) { + $this->_processExpressions(); + } + + foreach ($this->_values as $v) { + if ($v instanceof ExpressionInterface) { + $v->traverse($callback); + } + if (!is_array($v)) { + continue; + } + foreach ($v as $field) { + if ($field instanceof ExpressionInterface) { + $callback($field); + $field->traverse($callback); + } + } + } + + return $this; + } + + /** + * Converts values that need to be casted to expressions + * + * @return void + */ + protected function _processExpressions(): void + { + $types = []; + $typeMap = $this->getTypeMap(); + + $columns = $this->_columnNames(); + foreach ($columns as $c) { + if (!is_string($c) && !is_int($c)) { + continue; + } + $types[$c] = $typeMap->type($c); + } + + $types = $this->_requiresToExpressionCasting($types); + + if (empty($types)) { + return; + } + + foreach ($this->_values as $row => $values) { + foreach ($types as $col => $type) { + /** @var \Cake\Database\Type\ExpressionTypeInterface $type */ + $this->_values[$row][$col] = $type->toExpression($values[$col]); + } + } + $this->_castedExpressions = true; + } +} diff --git a/vendor/cakephp/database/Expression/WhenThenExpression.php b/vendor/cakephp/database/Expression/WhenThenExpression.php new file mode 100644 index 0000000..bf51eaf --- /dev/null +++ b/vendor/cakephp/database/Expression/WhenThenExpression.php @@ -0,0 +1,349 @@ + + */ + protected $validClauseNames = [ + 'when', + 'then', + ]; + + /** + * The type map to use when using an array of conditions for the + * `WHEN` value. + * + * @var \Cake\Database\TypeMap + */ + protected $_typeMap; + + /** + * Then `WHEN` value. + * + * @var \Cake\Database\ExpressionInterface|object|scalar|null + */ + protected $when = null; + + /** + * The `WHEN` value type. + * + * @var array|string|null + */ + protected $whenType = null; + + /** + * The `THEN` value. + * + * @var \Cake\Database\ExpressionInterface|object|scalar|null + */ + protected $then = null; + + /** + * Whether the `THEN` value has been defined, eg whether `then()` + * has been invoked. + * + * @var bool + */ + protected $hasThenBeenDefined = false; + + /** + * The `THEN` result type. + * + * @var string|null + */ + protected $thenType = null; + + /** + * Constructor. + * + * @param \Cake\Database\TypeMap|null $typeMap The type map to use when using an array of conditions for the `WHEN` + * value. + */ + public function __construct(?TypeMap $typeMap = null) + { + if ($typeMap === null) { + $typeMap = new TypeMap(); + } + $this->_typeMap = $typeMap; + } + + /** + * Sets the `WHEN` value. + * + * @param \Cake\Database\ExpressionInterface|object|array|scalar $when The `WHEN` value. When using an array of + * conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is _not_ + * completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If you + * plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to be + * a non-array, and then always binds the data), use a conditions array where the user data is only passed on the + * value side of the array entries, or custom bindings! + * @param array|string|null $type The when value type. Either an associative array when using array style + * conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value. + * @return $this + * @throws \InvalidArgumentException In case the `$when` argument is neither a non-empty array, nor a scalar value, + * an object, or an instance of `\Cake\Database\ExpressionInterface`. + * @throws \InvalidArgumentException In case the `$type` argument is neither an array, a string, nor null. + * @throws \InvalidArgumentException In case the `$when` argument is an array, and the `$type` argument is neither + * an array, nor null. + * @throws \InvalidArgumentException In case the `$when` argument is a non-array value, and the `$type` argument is + * neither a string, nor null. + * @see CaseStatementExpression::when() for a more detailed usage explanation. + */ + public function when($when, $type = null) + { + if ( + !(is_array($when) && !empty($when)) && + !is_scalar($when) && + !is_object($when) + ) { + throw new InvalidArgumentException(sprintf( + 'The `$when` argument must be either a non-empty array, a scalar value, an object, ' . + 'or an instance of `\%s`, `%s` given.', + ExpressionInterface::class, + is_array($when) ? '[]' : getTypeName($when) + )); + } + + if ( + $type !== null && + !is_array($type) && + !is_string($type) + ) { + throw new InvalidArgumentException(sprintf( + 'The `$type` argument must be either an array, a string, or `null`, `%s` given.', + getTypeName($type) + )); + } + + if (is_array($when)) { + if ( + $type !== null && + !is_array($type) + ) { + throw new InvalidArgumentException(sprintf( + 'When using an array for the `$when` argument, the `$type` argument must be an ' . + 'array too, `%s` given.', + getTypeName($type) + )); + } + + // avoid dirtying the type map for possible consecutive `when()` calls + $typeMap = clone $this->_typeMap; + if ( + is_array($type) && + count($type) > 0 + ) { + $typeMap = $typeMap->setTypes($type); + } + + $when = new QueryExpression($when, $typeMap); + } else { + if ( + $type !== null && + !is_string($type) + ) { + throw new InvalidArgumentException(sprintf( + 'When using a non-array value for the `$when` argument, the `$type` argument must ' . + 'be a string, `%s` given.', + getTypeName($type) + )); + } + + if ( + $type === null && + !($when instanceof ExpressionInterface) + ) { + $type = $this->inferType($when); + } + } + + $this->when = $when; + $this->whenType = $type; + + return $this; + } + + /** + * Sets the `THEN` result value. + * + * @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value. + * @param string|null $type The result type. If no type is provided, the type will be inferred from the given + * result value. + * @return $this + */ + public function then($result, ?string $type = null) + { + if ( + $result !== null && + !is_scalar($result) && + !(is_object($result) && !($result instanceof Closure)) + ) { + throw new InvalidArgumentException(sprintf( + 'The `$result` argument must be either `null`, a scalar value, an object, ' . + 'or an instance of `\%s`, `%s` given.', + ExpressionInterface::class, + getTypeName($result) + )); + } + + $this->then = $result; + + if ($type === null) { + $type = $this->inferType($result); + } + + $this->thenType = $type; + + $this->hasThenBeenDefined = true; + + return $this; + } + + /** + * Returns the expression's result value type. + * + * @return string|null + * @see WhenThenExpression::then() + */ + public function getResultType(): ?string + { + return $this->thenType; + } + + /** + * Returns the available data for the given clause. + * + * ### Available clauses + * + * The following clause names are available: + * + * * `when`: The `WHEN` value. + * * `then`: The `THEN` result value. + * + * @param string $clause The name of the clause to obtain. + * @return \Cake\Database\ExpressionInterface|object|scalar|null + * @throws \InvalidArgumentException In case the given clause name is invalid. + */ + public function clause(string $clause) + { + if (!in_array($clause, $this->validClauseNames, true)) { + throw new InvalidArgumentException( + sprintf( + 'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.', + implode('`, `', $this->validClauseNames), + $clause + ) + ); + } + + return $this->{$clause}; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + if ($this->when === null) { + throw new LogicException('Case expression has incomplete when clause. Missing `when()`.'); + } + + if (!$this->hasThenBeenDefined) { + throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.'); + } + + $when = $this->when; + if ( + is_string($this->whenType) && + !($when instanceof ExpressionInterface) + ) { + $when = $this->_castToExpression($when, $this->whenType); + } + if ($when instanceof Query) { + $when = sprintf('(%s)', $when->sql($binder)); + } elseif ($when instanceof ExpressionInterface) { + $when = $when->sql($binder); + } else { + $placeholder = $binder->placeholder('c'); + if (is_string($this->whenType)) { + $whenType = $this->whenType; + } else { + $whenType = null; + } + $binder->bind($placeholder, $when, $whenType); + $when = $placeholder; + } + + $then = $this->compileNullableValue($binder, $this->then, $this->thenType); + + return "WHEN $when THEN $then"; + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + if ($this->when instanceof ExpressionInterface) { + $callback($this->when); + $this->when->traverse($callback); + } + + if ($this->then instanceof ExpressionInterface) { + $callback($this->then); + $this->then->traverse($callback); + } + + return $this; + } + + /** + * Clones the inner expression objects. + * + * @return void + */ + public function __clone() + { + if ($this->when instanceof ExpressionInterface) { + $this->when = clone $this->when; + } + + if ($this->then instanceof ExpressionInterface) { + $this->then = clone $this->then; + } + } +} diff --git a/vendor/cakephp/database/Expression/WindowExpression.php b/vendor/cakephp/database/Expression/WindowExpression.php new file mode 100644 index 0000000..3604be4 --- /dev/null +++ b/vendor/cakephp/database/Expression/WindowExpression.php @@ -0,0 +1,337 @@ + + */ + protected $partitions = []; + + /** + * @var \Cake\Database\Expression\OrderByExpression|null + */ + protected $order; + + /** + * @var array|null + */ + protected $frame; + + /** + * @var string|null + */ + protected $exclusion; + + /** + * @param string $name Window name + */ + public function __construct(string $name = '') + { + $this->name = new IdentifierExpression($name); + } + + /** + * Return whether is only a named window expression. + * + * These window expressions only specify a named window and do not + * specify their own partitions, frame or order. + * + * @return bool + */ + public function isNamedOnly(): bool + { + return $this->name->getIdentifier() && (!$this->partitions && !$this->frame && !$this->order); + } + + /** + * Sets the window name. + * + * @param string $name Window name + * @return $this + */ + public function name(string $name) + { + $this->name = new IdentifierExpression($name); + + return $this; + } + + /** + * @inheritDoc + */ + public function partition($partitions) + { + if (!$partitions) { + return $this; + } + + if ($partitions instanceof Closure) { + $partitions = $partitions(new QueryExpression([], [], '')); + } + + if (!is_array($partitions)) { + $partitions = [$partitions]; + } + + foreach ($partitions as &$partition) { + if (is_string($partition)) { + $partition = new IdentifierExpression($partition); + } + } + + $this->partitions = array_merge($this->partitions, $partitions); + + return $this; + } + + /** + * @inheritDoc + */ + public function order($fields) + { + if (!$fields) { + return $this; + } + + if ($this->order === null) { + $this->order = new OrderByExpression(); + } + + if ($fields instanceof Closure) { + $fields = $fields(new QueryExpression([], [], '')); + } + + $this->order->add($fields); + + return $this; + } + + /** + * @inheritDoc + */ + public function range($start, $end = 0) + { + return $this->frame(self::RANGE, $start, self::PRECEDING, $end, self::FOLLOWING); + } + + /** + * @inheritDoc + */ + public function rows(?int $start, ?int $end = 0) + { + return $this->frame(self::ROWS, $start, self::PRECEDING, $end, self::FOLLOWING); + } + + /** + * @inheritDoc + */ + public function groups(?int $start, ?int $end = 0) + { + return $this->frame(self::GROUPS, $start, self::PRECEDING, $end, self::FOLLOWING); + } + + /** + * @inheritDoc + */ + public function frame( + string $type, + $startOffset, + string $startDirection, + $endOffset, + string $endDirection + ) { + $this->frame = [ + 'type' => $type, + 'start' => [ + 'offset' => $startOffset, + 'direction' => $startDirection, + ], + 'end' => [ + 'offset' => $endOffset, + 'direction' => $endDirection, + ], + ]; + + return $this; + } + + /** + * @inheritDoc + */ + public function excludeCurrent() + { + $this->exclusion = 'CURRENT ROW'; + + return $this; + } + + /** + * @inheritDoc + */ + public function excludeGroup() + { + $this->exclusion = 'GROUP'; + + return $this; + } + + /** + * @inheritDoc + */ + public function excludeTies() + { + $this->exclusion = 'TIES'; + + return $this; + } + + /** + * @inheritDoc + */ + public function sql(ValueBinder $binder): string + { + $clauses = []; + if ($this->name->getIdentifier()) { + $clauses[] = $this->name->sql($binder); + } + + if ($this->partitions) { + $expressions = []; + foreach ($this->partitions as $partition) { + $expressions[] = $partition->sql($binder); + } + + $clauses[] = 'PARTITION BY ' . implode(', ', $expressions); + } + + if ($this->order) { + $clauses[] = $this->order->sql($binder); + } + + if ($this->frame) { + $start = $this->buildOffsetSql( + $binder, + $this->frame['start']['offset'], + $this->frame['start']['direction'] + ); + $end = $this->buildOffsetSql( + $binder, + $this->frame['end']['offset'], + $this->frame['end']['direction'] + ); + + $frameSql = sprintf('%s BETWEEN %s AND %s', $this->frame['type'], $start, $end); + + if ($this->exclusion !== null) { + $frameSql .= ' EXCLUDE ' . $this->exclusion; + } + + $clauses[] = $frameSql; + } + + return implode(' ', $clauses); + } + + /** + * @inheritDoc + */ + public function traverse(Closure $callback) + { + $callback($this->name); + foreach ($this->partitions as $partition) { + $callback($partition); + $partition->traverse($callback); + } + + if ($this->order) { + $callback($this->order); + $this->order->traverse($callback); + } + + if ($this->frame !== null) { + $offset = $this->frame['start']['offset']; + if ($offset instanceof ExpressionInterface) { + $callback($offset); + $offset->traverse($callback); + } + $offset = $this->frame['end']['offset'] ?? null; + if ($offset instanceof ExpressionInterface) { + $callback($offset); + $offset->traverse($callback); + } + } + + return $this; + } + + /** + * Builds frame offset sql. + * + * @param \Cake\Database\ValueBinder $binder Value binder + * @param \Cake\Database\ExpressionInterface|string|int|null $offset Frame offset + * @param string $direction Frame offset direction + * @return string + */ + protected function buildOffsetSql(ValueBinder $binder, $offset, string $direction): string + { + if ($offset === 0) { + return 'CURRENT ROW'; + } + + if ($offset instanceof ExpressionInterface) { + $offset = $offset->sql($binder); + } + + $sql = sprintf( + '%s %s', + $offset ?? 'UNBOUNDED', + $direction + ); + + return $sql; + } + + /** + * Clone this object and its subtree of expressions. + * + * @return void + */ + public function __clone() + { + $this->name = clone $this->name; + foreach ($this->partitions as $i => $partition) { + $this->partitions[$i] = clone $partition; + } + if ($this->order !== null) { + $this->order = clone $this->order; + } + } +} diff --git a/vendor/cakephp/database/Expression/WindowInterface.php b/vendor/cakephp/database/Expression/WindowInterface.php new file mode 100644 index 0000000..7a7bac5 --- /dev/null +++ b/vendor/cakephp/database/Expression/WindowInterface.php @@ -0,0 +1,163 @@ +|string $partitions Partition expressions + * @return $this + */ + public function partition($partitions); + + /** + * Adds one or more order clauses to the window. + * + * @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $fields Order expressions + * @return $this + */ + public function order($fields); + + /** + * Adds a simple range frame to the window. + * + * `$start`: + * - `0` - 'CURRENT ROW' + * - `null` - 'UNBOUNDED PRECEDING' + * - offset - 'offset PRECEDING' + * + * `$end`: + * - `0` - 'CURRENT ROW' + * - `null` - 'UNBOUNDED FOLLOWING' + * - offset - 'offset FOLLOWING' + * + * If you need to use 'FOLLOWING' with frame start or + * 'PRECEDING' with frame end, use `frame()` instead. + * + * @param \Cake\Database\ExpressionInterface|string|int|null $start Frame start + * @param \Cake\Database\ExpressionInterface|string|int|null $end Frame end + * If not passed in, only frame start SQL will be generated. + * @return $this + */ + public function range($start, $end = 0); + + /** + * Adds a simple rows frame to the window. + * + * See `range()` for details. + * + * @param int|null $start Frame start + * @param int|null $end Frame end + * If not passed in, only frame start SQL will be generated. + * @return $this + */ + public function rows(?int $start, ?int $end = 0); + + /** + * Adds a simple groups frame to the window. + * + * See `range()` for details. + * + * @param int|null $start Frame start + * @param int|null $end Frame end + * If not passed in, only frame start SQL will be generated. + * @return $this + */ + public function groups(?int $start, ?int $end = 0); + + /** + * Adds a frame to the window. + * + * Use the `range()`, `rows()` or `groups()` helpers if you need simple + * 'BETWEEN offset PRECEDING and offset FOLLOWING' frames. + * + * You can specify any direction for both frame start and frame end. + * + * With both `$startOffset` and `$endOffset`: + * - `0` - 'CURRENT ROW' + * - `null` - 'UNBOUNDED' + * + * @param string $type Frame type + * @param \Cake\Database\ExpressionInterface|string|int|null $startOffset Frame start offset + * @param string $startDirection Frame start direction + * @param \Cake\Database\ExpressionInterface|string|int|null $endOffset Frame end offset + * @param string $endDirection Frame end direction + * @return $this + * @throws \InvalidArgumentException WHen offsets are negative. + * @psalm-param self::RANGE|self::ROWS|self::GROUPS $type + * @psalm-param self::PRECEDING|self::FOLLOWING $startDirection + * @psalm-param self::PRECEDING|self::FOLLOWING $endDirection + */ + public function frame( + string $type, + $startOffset, + string $startDirection, + $endOffset, + string $endDirection + ); + + /** + * Adds current row frame exclusion. + * + * @return $this + */ + public function excludeCurrent(); + + /** + * Adds group frame exclusion. + * + * @return $this + */ + public function excludeGroup(); + + /** + * Adds ties frame exclusion. + * + * @return $this + */ + public function excludeTies(); +} diff --git a/vendor/cakephp/database/ExpressionInterface.php b/vendor/cakephp/database/ExpressionInterface.php new file mode 100644 index 0000000..3e3588c --- /dev/null +++ b/vendor/cakephp/database/ExpressionInterface.php @@ -0,0 +1,44 @@ + + */ + protected $_typeMap; + + /** + * An array containing the name of the fields and the Type objects + * each should use when converting them using batching. + * + * @var array + */ + protected $batchingTypeMap; + + /** + * An array containing all the types registered in the Type system + * at the moment this object is created. Used so that the types list + * is not fetched on each single row of the results. + * + * @var array<\Cake\Database\TypeInterface|\Cake\Database\Type\BatchCastingInterface> + */ + protected $types; + + /** + * The driver object to be used in the type conversion + * + * @var \Cake\Database\DriverInterface + */ + protected $_driver; + + /** + * Builds the type map + * + * @param \Cake\Database\TypeMap $typeMap Contains the types to use for converting results + * @param \Cake\Database\DriverInterface $driver The driver to use for the type conversion + */ + public function __construct(TypeMap $typeMap, DriverInterface $driver) + { + $this->_driver = $driver; + $map = $typeMap->toArray(); + $types = TypeFactory::buildAll(); + + $simpleMap = $batchingMap = []; + $simpleResult = $batchingResult = []; + + foreach ($types as $k => $type) { + if ($type instanceof OptionalConvertInterface && !$type->requiresToPhpCast()) { + continue; + } + + if ($type instanceof BatchCastingInterface) { + $batchingMap[$k] = $type; + continue; + } + + $simpleMap[$k] = $type; + } + + foreach ($map as $field => $type) { + if (isset($simpleMap[$type])) { + $simpleResult[$field] = $simpleMap[$type]; + continue; + } + if (isset($batchingMap[$type])) { + $batchingResult[$type][] = $field; + } + } + + // Using batching when there is only a couple for the type is actually slower, + // so, let's check for that case here. + foreach ($batchingResult as $type => $fields) { + if (count($fields) > 2) { + continue; + } + + foreach ($fields as $f) { + $simpleResult[$f] = $batchingMap[$type]; + } + unset($batchingResult[$type]); + } + + $this->types = $types; + $this->_typeMap = $simpleResult; + $this->batchingTypeMap = $batchingResult; + } + + /** + * Converts each of the fields in the array that are present in the type map + * using the corresponding Type class. + * + * @param array $row The array with the fields to be casted + * @return array + */ + public function __invoke(array $row): array + { + if (!empty($this->_typeMap)) { + foreach ($this->_typeMap as $field => $type) { + $row[$field] = $type->toPHP($row[$field], $this->_driver); + } + } + + if (!empty($this->batchingTypeMap)) { + foreach ($this->batchingTypeMap as $t => $fields) { + /** @psalm-suppress PossiblyUndefinedMethod */ + $row = $this->types[$t]->manyToPHP($row, $fields, $this->_driver); + } + } + + return $row; + } +} diff --git a/vendor/cakephp/database/FunctionsBuilder.php b/vendor/cakephp/database/FunctionsBuilder.php new file mode 100644 index 0000000..1251fca --- /dev/null +++ b/vendor/cakephp/database/FunctionsBuilder.php @@ -0,0 +1,375 @@ +aggregate('SUM', $this->toLiteralParam($expression), $types, $returnType); + } + + /** + * Returns a AggregateExpression representing a call to SQL AVG function. + * + * @param \Cake\Database\ExpressionInterface|string $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\AggregateExpression + */ + public function avg($expression, $types = []): AggregateExpression + { + return $this->aggregate('AVG', $this->toLiteralParam($expression), $types, 'float'); + } + + /** + * Returns a AggregateExpression representing a call to SQL MAX function. + * + * @param \Cake\Database\ExpressionInterface|string $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\AggregateExpression + */ + public function max($expression, $types = []): AggregateExpression + { + return $this->aggregate('MAX', $this->toLiteralParam($expression), $types, current($types) ?: 'float'); + } + + /** + * Returns a AggregateExpression representing a call to SQL MIN function. + * + * @param \Cake\Database\ExpressionInterface|string $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\AggregateExpression + */ + public function min($expression, $types = []): AggregateExpression + { + return $this->aggregate('MIN', $this->toLiteralParam($expression), $types, current($types) ?: 'float'); + } + + /** + * Returns a AggregateExpression representing a call to SQL COUNT function. + * + * @param \Cake\Database\ExpressionInterface|string $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\AggregateExpression + */ + public function count($expression, $types = []): AggregateExpression + { + return $this->aggregate('COUNT', $this->toLiteralParam($expression), $types, 'integer'); + } + + /** + * Returns a FunctionExpression representing a string concatenation + * + * @param array $args List of strings or expressions to concatenate + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function concat(array $args, array $types = []): FunctionExpression + { + return new FunctionExpression('CONCAT', $args, $types, 'string'); + } + + /** + * Returns a FunctionExpression representing a call to SQL COALESCE function. + * + * @param array $args List of expressions to evaluate as function parameters + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function coalesce(array $args, array $types = []): FunctionExpression + { + return new FunctionExpression('COALESCE', $args, $types, current($types) ?: 'string'); + } + + /** + * Returns a FunctionExpression representing a SQL CAST. + * + * The `$type` parameter is a SQL type. The return type for the returned expression + * is the default type name. Use `setReturnType()` to update it. + * + * @param \Cake\Database\ExpressionInterface|string $field Field or expression to cast. + * @param string $type The SQL data type + * @return \Cake\Database\Expression\FunctionExpression + */ + public function cast($field, string $type = ''): FunctionExpression + { + if (is_array($field)) { + deprecationWarning( + 'Build cast function by FunctionsBuilder::cast(array $args) is deprecated. ' . + 'Use FunctionsBuilder::cast($field, string $type) instead.' + ); + + return new FunctionExpression('CAST', $field); + } + + if (empty($type)) { + throw new InvalidArgumentException('The `$type` in a cast cannot be empty.'); + } + + $expression = new FunctionExpression('CAST', $this->toLiteralParam($field)); + $expression->setConjunction(' AS')->add([$type => 'literal']); + + return $expression; + } + + /** + * Returns a FunctionExpression representing the difference in days between + * two dates. + * + * @param array $args List of expressions to obtain the difference in days. + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function dateDiff(array $args, array $types = []): FunctionExpression + { + return new FunctionExpression('DATEDIFF', $args, $types, 'integer'); + } + + /** + * Returns the specified date part from the SQL expression. + * + * @param string $part Part of the date to return. + * @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from. + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function datePart(string $part, $expression, array $types = []): FunctionExpression + { + return $this->extract($part, $expression, $types); + } + + /** + * Returns the specified date part from the SQL expression. + * + * @param string $part Part of the date to return. + * @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from. + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function extract(string $part, $expression, array $types = []): FunctionExpression + { + $expression = new FunctionExpression('EXTRACT', $this->toLiteralParam($expression), $types, 'integer'); + $expression->setConjunction(' FROM')->add([$part => 'literal'], [], true); + + return $expression; + } + + /** + * Add the time unit to the date expression + * + * @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from. + * @param string|int $value Value to be added. Use negative to subtract. + * @param string $unit Unit of the value e.g. hour or day. + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function dateAdd($expression, $value, string $unit, array $types = []): FunctionExpression + { + if (!is_numeric($value)) { + $value = 0; + } + $interval = $value . ' ' . $unit; + $expression = new FunctionExpression('DATE_ADD', $this->toLiteralParam($expression), $types, 'datetime'); + $expression->setConjunction(', INTERVAL')->add([$interval => 'literal']); + + return $expression; + } + + /** + * Returns a FunctionExpression representing a call to SQL WEEKDAY function. + * 1 - Sunday, 2 - Monday, 3 - Tuesday... + * + * @param \Cake\Database\ExpressionInterface|string $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function dayOfWeek($expression, $types = []): FunctionExpression + { + return new FunctionExpression('DAYOFWEEK', $this->toLiteralParam($expression), $types, 'integer'); + } + + /** + * Returns a FunctionExpression representing a call to SQL WEEKDAY function. + * 1 - Sunday, 2 - Monday, 3 - Tuesday... + * + * @param \Cake\Database\ExpressionInterface|string $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function weekday($expression, $types = []): FunctionExpression + { + return $this->dayOfWeek($expression, $types); + } + + /** + * Returns a FunctionExpression representing a call that will return the current + * date and time. By default it returns both date and time, but you can also + * make it generate only the date or only the time. + * + * @param string $type (datetime|date|time) + * @return \Cake\Database\Expression\FunctionExpression + */ + public function now(string $type = 'datetime'): FunctionExpression + { + if ($type === 'datetime') { + return new FunctionExpression('NOW', [], [], 'datetime'); + } + if ($type === 'date') { + return new FunctionExpression('CURRENT_DATE', [], [], 'date'); + } + if ($type === 'time') { + return new FunctionExpression('CURRENT_TIME', [], [], 'time'); + } + + throw new InvalidArgumentException('Invalid argument for FunctionsBuilder::now(): ' . $type); + } + + /** + * Returns an AggregateExpression representing call to SQL ROW_NUMBER(). + * + * @return \Cake\Database\Expression\AggregateExpression + */ + public function rowNumber(): AggregateExpression + { + return (new AggregateExpression('ROW_NUMBER', [], [], 'integer'))->over(); + } + + /** + * Returns an AggregateExpression representing call to SQL LAG(). + * + * @param \Cake\Database\ExpressionInterface|string $expression The value evaluated at offset + * @param int $offset The row offset + * @param mixed $default The default value if offset doesn't exist + * @param string $type The output type of the lag expression. Defaults to float. + * @return \Cake\Database\Expression\AggregateExpression + */ + public function lag($expression, int $offset, $default = null, $type = null): AggregateExpression + { + $params = $this->toLiteralParam($expression) + [$offset => 'literal']; + if ($default !== null) { + $params[] = $default; + } + + $types = []; + if ($type !== null) { + $types = [$type, 'integer', $type]; + } + + return (new AggregateExpression('LAG', $params, $types, $type ?? 'float'))->over(); + } + + /** + * Returns an AggregateExpression representing call to SQL LEAD(). + * + * @param \Cake\Database\ExpressionInterface|string $expression The value evaluated at offset + * @param int $offset The row offset + * @param mixed $default The default value if offset doesn't exist + * @param string $type The output type of the lead expression. Defaults to float. + * @return \Cake\Database\Expression\AggregateExpression + */ + public function lead($expression, int $offset, $default = null, $type = null): AggregateExpression + { + $params = $this->toLiteralParam($expression) + [$offset => 'literal']; + if ($default !== null) { + $params[] = $default; + } + + $types = []; + if ($type !== null) { + $types = [$type, 'integer', $type]; + } + + return (new AggregateExpression('LEAD', $params, $types, $type ?? 'float'))->over(); + } + + /** + * Helper method to create arbitrary SQL aggregate function calls. + * + * @param string $name The SQL aggregate function name + * @param array $params Array of arguments to be passed to the function. + * Can be an associative array with the literal value or identifier: + * `['value' => 'literal']` or `['value' => 'identifier'] + * @param array $types Array of types that match the names used in `$params`: + * `['name' => 'type']` + * @param string $return Return type of the entire expression. Defaults to float. + * @return \Cake\Database\Expression\AggregateExpression + */ + public function aggregate(string $name, array $params = [], array $types = [], string $return = 'float') + { + return new AggregateExpression($name, $params, $types, $return); + } + + /** + * Magic method dispatcher to create custom SQL function calls + * + * @param string $name the SQL function name to construct + * @param array $args list with up to 3 arguments, first one being an array with + * parameters for the SQL function, the second one a list of types to bind to those + * params, and the third one the return type of the function + * @return \Cake\Database\Expression\FunctionExpression + */ + public function __call(string $name, array $args): FunctionExpression + { + return new FunctionExpression($name, ...$args); + } + + /** + * Creates function parameter array from expression or string literal. + * + * @param \Cake\Database\ExpressionInterface|string $expression function argument + * @return array<\Cake\Database\ExpressionInterface|string> + */ + protected function toLiteralParam($expression) + { + if (is_string($expression)) { + return [$expression => 'literal']; + } + + return [$expression]; + } +} diff --git a/vendor/cakephp/database/IdentifierQuoter.php b/vendor/cakephp/database/IdentifierQuoter.php new file mode 100644 index 0000000..6b21a5a --- /dev/null +++ b/vendor/cakephp/database/IdentifierQuoter.php @@ -0,0 +1,269 @@ +_driver = $driver; + } + + /** + * Iterates over each of the clauses in a query looking for identifiers and + * quotes them + * + * @param \Cake\Database\Query $query The query to have its identifiers quoted + * @return \Cake\Database\Query + */ + public function quote(Query $query): Query + { + $binder = $query->getValueBinder(); + $query->setValueBinder(null); + + if ($query->type() === 'insert') { + $this->_quoteInsert($query); + } elseif ($query->type() === 'update') { + $this->_quoteUpdate($query); + } else { + $this->_quoteParts($query); + } + + $query->traverseExpressions([$this, 'quoteExpression']); + $query->setValueBinder($binder); + + return $query; + } + + /** + * Quotes identifiers inside expression objects + * + * @param \Cake\Database\ExpressionInterface $expression The expression object to walk and quote. + * @return void + */ + public function quoteExpression(ExpressionInterface $expression): void + { + if ($expression instanceof FieldInterface) { + $this->_quoteComparison($expression); + + return; + } + + if ($expression instanceof OrderByExpression) { + $this->_quoteOrderBy($expression); + + return; + } + + if ($expression instanceof IdentifierExpression) { + $this->_quoteIdentifierExpression($expression); + + return; + } + } + + /** + * Quotes all identifiers in each of the clauses of a query + * + * @param \Cake\Database\Query $query The query to quote. + * @return void + */ + protected function _quoteParts(Query $query): void + { + foreach (['distinct', 'select', 'from', 'group'] as $part) { + $contents = $query->clause($part); + + if (!is_array($contents)) { + continue; + } + + $result = $this->_basicQuoter($contents); + if (!empty($result)) { + $query->{$part}($result, true); + } + } + + $joins = $query->clause('join'); + if ($joins) { + $joins = $this->_quoteJoins($joins); + $query->join($joins, [], true); + } + } + + /** + * A generic identifier quoting function used for various parts of the query + * + * @param array $part the part of the query to quote + * @return array + */ + protected function _basicQuoter(array $part): array + { + $result = []; + foreach ($part as $alias => $value) { + $value = !is_string($value) ? $value : $this->_driver->quoteIdentifier($value); + $alias = is_numeric($alias) ? $alias : $this->_driver->quoteIdentifier($alias); + $result[$alias] = $value; + } + + return $result; + } + + /** + * Quotes both the table and alias for an array of joins as stored in a Query + * object + * + * @param array $joins The joins to quote. + * @return array + */ + protected function _quoteJoins(array $joins): array + { + $result = []; + foreach ($joins as $value) { + $alias = ''; + if (!empty($value['alias'])) { + $alias = $this->_driver->quoteIdentifier($value['alias']); + $value['alias'] = $alias; + } + + if (is_string($value['table'])) { + $value['table'] = $this->_driver->quoteIdentifier($value['table']); + } + + $result[$alias] = $value; + } + + return $result; + } + + /** + * Quotes the table name and columns for an insert query + * + * @param \Cake\Database\Query $query The insert query to quote. + * @return void + */ + protected function _quoteInsert(Query $query): void + { + $insert = $query->clause('insert'); + if (!isset($insert[0]) || !isset($insert[1])) { + return; + } + [$table, $columns] = $insert; + $table = $this->_driver->quoteIdentifier($table); + foreach ($columns as &$column) { + if (is_scalar($column)) { + $column = $this->_driver->quoteIdentifier((string)$column); + } + } + $query->insert($columns)->into($table); + } + + /** + * Quotes the table name for an update query + * + * @param \Cake\Database\Query $query The update query to quote. + * @return void + */ + protected function _quoteUpdate(Query $query): void + { + $table = $query->clause('update')[0]; + + if (is_string($table)) { + $query->update($this->_driver->quoteIdentifier($table)); + } + } + + /** + * Quotes identifiers in expression objects implementing the field interface + * + * @param \Cake\Database\Expression\FieldInterface $expression The expression to quote. + * @return void + */ + protected function _quoteComparison(FieldInterface $expression): void + { + $field = $expression->getField(); + if (is_string($field)) { + $expression->setField($this->_driver->quoteIdentifier($field)); + } elseif (is_array($field)) { + $quoted = []; + foreach ($field as $f) { + $quoted[] = $this->_driver->quoteIdentifier($f); + } + $expression->setField($quoted); + } elseif ($field instanceof ExpressionInterface) { + $this->quoteExpression($field); + } + } + + /** + * Quotes identifiers in "order by" expression objects + * + * Strings with spaces are treated as literal expressions + * and will not have identifiers quoted. + * + * @param \Cake\Database\Expression\OrderByExpression $expression The expression to quote. + * @return void + */ + protected function _quoteOrderBy(OrderByExpression $expression): void + { + $expression->iterateParts(function ($part, &$field) { + if (is_string($field)) { + $field = $this->_driver->quoteIdentifier($field); + + return $part; + } + if (is_string($part) && strpos($part, ' ') === false) { + return $this->_driver->quoteIdentifier($part); + } + + return $part; + }); + } + + /** + * Quotes identifiers in "order by" expression objects + * + * @param \Cake\Database\Expression\IdentifierExpression $expression The identifiers to quote. + * @return void + */ + protected function _quoteIdentifierExpression(IdentifierExpression $expression): void + { + $expression->setIdentifier( + $this->_driver->quoteIdentifier($expression->getIdentifier()) + ); + } +} diff --git a/vendor/cakephp/database/LICENSE.txt b/vendor/cakephp/database/LICENSE.txt new file mode 100644 index 0000000..b938c9e --- /dev/null +++ b/vendor/cakephp/database/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/cakephp/database/Log/LoggedQuery.php b/vendor/cakephp/database/Log/LoggedQuery.php new file mode 100644 index 0000000..6560a36 --- /dev/null +++ b/vendor/cakephp/database/Log/LoggedQuery.php @@ -0,0 +1,175 @@ +driver instanceof Sqlserver) { + return $p ? '1' : '0'; + } + + return $p ? 'TRUE' : 'FALSE'; + } + + if (is_string($p)) { + // Likely binary data like a blob or binary uuid. + // pattern matches ascii control chars. + if (preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $p) !== $p) { + $p = bin2hex($p); + } + + $replacements = [ + '$' => '\\$', + '\\' => '\\\\\\\\', + "'" => "''", + ]; + + $p = strtr($p, $replacements); + + return "'$p'"; + } + + return $p; + }, $this->params); + + $keys = []; + $limit = is_int(key($params)) ? 1 : -1; + foreach ($params as $key => $param) { + $keys[] = is_string($key) ? "/:$key\b/" : '/[?]/'; + } + + return preg_replace($keys, $params, $this->query, $limit); + } + + /** + * Get the logging context data for a query. + * + * @return array + */ + public function getContext(): array + { + return [ + 'numRows' => $this->numRows, + 'took' => $this->took, + ]; + } + + /** + * Returns data that will be serialized as JSON + * + * @return array + */ + public function jsonSerialize(): array + { + $error = $this->error; + if ($error !== null) { + $error = [ + 'class' => get_class($error), + 'message' => $error->getMessage(), + 'code' => $error->getCode(), + ]; + } + + return [ + 'query' => $this->query, + 'numRows' => $this->numRows, + 'params' => $this->params, + 'took' => $this->took, + 'error' => $error, + ]; + } + + /** + * Returns the string representation of this logged query + * + * @return string + */ + public function __toString(): string + { + $sql = $this->query; + if (!empty($this->params)) { + $sql = $this->interpolate(); + } + + return $sql; + } +} diff --git a/vendor/cakephp/database/Log/LoggingStatement.php b/vendor/cakephp/database/Log/LoggingStatement.php new file mode 100644 index 0000000..22814e3 --- /dev/null +++ b/vendor/cakephp/database/Log/LoggingStatement.php @@ -0,0 +1,195 @@ + + */ + protected $_compiledParams = []; + + /** + * Query execution start time. + * + * @var float + */ + protected $startTime = 0.0; + + /** + * Logged query + * + * @var \Cake\Database\Log\LoggedQuery|null + */ + protected $loggedQuery; + + /** + * Wrapper for the execute function to calculate time spent + * and log the query afterwards. + * + * @param array|null $params List of values to be bound to query + * @return bool True on success, false otherwise + * @throws \Exception Re-throws any exception raised during query execution. + */ + public function execute(?array $params = null): bool + { + $this->startTime = microtime(true); + + $this->loggedQuery = new LoggedQuery(); + $this->loggedQuery->driver = $this->_driver; + $this->loggedQuery->params = $params ?: $this->_compiledParams; + + try { + $result = parent::execute($params); + $this->loggedQuery->took = (int)round((microtime(true) - $this->startTime) * 1000, 0); + } catch (Exception $e) { + /** @psalm-suppress UndefinedPropertyAssignment */ + $e->queryString = $this->queryString; + $this->loggedQuery->error = $e; + $this->_log(); + throw $e; + } + + if (preg_match('/^(?!SELECT)/i', $this->queryString)) { + $this->rowCount(); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function fetch($type = self::FETCH_TYPE_NUM) + { + $record = parent::fetch($type); + + if ($this->loggedQuery) { + $this->rowCount(); + } + + return $record; + } + + /** + * @inheritDoc + */ + public function fetchAll($type = self::FETCH_TYPE_NUM) + { + $results = parent::fetchAll($type); + + if ($this->loggedQuery) { + $this->rowCount(); + } + + return $results; + } + + /** + * @inheritDoc + */ + public function rowCount(): int + { + $result = parent::rowCount(); + + if ($this->loggedQuery) { + $this->loggedQuery->numRows = $result; + $this->_log(); + } + + return $result; + } + + /** + * Copies the logging data to the passed LoggedQuery and sends it + * to the logging system. + * + * @return void + */ + protected function _log(): void + { + if ($this->loggedQuery === null) { + return; + } + + $this->loggedQuery->query = $this->queryString; + $this->getLogger()->debug((string)$this->loggedQuery, ['query' => $this->loggedQuery]); + + $this->loggedQuery = null; + } + + /** + * Wrapper for bindValue function to gather each parameter to be later used + * in the logger function. + * + * @param string|int $column Name or param position to be bound + * @param mixed $value The value to bind to variable in query + * @param string|int|null $type PDO type or name of configured Type class + * @return void + */ + public function bindValue($column, $value, $type = 'string'): void + { + parent::bindValue($column, $value, $type); + + if ($type === null) { + $type = 'string'; + } + if (!ctype_digit($type)) { + $value = $this->cast($value, $type)[0]; + } + $this->_compiledParams[$column] = $value; + } + + /** + * Sets a logger + * + * @param \Psr\Log\LoggerInterface $logger Logger object + * @return void + */ + public function setLogger(LoggerInterface $logger): void + { + $this->_logger = $logger; + } + + /** + * Gets the logger object + * + * @return \Psr\Log\LoggerInterface logger instance + */ + public function getLogger(): LoggerInterface + { + return $this->_logger; + } +} diff --git a/vendor/cakephp/database/Log/QueryLogger.php b/vendor/cakephp/database/Log/QueryLogger.php new file mode 100644 index 0000000..b6957d1 --- /dev/null +++ b/vendor/cakephp/database/Log/QueryLogger.php @@ -0,0 +1,57 @@ + $config Configuration array + */ + public function __construct(array $config = []) + { + $this->_defaultConfig['scopes'] = ['queriesLog']; + $this->_defaultConfig['connection'] = ''; + + parent::__construct($config); + } + + /** + * @inheritDoc + */ + public function log($level, $message, array $context = []) + { + $context['scope'] = $this->scopes() ?: ['queriesLog']; + $context['connection'] = $this->getConfig('connection'); + + if ($context['query'] instanceof LoggedQuery) { + $context = $context['query']->getContext() + $context; + $message = 'connection={connection} duration={took} rows={numRows} ' . $message; + } + Log::write('debug', $message, $context); + } +} diff --git a/vendor/cakephp/database/PostgresCompiler.php b/vendor/cakephp/database/PostgresCompiler.php new file mode 100644 index 0000000..7beae8b --- /dev/null +++ b/vendor/cakephp/database/PostgresCompiler.php @@ -0,0 +1,93 @@ + 'DELETE', + 'where' => ' WHERE %s', + 'group' => ' GROUP BY %s', + 'order' => ' %s', + 'limit' => ' LIMIT %s', + 'offset' => ' OFFSET %s', + 'epilog' => ' %s', + ]; + + /** + * Helper function used to build the string representation of a HAVING clause, + * it constructs the field list taking care of aliasing and + * converting expression objects to string. + * + * @param array $parts list of fields to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildHavingPart($parts, $query, $binder) + { + $selectParts = $query->clause('select'); + + foreach ($selectParts as $selectKey => $selectPart) { + if (!$selectPart instanceof FunctionExpression) { + continue; + } + foreach ($parts as $k => $p) { + if (!is_string($p)) { + continue; + } + preg_match_all( + '/\b' . trim($selectKey, '"') . '\b/i', + $p, + $matches + ); + + if (empty($matches[0])) { + continue; + } + + $parts[$k] = preg_replace( + ['/"/', '/\b' . trim($selectKey, '"') . '\b/i'], + ['', $selectPart->sql($binder)], + $p + ); + } + } + + return sprintf(' HAVING %s', implode(', ', $parts)); + } +} diff --git a/vendor/cakephp/database/Query.php b/vendor/cakephp/database/Query.php new file mode 100644 index 0000000..7cc59b5 --- /dev/null +++ b/vendor/cakephp/database/Query.php @@ -0,0 +1,2430 @@ + + */ + protected $_parts = [ + 'delete' => true, + 'update' => [], + 'set' => [], + 'insert' => [], + 'values' => [], + 'with' => [], + 'select' => [], + 'distinct' => false, + 'modifier' => [], + 'from' => [], + 'join' => [], + 'where' => null, + 'group' => [], + 'having' => null, + 'window' => [], + 'order' => null, + 'limit' => null, + 'offset' => null, + 'union' => [], + 'epilog' => null, + ]; + + /** + * The list of query clauses to traverse for generating a SELECT statement + * + * @var array + */ + protected $_selectParts = [ + 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit', + 'offset', 'union', 'epilog', + ]; + + /** + * The list of query clauses to traverse for generating an UPDATE statement + * + * @var array + */ + protected $_updateParts = ['with', 'update', 'set', 'where', 'epilog']; + + /** + * The list of query clauses to traverse for generating a DELETE statement + * + * @var array + */ + protected $_deleteParts = ['with', 'delete', 'modifier', 'from', 'where', 'epilog']; + + /** + * The list of query clauses to traverse for generating an INSERT statement + * + * @var array + */ + protected $_insertParts = ['with', 'insert', 'values', 'epilog']; + + /** + * Indicates whether internal state of this query was changed, this is used to + * discard internal cached objects such as the transformed query or the reference + * to the executed statement. + * + * @var bool + */ + protected $_dirty = false; + + /** + * A list of callback functions to be called to alter each row from resulting + * statement upon retrieval. Each one of the callback function will receive + * the row array as first argument. + * + * @var array + */ + protected $_resultDecorators = []; + + /** + * Statement object resulting from executing this query. + * + * @var \Cake\Database\StatementInterface|null + */ + protected $_iterator; + + /** + * The object responsible for generating query placeholders and temporarily store values + * associated to each of those. + * + * @var \Cake\Database\ValueBinder|null + */ + protected $_valueBinder; + + /** + * Instance of functions builder object used for generating arbitrary SQL functions. + * + * @var \Cake\Database\FunctionsBuilder|null + */ + protected $_functionsBuilder; + + /** + * Boolean for tracking whether buffered results + * are enabled. + * + * @var bool + */ + protected $_useBufferedResults = true; + + /** + * The Type map for fields in the select clause + * + * @var \Cake\Database\TypeMap|null + */ + protected $_selectTypeMap; + + /** + * Tracking flag to disable casting + * + * @var bool + */ + protected $typeCastEnabled = true; + + /** + * Constructor. + * + * @param \Cake\Database\Connection $connection The connection + * object to be used for transforming and executing this query + */ + public function __construct(Connection $connection) + { + $this->setConnection($connection); + } + + /** + * Sets the connection instance to be used for executing and transforming this query. + * + * @param \Cake\Database\Connection $connection Connection instance + * @return $this + */ + public function setConnection(Connection $connection) + { + $this->_dirty(); + $this->_connection = $connection; + + return $this; + } + + /** + * Gets the connection instance to be used for executing and transforming this query. + * + * @return \Cake\Database\Connection + */ + public function getConnection(): Connection + { + return $this->_connection; + } + + /** + * Compiles the SQL representation of this query and executes it using the + * configured connection object. Returns the resulting statement object. + * + * Executing a query internally executes several steps, the first one is + * letting the connection transform this object to fit its particular dialect, + * this might result in generating a different Query object that will be the one + * to actually be executed. Immediately after, literal values are passed to the + * connection so they are bound to the query in a safe way. Finally, the resulting + * statement is decorated with custom objects to execute callbacks for each row + * retrieved if necessary. + * + * Resulting statement is traversable, so it can be used in any loop as you would + * with an array. + * + * This method can be overridden in query subclasses to decorate behavior + * around query execution. + * + * @return \Cake\Database\StatementInterface + */ + public function execute(): StatementInterface + { + $statement = $this->_connection->run($this); + $this->_iterator = $this->_decorateStatement($statement); + $this->_dirty = false; + + return $this->_iterator; + } + + /** + * Executes the SQL of this query and immediately closes the statement before returning the row count of records + * changed. + * + * This method can be used with UPDATE and DELETE queries, but is not recommended for SELECT queries and is not + * used to count records. + * + * ## Example + * + * ``` + * $rowCount = $query->update('articles') + * ->set(['published'=>true]) + * ->where(['published'=>false]) + * ->rowCountAndClose(); + * ``` + * + * The above example will change the published column to true for all false records, and return the number of + * records that were updated. + * + * @return int + */ + public function rowCountAndClose(): int + { + $statement = $this->execute(); + try { + return $statement->rowCount(); + } finally { + $statement->closeCursor(); + } + } + + /** + * Returns the SQL representation of this object. + * + * This function will compile this query to make it compatible + * with the SQL dialect that is used by the connection, This process might + * add, remove or alter any query part or internal expression to make it + * executable in the target platform. + * + * The resulting query may have placeholders that will be replaced with the actual + * values when the query is executed, hence it is most suitable to use with + * prepared statements. + * + * @param \Cake\Database\ValueBinder|null $binder Value binder that generates parameter placeholders + * @return string + */ + public function sql(?ValueBinder $binder = null): string + { + if (!$binder) { + $binder = $this->getValueBinder(); + $binder->resetCount(); + } + + return $this->getConnection()->compileQuery($this, $binder); + } + + /** + * Will iterate over every specified part. Traversing functions can aggregate + * results using variables in the closure or instance variables. This function + * is commonly used as a way for traversing all query parts that + * are going to be used for constructing a query. + * + * The callback will receive 2 parameters, the first one is the value of the query + * part that is being iterated and the second the name of such part. + * + * ### Example + * ``` + * $query->select(['title'])->from('articles')->traverse(function ($value, $clause) { + * if ($clause === 'select') { + * var_dump($value); + * } + * }); + * ``` + * + * @param callable $callback A function or callable to be executed for each part + * @return $this + */ + public function traverse($callback) + { + foreach ($this->_parts as $name => $part) { + $callback($part, $name); + } + + return $this; + } + + /** + * Will iterate over the provided parts. + * + * Traversing functions can aggregate results using variables in the closure + * or instance variables. This method can be used to traverse a subset of + * query parts in order to render a SQL query. + * + * The callback will receive 2 parameters, the first one is the value of the query + * part that is being iterated and the second the name of such part. + * + * ### Example + * + * ``` + * $query->select(['title'])->from('articles')->traverse(function ($value, $clause) { + * if ($clause === 'select') { + * var_dump($value); + * } + * }, ['select', 'from']); + * ``` + * + * @param callable $visitor A function or callable to be executed for each part + * @param array $parts The list of query parts to traverse + * @return $this + */ + public function traverseParts(callable $visitor, array $parts) + { + foreach ($parts as $name) { + $visitor($this->_parts[$name], $name); + } + + return $this; + } + + /** + * Adds a new common table expression (CTE) to the query. + * + * ### Examples: + * + * Common table expressions can either be passed as preconstructed expression + * objects: + * + * ``` + * $cte = new \Cake\Database\Expression\CommonTableExpression( + * 'cte', + * $connection + * ->newQuery() + * ->select('*') + * ->from('articles') + * ); + * + * $query->with($cte); + * ``` + * + * or returned from a closure, which will receive a new common table expression + * object as the first argument, and a new blank query object as + * the second argument: + * + * ``` + * $query->with(function ( + * \Cake\Database\Expression\CommonTableExpression $cte, + * \Cake\Database\Query $query + * ) { + * $cteQuery = $query + * ->select('*') + * ->from('articles'); + * + * return $cte + * ->name('cte') + * ->query($cteQuery); + * }); + * ``` + * + * @param \Cake\Database\Expression\CommonTableExpression|\Closure $cte The CTE to add. + * @param bool $overwrite Whether to reset the list of CTEs. + * @return $this + */ + public function with($cte, bool $overwrite = false) + { + if ($overwrite) { + $this->_parts['with'] = []; + } + + if ($cte instanceof Closure) { + $query = $this->getConnection()->newQuery(); + $cte = $cte(new CommonTableExpression(), $query); + if (!($cte instanceof CommonTableExpression)) { + throw new RuntimeException( + 'You must return a `CommonTableExpression` from a Closure passed to `with()`.' + ); + } + } + + $this->_parts['with'][] = $cte; + $this->_dirty(); + + return $this; + } + + /** + * Adds new fields to be returned by a `SELECT` statement when this query is + * executed. Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used to alias fields using the value as the + * real field to be aliased. It is possible to alias strings, Expression objects or + * even other Query objects. + * + * If a callable function is passed, the returning array of the function will + * be used as the list of fields. + * + * By default this function will append any passed argument to the list of fields + * to be selected, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * $query->select(['id', 'title']); // Produces SELECT id, title + * $query->select(['author' => 'author_id']); // Appends author: SELECT id, title, author_id as author + * $query->select('id', true); // Resets the list: SELECT id + * $query->select(['total' => $countQuery]); // SELECT id, (SELECT ...) AS total + * $query->select(function ($query) { + * return ['article_id', 'total' => $query->count('*')]; + * }) + * ``` + * + * By default no fields are selected, if you have an instance of `Cake\ORM\Query` and try to append + * fields you should also call `Cake\ORM\Query::enableAutoFields()` to select the default fields + * from the table. + * + * @param \Cake\Database\ExpressionInterface|callable|array|string $fields fields to be added to the list. + * @param bool $overwrite whether to reset fields with passed list or not + * @return $this + */ + public function select($fields = [], bool $overwrite = false) + { + if (!is_string($fields) && is_callable($fields)) { + $fields = $fields($this); + } + + if (!is_array($fields)) { + $fields = [$fields]; + } + + if ($overwrite) { + $this->_parts['select'] = $fields; + } else { + $this->_parts['select'] = array_merge($this->_parts['select'], $fields); + } + + $this->_dirty(); + $this->_type = 'select'; + + return $this; + } + + /** + * Adds a `DISTINCT` clause to the query to remove duplicates from the result set. + * This clause can only be used for select statements. + * + * If you wish to filter duplicates based of those rows sharing a particular field + * or set of fields, you may pass an array of fields to filter on. Beware that + * this option might not be fully supported in all database systems. + * + * ### Examples: + * + * ``` + * // Filters products with the same name and city + * $query->select(['name', 'city'])->from('products')->distinct(); + * + * // Filters products in the same city + * $query->distinct(['city']); + * $query->distinct('city'); + * + * // Filter products with the same name + * $query->distinct(['name'], true); + * $query->distinct('name', true); + * ``` + * + * @param \Cake\Database\ExpressionInterface|array|string|bool $on Enable/disable distinct class + * or list of fields to be filtered on + * @param bool $overwrite whether to reset fields with passed list or not + * @return $this + */ + public function distinct($on = [], $overwrite = false) + { + if ($on === []) { + $on = true; + } elseif (is_string($on)) { + $on = [$on]; + } + + if (is_array($on)) { + $merge = []; + if (is_array($this->_parts['distinct'])) { + $merge = $this->_parts['distinct']; + } + $on = $overwrite ? array_values($on) : array_merge($merge, array_values($on)); + } + + $this->_parts['distinct'] = $on; + $this->_dirty(); + + return $this; + } + + /** + * Adds a single or multiple `SELECT` modifiers to be used in the `SELECT`. + * + * By default this function will append any passed argument to the list of modifiers + * to be applied, unless the second argument is set to true. + * + * ### Example: + * + * ``` + * // Ignore cache query in MySQL + * $query->select(['name', 'city'])->from('products')->modifier('SQL_NO_CACHE'); + * // It will produce the SQL: SELECT SQL_NO_CACHE name, city FROM products + * + * // Or with multiple modifiers + * $query->select(['name', 'city'])->from('products')->modifier(['HIGH_PRIORITY', 'SQL_NO_CACHE']); + * // It will produce the SQL: SELECT HIGH_PRIORITY SQL_NO_CACHE name, city FROM products + * ``` + * + * @param \Cake\Database\ExpressionInterface|array|string $modifiers modifiers to be applied to the query + * @param bool $overwrite whether to reset order with field list or not + * @return $this + */ + public function modifier($modifiers, $overwrite = false) + { + $this->_dirty(); + if ($overwrite) { + $this->_parts['modifier'] = []; + } + if (!is_array($modifiers)) { + $modifiers = [$modifiers]; + } + $this->_parts['modifier'] = array_merge($this->_parts['modifier'], $modifiers); + + return $this; + } + + /** + * Adds a single or multiple tables to be used in the FROM clause for this query. + * Tables can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used to alias tables using the value as the + * real field to be aliased. It is possible to alias strings, ExpressionInterface objects or + * even other Query objects. + * + * By default this function will append any passed argument to the list of tables + * to be selected from, unless the second argument is set to true. + * + * This method can be used for select, update and delete statements. + * + * ### Examples: + * + * ``` + * $query->from(['p' => 'posts']); // Produces FROM posts p + * $query->from('authors'); // Appends authors: FROM posts p, authors + * $query->from(['products'], true); // Resets the list: FROM products + * $query->from(['sub' => $countQuery]); // FROM (SELECT ...) sub + * ``` + * + * @param array|string $tables tables to be added to the list. This argument, can be + * passed as an array of strings, array of expression objects, or a single string. See + * the examples above for the valid call types. + * @param bool $overwrite whether to reset tables with passed list or not + * @return $this + */ + public function from($tables = [], $overwrite = false) + { + $tables = (array)$tables; + + if ($overwrite) { + $this->_parts['from'] = $tables; + } else { + $this->_parts['from'] = array_merge($this->_parts['from'], $tables); + } + + $this->_dirty(); + + return $this; + } + + /** + * Adds a single or multiple tables to be used as JOIN clauses to this query. + * Tables can be passed as an array of strings, an array describing the + * join parts, an array with multiple join descriptions, or a single string. + * + * By default this function will append any passed argument to the list of tables + * to be joined, unless the third argument is set to true. + * + * When no join type is specified an `INNER JOIN` is used by default: + * `$query->join(['authors'])` will produce `INNER JOIN authors ON 1 = 1` + * + * It is also possible to alias joins using the array key: + * `$query->join(['a' => 'authors'])` will produce `INNER JOIN authors a ON 1 = 1` + * + * A join can be fully described and aliased using the array notation: + * + * ``` + * $query->join([ + * 'a' => [ + * 'table' => 'authors', + * 'type' => 'LEFT', + * 'conditions' => 'a.id = b.author_id' + * ] + * ]); + * // Produces LEFT JOIN authors a ON a.id = b.author_id + * ``` + * + * You can even specify multiple joins in an array, including the full description: + * + * ``` + * $query->join([ + * 'a' => [ + * 'table' => 'authors', + * 'type' => 'LEFT', + * 'conditions' => 'a.id = b.author_id' + * ], + * 'p' => [ + * 'table' => 'publishers', + * 'type' => 'INNER', + * 'conditions' => 'p.id = b.publisher_id AND p.name = "Cake Software Foundation"' + * ] + * ]); + * // LEFT JOIN authors a ON a.id = b.author_id + * // INNER JOIN publishers p ON p.id = b.publisher_id AND p.name = "Cake Software Foundation" + * ``` + * + * ### Using conditions and types + * + * Conditions can be expressed, as in the examples above, using a string for comparing + * columns, or string with already quoted literal values. Additionally it is + * possible to use conditions expressed in arrays or expression objects. + * + * When using arrays for expressing conditions, it is often desirable to convert + * the literal values to the correct database representation. This is achieved + * using the second parameter of this function. + * + * ``` + * $query->join(['a' => [ + * 'table' => 'articles', + * 'conditions' => [ + * 'a.posted >=' => new DateTime('-3 days'), + * 'a.published' => true, + * 'a.author_id = authors.id' + * ] + * ]], ['a.posted' => 'datetime', 'a.published' => 'boolean']) + * ``` + * + * ### Overwriting joins + * + * When creating aliased joins using the array notation, you can override + * previous join definitions by using the same alias in consequent + * calls to this function or you can replace all previously defined joins + * with another list if the third parameter for this function is set to true. + * + * ``` + * $query->join(['alias' => 'table']); // joins table with as alias + * $query->join(['alias' => 'another_table']); // joins another_table with as alias + * $query->join(['something' => 'different_table'], [], true); // resets joins list + * ``` + * + * @param array|string $tables list of tables to be joined in the query + * @param array $types Associative array of type names used to bind values to query + * @param bool $overwrite whether to reset joins with passed list or not + * @see \Cake\Database\TypeFactory + * @return $this + */ + public function join($tables, $types = [], $overwrite = false) + { + if (is_string($tables) || isset($tables['table'])) { + $tables = [$tables]; + } + + $joins = []; + $i = count($this->_parts['join']); + foreach ($tables as $alias => $t) { + if (!is_array($t)) { + $t = ['table' => $t, 'conditions' => $this->newExpr()]; + } + + if (!is_string($t['conditions']) && is_callable($t['conditions'])) { + $t['conditions'] = $t['conditions']($this->newExpr(), $this); + } + + if (!($t['conditions'] instanceof ExpressionInterface)) { + $t['conditions'] = $this->newExpr()->add($t['conditions'], $types); + } + $alias = is_string($alias) ? $alias : null; + $joins[$alias ?: $i++] = $t + ['type' => static::JOIN_TYPE_INNER, 'alias' => $alias]; + } + + if ($overwrite) { + $this->_parts['join'] = $joins; + } else { + $this->_parts['join'] = array_merge($this->_parts['join'], $joins); + } + + $this->_dirty(); + + return $this; + } + + /** + * Remove a join if it has been defined. + * + * Useful when you are redefining joins or want to re-order + * the join clauses. + * + * @param string $name The alias/name of the join to remove. + * @return $this + */ + public function removeJoin(string $name) + { + unset($this->_parts['join'][$name]); + $this->_dirty(); + + return $this; + } + + /** + * Adds a single `LEFT JOIN` clause to the query. + * + * This is a shorthand method for building joins via `join()`. + * + * The table name can be passed as a string, or as an array in case it needs to + * be aliased: + * + * ``` + * // LEFT JOIN authors ON authors.id = posts.author_id + * $query->leftJoin('authors', 'authors.id = posts.author_id'); + * + * // LEFT JOIN authors a ON a.id = posts.author_id + * $query->leftJoin(['a' => 'authors'], 'a.id = posts.author_id'); + * ``` + * + * Conditions can be passed as strings, arrays, or expression objects. When + * using arrays it is possible to combine them with the `$types` parameter + * in order to define how to convert the values: + * + * ``` + * $query->leftJoin(['a' => 'articles'], [ + * 'a.posted >=' => new DateTime('-3 days'), + * 'a.published' => true, + * 'a.author_id = authors.id' + * ], ['a.posted' => 'datetime', 'a.published' => 'boolean']); + * ``` + * + * See `join()` for further details on conditions and types. + * + * @param array|string $table The table to join with + * @param \Cake\Database\ExpressionInterface|array|string $conditions The conditions + * to use for joining. + * @param array $types a list of types associated to the conditions used for converting + * values to the corresponding database representation. + * @return $this + */ + public function leftJoin($table, $conditions = [], $types = []) + { + $this->join($this->_makeJoin($table, $conditions, static::JOIN_TYPE_LEFT), $types); + + return $this; + } + + /** + * Adds a single `RIGHT JOIN` clause to the query. + * + * This is a shorthand method for building joins via `join()`. + * + * The arguments of this method are identical to the `leftJoin()` shorthand, please refer + * to that methods description for further details. + * + * @param array|string $table The table to join with + * @param \Cake\Database\ExpressionInterface|array|string $conditions The conditions + * to use for joining. + * @param array $types a list of types associated to the conditions used for converting + * values to the corresponding database representation. + * @return $this + */ + public function rightJoin($table, $conditions = [], $types = []) + { + $this->join($this->_makeJoin($table, $conditions, static::JOIN_TYPE_RIGHT), $types); + + return $this; + } + + /** + * Adds a single `INNER JOIN` clause to the query. + * + * This is a shorthand method for building joins via `join()`. + * + * The arguments of this method are identical to the `leftJoin()` shorthand, please refer + * to that method's description for further details. + * + * @param array|string $table The table to join with + * @param \Cake\Database\ExpressionInterface|array|string $conditions The conditions + * to use for joining. + * @param array $types a list of types associated to the conditions used for converting + * values to the corresponding database representation. + * @return $this + */ + public function innerJoin($table, $conditions = [], $types = []) + { + $this->join($this->_makeJoin($table, $conditions, static::JOIN_TYPE_INNER), $types); + + return $this; + } + + /** + * Returns an array that can be passed to the join method describing a single join clause + * + * @param array|string $table The table to join with + * @param \Cake\Database\ExpressionInterface|array|string $conditions The conditions + * to use for joining. + * @param string $type the join type to use + * @return array + * @psalm-suppress InvalidReturnType + */ + protected function _makeJoin($table, $conditions, $type): array + { + $alias = $table; + + if (is_array($table)) { + $alias = key($table); + $table = current($table); + } + + /** + * @psalm-suppress InvalidArrayOffset + * @psalm-suppress InvalidReturnStatement + */ + return [ + $alias => [ + 'table' => $table, + 'conditions' => $conditions, + 'type' => $type, + ], + ]; + } + + /** + * Adds a condition or set of conditions to be used in the WHERE clause for this + * query. Conditions can be expressed as an array of fields as keys with + * comparison operators in it, the values for the array will be used for comparing + * the field to such literal. Finally, conditions can be expressed as a single + * string or an array of strings. + * + * When using arrays, each entry will be joined to the rest of the conditions using + * an `AND` operator. Consecutive calls to this function will also join the new + * conditions specified using the AND operator. Additionally, values can be + * expressed using expression objects which can include other query objects. + * + * Any conditions created with this methods can be used with any `SELECT`, `UPDATE` + * and `DELETE` type of queries. + * + * ### Conditions using operators: + * + * ``` + * $query->where([ + * 'posted >=' => new DateTime('3 days ago'), + * 'title LIKE' => 'Hello W%', + * 'author_id' => 1, + * ], ['posted' => 'datetime']); + * ``` + * + * The previous example produces: + * + * `WHERE posted >= 2012-01-27 AND title LIKE 'Hello W%' AND author_id = 1` + * + * Second parameter is used to specify what type is expected for each passed + * key. Valid types can be used from the mapped with Database\Type class. + * + * ### Nesting conditions with conjunctions: + * + * ``` + * $query->where([ + * 'author_id !=' => 1, + * 'OR' => ['published' => true, 'posted <' => new DateTime('now')], + * 'NOT' => ['title' => 'Hello'] + * ], ['published' => boolean, 'posted' => 'datetime'] + * ``` + * + * The previous example produces: + * + * `WHERE author_id = 1 AND (published = 1 OR posted < '2012-02-01') AND NOT (title = 'Hello')` + * + * You can nest conditions using conjunctions as much as you like. Sometimes, you + * may want to define 2 different options for the same key, in that case, you can + * wrap each condition inside a new array: + * + * `$query->where(['OR' => [['published' => false], ['published' => true]])` + * + * Would result in: + * + * `WHERE (published = false) OR (published = true)` + * + * Keep in mind that every time you call where() with the third param set to false + * (default), it will join the passed conditions to the previous stored list using + * the `AND` operator. Also, using the same array key twice in consecutive calls to + * this method will not override the previous value. + * + * ### Using expressions objects: + * + * ``` + * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR'); + * $query->where(['published' => true], ['published' => 'boolean'])->where($exp); + * ``` + * + * The previous example produces: + * + * `WHERE (id != 100 OR author_id != 1) AND published = 1` + * + * Other Query objects that be used as conditions for any field. + * + * ### Adding conditions in multiple steps: + * + * You can use callable functions to construct complex expressions, functions + * receive as first argument a new QueryExpression object and this query instance + * as second argument. Functions must return an expression object, that will be + * added the list of conditions for the query using the `AND` operator. + * + * ``` + * $query + * ->where(['title !=' => 'Hello World']) + * ->where(function ($exp, $query) { + * $or = $exp->or(['id' => 1]); + * $and = $exp->and(['id >' => 2, 'id <' => 10]); + * return $or->add($and); + * }); + * ``` + * + * * The previous example produces: + * + * `WHERE title != 'Hello World' AND (id = 1 OR (id > 2 AND id < 10))` + * + * ### Conditions as strings: + * + * ``` + * $query->where(['articles.author_id = authors.id', 'modified IS NULL']); + * ``` + * + * The previous example produces: + * + * `WHERE articles.author_id = authors.id AND modified IS NULL` + * + * Please note that when using the array notation or the expression objects, all + * *values* will be correctly quoted and transformed to the correspondent database + * data type automatically for you, thus securing your application from SQL injections. + * The keys however, are not treated as unsafe input, and should be validated/sanitized. + * + * If you use string conditions make sure that your values are correctly quoted. + * The safest thing you can do is to never use string conditions. + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions The conditions to filter on. + * @param array $types Associative array of type names used to bind values to query + * @param bool $overwrite whether to reset conditions with passed list or not + * @see \Cake\Database\TypeFactory + * @see \Cake\Database\Expression\QueryExpression + * @return $this + */ + public function where($conditions = null, array $types = [], bool $overwrite = false) + { + if ($overwrite) { + $this->_parts['where'] = $this->newExpr(); + } + $this->_conjugate('where', $conditions, 'AND', $types); + + return $this; + } + + /** + * Convenience method that adds a NOT NULL condition to the query + * + * @param \Cake\Database\ExpressionInterface|array|string $fields A single field or expressions or a list of them + * that should be not null. + * @return $this + */ + public function whereNotNull($fields) + { + if (!is_array($fields)) { + $fields = [$fields]; + } + + $exp = $this->newExpr(); + + foreach ($fields as $field) { + $exp->isNotNull($field); + } + + return $this->where($exp); + } + + /** + * Convenience method that adds a IS NULL condition to the query + * + * @param \Cake\Database\ExpressionInterface|array|string $fields A single field or expressions or a list of them + * that should be null. + * @return $this + */ + public function whereNull($fields) + { + if (!is_array($fields)) { + $fields = [$fields]; + } + + $exp = $this->newExpr(); + + foreach ($fields as $field) { + $exp->isNull($field); + } + + return $this->where($exp); + } + + /** + * Adds an IN condition or set of conditions to be used in the WHERE clause for this + * query. + * + * This method does allow empty inputs in contrast to where() if you set + * 'allowEmpty' to true. + * Be careful about using it without proper sanity checks. + * + * Options: + * + * - `types` - Associative array of type names used to bind values to query + * - `allowEmpty` - Allow empty array. + * + * @param string $field Field + * @param array $values Array of values + * @param array $options Options + * @return $this + */ + public function whereInList(string $field, array $values, array $options = []) + { + $options += [ + 'types' => [], + 'allowEmpty' => false, + ]; + + if ($options['allowEmpty'] && !$values) { + return $this->where('1=0'); + } + + return $this->where([$field . ' IN' => $values], $options['types']); + } + + /** + * Adds a NOT IN condition or set of conditions to be used in the WHERE clause for this + * query. + * + * This method does allow empty inputs in contrast to where() if you set + * 'allowEmpty' to true. + * Be careful about using it without proper sanity checks. + * + * @param string $field Field + * @param array $values Array of values + * @param array $options Options + * @return $this + */ + public function whereNotInList(string $field, array $values, array $options = []) + { + $options += [ + 'types' => [], + 'allowEmpty' => false, + ]; + + if ($options['allowEmpty'] && !$values) { + return $this->where([$field . ' IS NOT' => null]); + } + + return $this->where([$field . ' NOT IN' => $values], $options['types']); + } + + /** + * Adds a NOT IN condition or set of conditions to be used in the WHERE clause for this + * query. This also allows the field to be null with a IS NULL condition since the null + * value would cause the NOT IN condition to always fail. + * + * This method does allow empty inputs in contrast to where() if you set + * 'allowEmpty' to true. + * Be careful about using it without proper sanity checks. + * + * @param string $field Field + * @param array $values Array of values + * @param array $options Options + * @return $this + */ + public function whereNotInListOrNull(string $field, array $values, array $options = []) + { + $options += [ + 'types' => [], + 'allowEmpty' => false, + ]; + + if ($options['allowEmpty'] && !$values) { + return $this->where([$field . ' IS NOT' => null]); + } + + return $this->where( + [ + 'OR' => [$field . ' NOT IN' => $values, $field . ' IS' => null], + ], + $options['types'] + ); + } + + /** + * Connects any previously defined set of conditions to the provided list + * using the AND operator. This function accepts the conditions list in the same + * format as the method `where` does, hence you can use arrays, expression objects + * callback functions or strings. + * + * It is important to notice that when calling this function, any previous set + * of conditions defined for this query will be treated as a single argument for + * the AND operator. This function will not only operate the most recently defined + * condition, but all the conditions as a whole. + * + * When using an array for defining conditions, creating constraints form each + * array entry will use the same logic as with the `where()` function. This means + * that each array entry will be joined to the other using the AND operator, unless + * you nest the conditions in the array using other operator. + * + * ### Examples: + * + * ``` + * $query->where(['title' => 'Hello World')->andWhere(['author_id' => 1]); + * ``` + * + * Will produce: + * + * `WHERE title = 'Hello World' AND author_id = 1` + * + * ``` + * $query + * ->where(['OR' => ['published' => false, 'published is NULL']]) + * ->andWhere(['author_id' => 1, 'comments_count >' => 10]) + * ``` + * + * Produces: + * + * `WHERE (published = 0 OR published IS NULL) AND author_id = 1 AND comments_count > 10` + * + * ``` + * $query + * ->where(['title' => 'Foo']) + * ->andWhere(function ($exp, $query) { + * return $exp + * ->or(['author_id' => 1]) + * ->add(['author_id' => 2]); + * }); + * ``` + * + * Generates the following conditions: + * + * `WHERE (title = 'Foo') AND (author_id = 1 OR author_id = 2)` + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The conditions to add with AND. + * @param array $types Associative array of type names used to bind values to query + * @see \Cake\Database\Query::where() + * @see \Cake\Database\TypeFactory + * @return $this + */ + public function andWhere($conditions, array $types = []) + { + $this->_conjugate('where', $conditions, 'AND', $types); + + return $this; + } + + /** + * Adds a single or multiple fields to be used in the ORDER clause for this query. + * Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used as the field itself and the value will + * represent the order in which such field should be ordered. When called multiple + * times with the same fields as key, the last order definition will prevail over + * the others. + * + * By default this function will append any passed argument to the list of fields + * to be selected, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * $query->order(['title' => 'DESC', 'author_id' => 'ASC']); + * ``` + * + * Produces: + * + * `ORDER BY title DESC, author_id ASC` + * + * ``` + * $query + * ->order(['title' => $query->newExpr('DESC NULLS FIRST')]) + * ->order('author_id'); + * ``` + * + * Will generate: + * + * `ORDER BY title DESC NULLS FIRST, author_id` + * + * ``` + * $expression = $query->newExpr()->add(['id % 2 = 0']); + * $query->order($expression)->order(['title' => 'ASC']); + * ``` + * + * and + * + * ``` + * $query->order(function ($exp, $query) { + * return [$exp->add(['id % 2 = 0']), 'title' => 'ASC']; + * }); + * ``` + * + * Will both become: + * + * `ORDER BY (id %2 = 0), title ASC` + * + * Order fields/directions are not sanitized by the query builder. + * You should use an allowed list of fields/directions when passing + * in user-supplied data to `order()`. + * + * If you need to set complex expressions as order conditions, you + * should use `orderAsc()` or `orderDesc()`. + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $fields fields to be added to the list + * @param bool $overwrite whether to reset order with field list or not + * @return $this + */ + public function order($fields, $overwrite = false) + { + if ($overwrite) { + $this->_parts['order'] = null; + } + + if (!$fields) { + return $this; + } + + if (!$this->_parts['order']) { + $this->_parts['order'] = new OrderByExpression(); + } + $this->_conjugate('order', $fields, '', []); + + return $this; + } + + /** + * Add an ORDER BY clause with an ASC direction. + * + * This method allows you to set complex expressions + * as order conditions unlike order() + * + * Order fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param \Cake\Database\ExpressionInterface|\Closure|string $field The field to order on. + * @param bool $overwrite Whether to reset the order clauses. + * @return $this + */ + public function orderAsc($field, $overwrite = false) + { + if ($overwrite) { + $this->_parts['order'] = null; + } + if (!$field) { + return $this; + } + + if ($field instanceof Closure) { + $field = $field($this->newExpr(), $this); + } + + if (!$this->_parts['order']) { + $this->_parts['order'] = new OrderByExpression(); + } + $this->_parts['order']->add(new OrderClauseExpression($field, 'ASC')); + + return $this; + } + + /** + * Add an ORDER BY clause with a DESC direction. + * + * This method allows you to set complex expressions + * as order conditions unlike order() + * + * Order fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param \Cake\Database\ExpressionInterface|\Closure|string $field The field to order on. + * @param bool $overwrite Whether to reset the order clauses. + * @return $this + */ + public function orderDesc($field, $overwrite = false) + { + if ($overwrite) { + $this->_parts['order'] = null; + } + if (!$field) { + return $this; + } + + if ($field instanceof Closure) { + $field = $field($this->newExpr(), $this); + } + + if (!$this->_parts['order']) { + $this->_parts['order'] = new OrderByExpression(); + } + $this->_parts['order']->add(new OrderClauseExpression($field, 'DESC')); + + return $this; + } + + /** + * Adds a single or multiple fields to be used in the GROUP BY clause for this query. + * Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * By default this function will append any passed argument to the list of fields + * to be grouped, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * // Produces GROUP BY id, title + * $query->group(['id', 'title']); + * + * // Produces GROUP BY title + * $query->group('title'); + * ``` + * + * Group fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param \Cake\Database\ExpressionInterface|array|string $fields fields to be added to the list + * @param bool $overwrite whether to reset fields with passed list or not + * @return $this + */ + public function group($fields, $overwrite = false) + { + if ($overwrite) { + $this->_parts['group'] = []; + } + + if (!is_array($fields)) { + $fields = [$fields]; + } + + $this->_parts['group'] = array_merge($this->_parts['group'], array_values($fields)); + $this->_dirty(); + + return $this; + } + + /** + * Adds a condition or set of conditions to be used in the `HAVING` clause for this + * query. This method operates in exactly the same way as the method `where()` + * does. Please refer to its documentation for an insight on how to using each + * parameter. + * + * Having fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions The having conditions. + * @param array $types Associative array of type names used to bind values to query + * @param bool $overwrite whether to reset conditions with passed list or not + * @see \Cake\Database\Query::where() + * @return $this + */ + public function having($conditions = null, $types = [], $overwrite = false) + { + if ($overwrite) { + $this->_parts['having'] = $this->newExpr(); + } + $this->_conjugate('having', $conditions, 'AND', $types); + + return $this; + } + + /** + * Connects any previously defined set of conditions to the provided list + * using the AND operator in the HAVING clause. This method operates in exactly + * the same way as the method `andWhere()` does. Please refer to its + * documentation for an insight on how to using each parameter. + * + * Having fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The AND conditions for HAVING. + * @param array $types Associative array of type names used to bind values to query + * @see \Cake\Database\Query::andWhere() + * @return $this + */ + public function andHaving($conditions, $types = []) + { + $this->_conjugate('having', $conditions, 'AND', $types); + + return $this; + } + + /** + * Adds a named window expression. + * + * You are responsible for adding windows in the order your database requires. + * + * @param string $name Window name + * @param \Cake\Database\Expression\WindowExpression|\Closure $window Window expression + * @param bool $overwrite Clear all previous query window expressions + * @return $this + */ + public function window(string $name, $window, bool $overwrite = false) + { + if ($overwrite) { + $this->_parts['window'] = []; + } + + if ($window instanceof Closure) { + $window = $window(new WindowExpression(), $this); + if (!($window instanceof WindowExpression)) { + throw new RuntimeException('You must return a `WindowExpression` from a Closure passed to `window()`.'); + } + } + + $this->_parts['window'][] = ['name' => new IdentifierExpression($name), 'window' => $window]; + $this->_dirty(); + + return $this; + } + + /** + * Set the page of results you want. + * + * This method provides an easier to use interface to set the limit + offset + * in the record set you want as results. If empty the limit will default to + * the existing limit clause, and if that too is empty, then `25` will be used. + * + * Pages must start at 1. + * + * @param int $num The page number you want. + * @param int|null $limit The number of rows you want in the page. If null + * the current limit clause will be used. + * @return $this + * @throws \InvalidArgumentException If page number < 1. + */ + public function page(int $num, ?int $limit = null) + { + if ($num < 1) { + throw new InvalidArgumentException('Pages must start at 1.'); + } + if ($limit !== null) { + $this->limit($limit); + } + $limit = $this->clause('limit'); + if ($limit === null) { + $limit = 25; + $this->limit($limit); + } + $offset = ($num - 1) * $limit; + if (PHP_INT_MAX <= $offset) { + $offset = PHP_INT_MAX; + } + $this->offset((int)$offset); + + return $this; + } + + /** + * Sets the number of records that should be retrieved from database, + * accepts an integer or an expression object that evaluates to an integer. + * In some databases, this operation might not be supported or will require + * the query to be transformed in order to limit the result set size. + * + * ### Examples + * + * ``` + * $query->limit(10) // generates LIMIT 10 + * $query->limit($query->newExpr()->add(['1 + 1'])); // LIMIT (1 + 1) + * ``` + * + * @param \Cake\Database\ExpressionInterface|int|null $limit number of records to be returned + * @return $this + */ + public function limit($limit) + { + $this->_dirty(); + $this->_parts['limit'] = $limit; + + return $this; + } + + /** + * Sets the number of records that should be skipped from the original result set + * This is commonly used for paginating large results. Accepts an integer or an + * expression object that evaluates to an integer. + * + * In some databases, this operation might not be supported or will require + * the query to be transformed in order to limit the result set size. + * + * ### Examples + * + * ``` + * $query->offset(10) // generates OFFSET 10 + * $query->offset($query->newExpr()->add(['1 + 1'])); // OFFSET (1 + 1) + * ``` + * + * @param \Cake\Database\ExpressionInterface|int|null $offset number of records to be skipped + * @return $this + */ + public function offset($offset) + { + $this->_dirty(); + $this->_parts['offset'] = $offset; + + return $this; + } + + /** + * Adds a complete query to be used in conjunction with an UNION operator with + * this query. This is used to combine the result set of this query with the one + * that will be returned by the passed query. You can add as many queries as you + * required by calling multiple times this method with different queries. + * + * By default, the UNION operator will remove duplicate rows, if you wish to include + * every row for all queries, use unionAll(). + * + * ### Examples + * + * ``` + * $union = (new Query($conn))->select(['id', 'title'])->from(['a' => 'articles']); + * $query->select(['id', 'name'])->from(['d' => 'things'])->union($union); + * ``` + * + * Will produce: + * + * `SELECT id, name FROM things d UNION SELECT id, title FROM articles a` + * + * @param \Cake\Database\Query|string $query full SQL query to be used in UNION operator + * @param bool $overwrite whether to reset the list of queries to be operated or not + * @return $this + */ + public function union($query, $overwrite = false) + { + if ($overwrite) { + $this->_parts['union'] = []; + } + $this->_parts['union'][] = [ + 'all' => false, + 'query' => $query, + ]; + $this->_dirty(); + + return $this; + } + + /** + * Adds a complete query to be used in conjunction with the UNION ALL operator with + * this query. This is used to combine the result set of this query with the one + * that will be returned by the passed query. You can add as many queries as you + * required by calling multiple times this method with different queries. + * + * Unlike UNION, UNION ALL will not remove duplicate rows. + * + * ``` + * $union = (new Query($conn))->select(['id', 'title'])->from(['a' => 'articles']); + * $query->select(['id', 'name'])->from(['d' => 'things'])->unionAll($union); + * ``` + * + * Will produce: + * + * `SELECT id, name FROM things d UNION ALL SELECT id, title FROM articles a` + * + * @param \Cake\Database\Query|string $query full SQL query to be used in UNION operator + * @param bool $overwrite whether to reset the list of queries to be operated or not + * @return $this + */ + public function unionAll($query, $overwrite = false) + { + if ($overwrite) { + $this->_parts['union'] = []; + } + $this->_parts['union'][] = [ + 'all' => true, + 'query' => $query, + ]; + $this->_dirty(); + + return $this; + } + + /** + * Create an insert query. + * + * Note calling this method will reset any data previously set + * with Query::values(). + * + * @param array $columns The columns to insert into. + * @param array $types A map between columns & their datatypes. + * @return $this + * @throws \RuntimeException When there are 0 columns. + */ + public function insert(array $columns, array $types = []) + { + if (empty($columns)) { + throw new RuntimeException('At least 1 column is required to perform an insert.'); + } + $this->_dirty(); + $this->_type = 'insert'; + $this->_parts['insert'][1] = $columns; + if (!$this->_parts['values']) { + $this->_parts['values'] = new ValuesExpression($columns, $this->getTypeMap()->setTypes($types)); + } else { + $this->_parts['values']->setColumns($columns); + } + + return $this; + } + + /** + * Set the table name for insert queries. + * + * @param string $table The table name to insert into. + * @return $this + */ + public function into(string $table) + { + $this->_dirty(); + $this->_type = 'insert'; + $this->_parts['insert'][0] = $table; + + return $this; + } + + /** + * Creates an expression that refers to an identifier. Identifiers are used to refer to field names and allow + * the SQL compiler to apply quotes or escape the identifier. + * + * The value is used as is, and you might be required to use aliases or include the table reference in + * the identifier. Do not use this method to inject SQL methods or logical statements. + * + * ### Example + * + * ``` + * $query->newExpr()->lte('count', $query->identifier('total')); + * ``` + * + * @param string $identifier The identifier for an expression + * @return \Cake\Database\ExpressionInterface + */ + public function identifier(string $identifier): ExpressionInterface + { + return new IdentifierExpression($identifier); + } + + /** + * Set the values for an insert query. + * + * Multi inserts can be performed by calling values() more than one time, + * or by providing an array of value sets. Additionally $data can be a Query + * instance to insert data from another SELECT statement. + * + * @param \Cake\Database\Expression\ValuesExpression|\Cake\Database\Query|array $data The data to insert. + * @return $this + * @throws \Cake\Database\Exception\DatabaseException if you try to set values before declaring columns. + * Or if you try to set values on non-insert queries. + */ + public function values($data) + { + if ($this->_type !== 'insert') { + throw new DatabaseException( + 'You cannot add values before defining columns to use.' + ); + } + if (empty($this->_parts['insert'])) { + throw new DatabaseException( + 'You cannot add values before defining columns to use.' + ); + } + + $this->_dirty(); + if ($data instanceof ValuesExpression) { + $this->_parts['values'] = $data; + + return $this; + } + + $this->_parts['values']->add($data); + + return $this; + } + + /** + * Create an update query. + * + * Can be combined with set() and where() methods to create update queries. + * + * @param \Cake\Database\ExpressionInterface|string $table The table you want to update. + * @return $this + */ + public function update($table) + { + if (!is_string($table) && !($table instanceof ExpressionInterface)) { + $text = 'Table must be of type string or "%s", got "%s"'; + $message = sprintf($text, ExpressionInterface::class, gettype($table)); + throw new InvalidArgumentException($message); + } + + $this->_dirty(); + $this->_type = 'update'; + $this->_parts['update'][0] = $table; + + return $this; + } + + /** + * Set one or many fields to update. + * + * ### Examples + * + * Passing a string: + * + * ``` + * $query->update('articles')->set('title', 'The Title'); + * ``` + * + * Passing an array: + * + * ``` + * $query->update('articles')->set(['title' => 'The Title'], ['title' => 'string']); + * ``` + * + * Passing a callable: + * + * ``` + * $query->update('articles')->set(function ($exp) { + * return $exp->eq('title', 'The title', 'string'); + * }); + * ``` + * + * @param \Cake\Database\Expression\QueryExpression|\Closure|array|string $key The column name or array of keys + * + values to set. This can also be a QueryExpression containing a SQL fragment. + * It can also be a Closure, that is required to return an expression object. + * @param mixed $value The value to update $key to. Can be null if $key is an + * array or QueryExpression. When $key is an array, this parameter will be + * used as $types instead. + * @param array|string $types The column types to treat data as. + * @return $this + */ + public function set($key, $value = null, $types = []) + { + if (empty($this->_parts['set'])) { + $this->_parts['set'] = $this->newExpr()->setConjunction(','); + } + + if ($key instanceof Closure) { + $exp = $this->newExpr()->setConjunction(','); + $this->_parts['set']->add($key($exp)); + + return $this; + } + + if (is_array($key) || $key instanceof ExpressionInterface) { + $types = (array)$value; + $this->_parts['set']->add($key, $types); + + return $this; + } + + if (!is_string($types)) { + $types = null; + } + $this->_parts['set']->eq($key, $value, $types); + + return $this; + } + + /** + * Create a delete query. + * + * Can be combined with from(), where() and other methods to + * create delete queries with specific conditions. + * + * @param string|null $table The table to use when deleting. + * @return $this + */ + public function delete(?string $table = null) + { + $this->_dirty(); + $this->_type = 'delete'; + if ($table !== null) { + $this->from($table); + } + + return $this; + } + + /** + * A string or expression that will be appended to the generated query + * + * ### Examples: + * ``` + * $query->select('id')->where(['author_id' => 1])->epilog('FOR UPDATE'); + * $query + * ->insert('articles', ['title']) + * ->values(['author_id' => 1]) + * ->epilog('RETURNING id'); + * ``` + * + * Epliog content is raw SQL and not suitable for use with user supplied data. + * + * @param \Cake\Database\ExpressionInterface|string|null $expression The expression to be appended + * @return $this + */ + public function epilog($expression = null) + { + $this->_dirty(); + $this->_parts['epilog'] = $expression; + + return $this; + } + + /** + * Returns the type of this query (select, insert, update, delete) + * + * @return string + */ + public function type(): string + { + return $this->_type; + } + + /** + * Returns a new QueryExpression object. This is a handy function when + * building complex queries using a fluent interface. You can also override + * this function in subclasses to use a more specialized QueryExpression class + * if required. + * + * You can optionally pass a single raw SQL string or an array or expressions in + * any format accepted by \Cake\Database\Expression\QueryExpression: + * + * ``` + * $expression = $query->newExpr(); // Returns an empty expression object + * $expression = $query->newExpr('Table.column = Table2.column'); // Return a raw SQL expression + * ``` + * + * @param \Cake\Database\ExpressionInterface|array|string|null $rawExpression A string, array or anything you want wrapped in an expression object + * @return \Cake\Database\Expression\QueryExpression + */ + public function newExpr($rawExpression = null): QueryExpression + { + $expression = new QueryExpression([], $this->getTypeMap()); + + if ($rawExpression !== null) { + $expression->add($rawExpression); + } + + return $expression; + } + + /** + * Returns an instance of a functions builder object that can be used for + * generating arbitrary SQL functions. + * + * ### Example: + * + * ``` + * $query->func()->count('*'); + * $query->func()->dateDiff(['2012-01-05', '2012-01-02']) + * ``` + * + * @return \Cake\Database\FunctionsBuilder + */ + public function func(): FunctionsBuilder + { + if ($this->_functionsBuilder === null) { + $this->_functionsBuilder = new FunctionsBuilder(); + } + + return $this->_functionsBuilder; + } + + /** + * Executes this query and returns a results iterator. This function is required + * for implementing the IteratorAggregate interface and allows the query to be + * iterated without having to call execute() manually, thus making it look like + * a result set instead of the query itself. + * + * @return \Cake\Database\StatementInterface + * @psalm-suppress ImplementedReturnTypeMismatch + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + if ($this->_iterator === null || $this->_dirty) { + $this->_iterator = $this->execute(); + } + + return $this->_iterator; + } + + /** + * Returns any data that was stored in the specified clause. This is useful for + * modifying any internal part of the query and it is used by the SQL dialects + * to transform the query accordingly before it is executed. The valid clauses that + * can be retrieved are: delete, update, set, insert, values, select, distinct, + * from, join, set, where, group, having, order, limit, offset and union. + * + * The return value for each of those parts may vary. Some clauses use QueryExpression + * to internally store their state, some use arrays and others may use booleans or + * integers. This is summary of the return types for each clause. + * + * - update: string The name of the table to update + * - set: QueryExpression + * - insert: array, will return an array containing the table + columns. + * - values: ValuesExpression + * - select: array, will return empty array when no fields are set + * - distinct: boolean + * - from: array of tables + * - join: array + * - set: array + * - where: QueryExpression, returns null when not set + * - group: array + * - having: QueryExpression, returns null when not set + * - order: OrderByExpression, returns null when not set + * - limit: integer or QueryExpression, null when not set + * - offset: integer or QueryExpression, null when not set + * - union: array + * + * @param string $name name of the clause to be returned + * @return mixed + * @throws \InvalidArgumentException When the named clause does not exist. + */ + public function clause(string $name) + { + if (!array_key_exists($name, $this->_parts)) { + $clauses = implode(', ', array_keys($this->_parts)); + throw new InvalidArgumentException("The '$name' clause is not defined. Valid clauses are: $clauses"); + } + + return $this->_parts[$name]; + } + + /** + * Registers a callback to be executed for each result that is fetched from the + * result set, the callback function will receive as first parameter an array with + * the raw data from the database for every row that is fetched and must return the + * row with any possible modifications. + * + * Callbacks will be executed lazily, if only 3 rows are fetched for database it will + * called 3 times, event though there might be more rows to be fetched in the cursor. + * + * Callbacks are stacked in the order they are registered, if you wish to reset the stack + * the call this function with the second parameter set to true. + * + * If you wish to remove all decorators from the stack, set the first parameter + * to null and the second to true. + * + * ### Example + * + * ``` + * $query->decorateResults(function ($row) { + * $row['order_total'] = $row['subtotal'] + ($row['subtotal'] * $row['tax']); + * return $row; + * }); + * ``` + * + * @param callable|null $callback The callback to invoke when results are fetched. + * @param bool $overwrite Whether this should append or replace all existing decorators. + * @return $this + */ + public function decorateResults(?callable $callback, bool $overwrite = false) + { + if ($overwrite) { + $this->_resultDecorators = []; + } + + if ($callback !== null) { + $this->_resultDecorators[] = $callback; + } + + return $this; + } + + /** + * This function works similar to the traverse() function, with the difference + * that it does a full depth traversal of the entire expression tree. This will execute + * the provided callback function for each ExpressionInterface object that is + * stored inside this query at any nesting depth in any part of the query. + * + * Callback will receive as first parameter the currently visited expression. + * + * @param callable $callback the function to be executed for each ExpressionInterface + * found inside this query. + * @return $this + */ + public function traverseExpressions(callable $callback) + { + if (!$callback instanceof Closure) { + $callback = Closure::fromCallable($callback); + } + + foreach ($this->_parts as $part) { + $this->_expressionsVisitor($part, $callback); + } + + return $this; + } + + /** + * Query parts traversal method used by traverseExpressions() + * + * @param \Cake\Database\ExpressionInterface|array<\Cake\Database\ExpressionInterface> $expression Query expression or + * array of expressions. + * @param \Closure $callback The callback to be executed for each ExpressionInterface + * found inside this query. + * @return void + */ + protected function _expressionsVisitor($expression, Closure $callback): void + { + if (is_array($expression)) { + foreach ($expression as $e) { + $this->_expressionsVisitor($e, $callback); + } + + return; + } + + if ($expression instanceof ExpressionInterface) { + $expression->traverse(function ($exp) use ($callback) { + $this->_expressionsVisitor($exp, $callback); + }); + + if (!$expression instanceof self) { + $callback($expression); + } + } + } + + /** + * Associates a query placeholder to a value and a type. + * + * ``` + * $query->bind(':id', 1, 'integer'); + * ``` + * + * @param string|int $param placeholder to be replaced with quoted version + * of $value + * @param mixed $value The value to be bound + * @param string|int|null $type the mapped type name, used for casting when sending + * to database + * @return $this + */ + public function bind($param, $value, $type = null) + { + $this->getValueBinder()->bind($param, $value, $type); + + return $this; + } + + /** + * Returns the currently used ValueBinder instance. + * + * A ValueBinder is responsible for generating query placeholders and temporarily + * associate values to those placeholders so that they can be passed correctly + * to the statement object. + * + * @return \Cake\Database\ValueBinder + */ + public function getValueBinder(): ValueBinder + { + if ($this->_valueBinder === null) { + $this->_valueBinder = new ValueBinder(); + } + + return $this->_valueBinder; + } + + /** + * Overwrite the current value binder + * + * A ValueBinder is responsible for generating query placeholders and temporarily + * associate values to those placeholders so that they can be passed correctly + * to the statement object. + * + * @param \Cake\Database\ValueBinder|null $binder The binder or null to disable binding. + * @return $this + */ + public function setValueBinder(?ValueBinder $binder) + { + $this->_valueBinder = $binder; + + return $this; + } + + /** + * Enables/Disables buffered results. + * + * When enabled the results returned by this Query will be + * buffered. This enables you to iterate a result set multiple times, or + * both cache and iterate it. + * + * When disabled it will consume less memory as fetched results are not + * remembered for future iterations. + * + * @param bool $enable Whether to enable buffering + * @return $this + */ + public function enableBufferedResults(bool $enable = true) + { + $this->_dirty(); + $this->_useBufferedResults = $enable; + + return $this; + } + + /** + * Disables buffered results. + * + * Disabling buffering will consume less memory as fetched results are not + * remembered for future iterations. + * + * @return $this + */ + public function disableBufferedResults() + { + $this->_dirty(); + $this->_useBufferedResults = false; + + return $this; + } + + /** + * Returns whether buffered results are enabled/disabled. + * + * When enabled the results returned by this Query will be + * buffered. This enables you to iterate a result set multiple times, or + * both cache and iterate it. + * + * When disabled it will consume less memory as fetched results are not + * remembered for future iterations. + * + * @return bool + */ + public function isBufferedResultsEnabled(): bool + { + return $this->_useBufferedResults; + } + + /** + * Sets the TypeMap class where the types for each of the fields in the + * select clause are stored. + * + * @param \Cake\Database\TypeMap $typeMap The map object to use + * @return $this + */ + public function setSelectTypeMap(TypeMap $typeMap) + { + $this->_selectTypeMap = $typeMap; + $this->_dirty(); + + return $this; + } + + /** + * Gets the TypeMap class where the types for each of the fields in the + * select clause are stored. + * + * @return \Cake\Database\TypeMap + */ + public function getSelectTypeMap(): TypeMap + { + if ($this->_selectTypeMap === null) { + $this->_selectTypeMap = new TypeMap(); + } + + return $this->_selectTypeMap; + } + + /** + * Disables result casting. + * + * When disabled, the fields will be returned as received from the database + * driver (which in most environments means they are being returned as + * strings), which can improve performance with larger datasets. + * + * @return $this + */ + public function disableResultsCasting() + { + $this->typeCastEnabled = false; + + return $this; + } + + /** + * Enables result casting. + * + * When enabled, the fields in the results returned by this Query will be + * cast to their corresponding PHP data type. + * + * @return $this + */ + public function enableResultsCasting() + { + $this->typeCastEnabled = true; + + return $this; + } + + /** + * Returns whether result casting is enabled/disabled. + * + * When enabled, the fields in the results returned by this Query will be + * casted to their corresponding PHP data type. + * + * When disabled, the fields will be returned as received from the database + * driver (which in most environments means they are being returned as + * strings), which can improve performance with larger datasets. + * + * @return bool + */ + public function isResultsCastingEnabled(): bool + { + return $this->typeCastEnabled; + } + + /** + * Auxiliary function used to wrap the original statement from the driver with + * any registered callbacks. + * + * @param \Cake\Database\StatementInterface $statement to be decorated + * @return \Cake\Database\Statement\CallbackStatement|\Cake\Database\StatementInterface + */ + protected function _decorateStatement(StatementInterface $statement) + { + $typeMap = $this->getSelectTypeMap(); + $driver = $this->getConnection()->getDriver(); + + if ($this->typeCastEnabled && $typeMap->toArray()) { + $statement = new CallbackStatement($statement, $driver, new FieldTypeConverter($typeMap, $driver)); + } + + foreach ($this->_resultDecorators as $f) { + $statement = new CallbackStatement($statement, $driver, $f); + } + + return $statement; + } + + /** + * Helper function used to build conditions by composing QueryExpression objects. + * + * @param string $part Name of the query part to append the new part to + * @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $append Expression or builder function to append. + * to append. + * @param string $conjunction type of conjunction to be used to operate part + * @param array $types Associative array of type names used to bind values to query + * @return void + */ + protected function _conjugate(string $part, $append, $conjunction, array $types): void + { + $expression = $this->_parts[$part] ?: $this->newExpr(); + if (empty($append)) { + $this->_parts[$part] = $expression; + + return; + } + + if ($append instanceof Closure) { + $append = $append($this->newExpr(), $this); + } + + if ($expression->getConjunction() === $conjunction) { + $expression->add($append, $types); + } else { + $expression = $this->newExpr() + ->setConjunction($conjunction) + ->add([$expression, $append], $types); + } + + $this->_parts[$part] = $expression; + $this->_dirty(); + } + + /** + * Marks a query as dirty, removing any preprocessed information + * from in memory caching. + * + * @return void + */ + protected function _dirty(): void + { + $this->_dirty = true; + + if ($this->_iterator && $this->_valueBinder) { + $this->getValueBinder()->reset(); + } + } + + /** + * Handles clearing iterator and cloning all expressions and value binders. + * + * @return void + */ + public function __clone() + { + $this->_iterator = null; + if ($this->_valueBinder !== null) { + $this->_valueBinder = clone $this->_valueBinder; + } + if ($this->_selectTypeMap !== null) { + $this->_selectTypeMap = clone $this->_selectTypeMap; + } + foreach ($this->_parts as $name => $part) { + if (empty($part)) { + continue; + } + if (is_array($part)) { + foreach ($part as $i => $piece) { + if (is_array($piece)) { + foreach ($piece as $j => $value) { + if ($value instanceof ExpressionInterface) { + /** @psalm-suppress PossiblyUndefinedMethod */ + $this->_parts[$name][$i][$j] = clone $value; + } + } + } elseif ($piece instanceof ExpressionInterface) { + /** @psalm-suppress PossiblyUndefinedMethod */ + $this->_parts[$name][$i] = clone $piece; + } + } + } + if ($part instanceof ExpressionInterface) { + $this->_parts[$name] = clone $part; + } + } + } + + /** + * Returns string representation of this query (complete SQL statement). + * + * @return string + */ + public function __toString(): string + { + return $this->sql(); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo(): array + { + try { + set_error_handler( + /** @return no-return */ + function ($errno, $errstr) { + throw new RuntimeException($errstr, $errno); + }, + E_ALL + ); + $sql = $this->sql(); + $params = $this->getValueBinder()->bindings(); + } catch (RuntimeException $e) { + $sql = 'SQL could not be generated for this query as it is incomplete.'; + $params = []; + } finally { + restore_error_handler(); + } + + return [ + '(help)' => 'This is a Query object, to get the results execute or iterate it.', + 'sql' => $sql, + 'params' => $params, + 'defaultTypes' => $this->getDefaultTypes(), + 'decorators' => count($this->_resultDecorators), + 'executed' => $this->_iterator ? true : false, + ]; + } +} diff --git a/vendor/cakephp/database/QueryCompiler.php b/vendor/cakephp/database/QueryCompiler.php new file mode 100644 index 0000000..f0b6641 --- /dev/null +++ b/vendor/cakephp/database/QueryCompiler.php @@ -0,0 +1,463 @@ + + */ + protected $_templates = [ + 'delete' => 'DELETE', + 'where' => ' WHERE %s', + 'group' => ' GROUP BY %s ', + 'having' => ' HAVING %s ', + 'order' => ' %s', + 'limit' => ' LIMIT %s', + 'offset' => ' OFFSET %s', + 'epilog' => ' %s', + ]; + + /** + * The list of query clauses to traverse for generating a SELECT statement + * + * @var array + */ + protected $_selectParts = [ + 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order', + 'limit', 'offset', 'union', 'epilog', + ]; + + /** + * The list of query clauses to traverse for generating an UPDATE statement + * + * @var array + * @deprecated Not used. + */ + protected $_updateParts = ['with', 'update', 'set', 'where', 'epilog']; + + /** + * The list of query clauses to traverse for generating a DELETE statement + * + * @var array + */ + protected $_deleteParts = ['with', 'delete', 'modifier', 'from', 'where', 'epilog']; + + /** + * The list of query clauses to traverse for generating an INSERT statement + * + * @var array + */ + protected $_insertParts = ['with', 'insert', 'values', 'epilog']; + + /** + * Indicate whether this query dialect supports ordered unions. + * + * Overridden in subclasses. + * + * @var bool + */ + protected $_orderedUnion = true; + + /** + * Indicate whether aliases in SELECT clause need to be always quoted. + * + * @var bool + */ + protected $_quotedSelectAliases = false; + + /** + * Returns the SQL representation of the provided query after generating + * the placeholders for the bound values using the provided generator + * + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholders + * @return string + */ + public function compile(Query $query, ValueBinder $binder): string + { + $sql = ''; + $type = $query->type(); + $query->traverseParts( + $this->_sqlCompiler($sql, $query, $binder), + $this->{"_{$type}Parts"} + ); + + // Propagate bound parameters from sub-queries if the + // placeholders can be found in the SQL statement. + if ($query->getValueBinder() !== $binder) { + foreach ($query->getValueBinder()->bindings() as $binding) { + $placeholder = ':' . $binding['placeholder']; + if (preg_match('/' . $placeholder . '(?:\W|$)/', $sql) > 0) { + $binder->bind($placeholder, $binding['value'], $binding['type']); + } + } + } + + return $sql; + } + + /** + * Returns a callable object that can be used to compile a SQL string representation + * of this query. + * + * @param string $sql initial sql string to append to + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return \Closure + */ + protected function _sqlCompiler(string &$sql, Query $query, ValueBinder $binder): Closure + { + return function ($part, $partName) use (&$sql, $query, $binder) { + if ( + $part === null || + (is_array($part) && empty($part)) || + ($part instanceof Countable && count($part) === 0) + ) { + return; + } + + if ($part instanceof ExpressionInterface) { + $part = [$part->sql($binder)]; + } + if (isset($this->_templates[$partName])) { + $part = $this->_stringifyExpressions((array)$part, $binder); + $sql .= sprintf($this->_templates[$partName], implode(', ', $part)); + + return; + } + + $sql .= $this->{'_build' . $partName . 'Part'}($part, $query, $binder); + }; + } + + /** + * Helper function used to build the string representation of a `WITH` clause, + * it constructs the CTE definitions list and generates the `RECURSIVE` + * keyword when required. + * + * @param array $parts List of CTEs to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string + { + $recursive = false; + $expressions = []; + foreach ($parts as $cte) { + $recursive = $recursive || $cte->isRecursive(); + $expressions[] = $cte->sql($binder); + } + + $recursive = $recursive ? 'RECURSIVE ' : ''; + + return sprintf('WITH %s%s ', $recursive, implode(', ', $expressions)); + } + + /** + * Helper function used to build the string representation of a SELECT clause, + * it constructs the field list taking care of aliasing and + * converting expression objects to string. This function also constructs the + * DISTINCT clause for the query. + * + * @param array $parts list of fields to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildSelectPart(array $parts, Query $query, ValueBinder $binder): string + { + $select = 'SELECT%s %s%s'; + if ($this->_orderedUnion && $query->clause('union')) { + $select = '(SELECT%s %s%s'; + } + $distinct = $query->clause('distinct'); + $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder); + + $driver = $query->getConnection()->getDriver(); + $quoteIdentifiers = $driver->isAutoQuotingEnabled() || $this->_quotedSelectAliases; + $normalized = []; + $parts = $this->_stringifyExpressions($parts, $binder); + foreach ($parts as $k => $p) { + if (!is_numeric($k)) { + $p = $p . ' AS '; + if ($quoteIdentifiers) { + $p .= $driver->quoteIdentifier($k); + } else { + $p .= $k; + } + } + $normalized[] = $p; + } + + if ($distinct === true) { + $distinct = 'DISTINCT '; + } + + if (is_array($distinct)) { + $distinct = $this->_stringifyExpressions($distinct, $binder); + $distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct)); + } + + return sprintf($select, $modifiers, $distinct, implode(', ', $normalized)); + } + + /** + * Helper function used to build the string representation of a FROM clause, + * it constructs the tables list taking care of aliasing and + * converting expression objects to string. + * + * @param array $parts list of tables to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildFromPart(array $parts, Query $query, ValueBinder $binder): string + { + $select = ' FROM %s'; + $normalized = []; + $parts = $this->_stringifyExpressions($parts, $binder); + foreach ($parts as $k => $p) { + if (!is_numeric($k)) { + $p = $p . ' ' . $k; + } + $normalized[] = $p; + } + + return sprintf($select, implode(', ', $normalized)); + } + + /** + * Helper function used to build the string representation of multiple JOIN clauses, + * it constructs the joins list taking care of aliasing and converting + * expression objects to string in both the table to be joined and the conditions + * to be used. + * + * @param array $parts list of joins to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildJoinPart(array $parts, Query $query, ValueBinder $binder): string + { + $joins = ''; + foreach ($parts as $join) { + if (!isset($join['table'])) { + throw new DatabaseException(sprintf( + 'Could not compile join clause for alias `%s`. No table was specified. ' . + 'Use the `table` key to define a table.', + $join['alias'] + )); + } + if ($join['table'] instanceof ExpressionInterface) { + $join['table'] = '(' . $join['table']->sql($binder) . ')'; + } + + $joins .= sprintf(' %s JOIN %s %s', $join['type'], $join['table'], $join['alias']); + + $condition = ''; + if (isset($join['conditions']) && $join['conditions'] instanceof ExpressionInterface) { + $condition = $join['conditions']->sql($binder); + } + if ($condition === '') { + $joins .= ' ON 1 = 1'; + } else { + $joins .= " ON {$condition}"; + } + } + + return $joins; + } + + /** + * Helper function to build the string representation of a window clause. + * + * @param array $parts List of windows to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildWindowPart(array $parts, Query $query, ValueBinder $binder): string + { + $windows = []; + foreach ($parts as $window) { + $windows[] = $window['name']->sql($binder) . ' AS (' . $window['window']->sql($binder) . ')'; + } + + return ' WINDOW ' . implode(', ', $windows); + } + + /** + * Helper function to generate SQL for SET expressions. + * + * @param array $parts List of keys & values to set. + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildSetPart(array $parts, Query $query, ValueBinder $binder): string + { + $set = []; + foreach ($parts as $part) { + if ($part instanceof ExpressionInterface) { + $part = $part->sql($binder); + } + if ($part[0] === '(') { + $part = substr($part, 1, -1); + } + $set[] = $part; + } + + return ' SET ' . implode('', $set); + } + + /** + * Builds the SQL string for all the UNION clauses in this query, when dealing + * with query objects it will also transform them using their configured SQL + * dialect. + * + * @param array $parts list of queries to be operated with UNION + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildUnionPart(array $parts, Query $query, ValueBinder $binder): string + { + $parts = array_map(function ($p) use ($binder) { + $p['query'] = $p['query']->sql($binder); + $p['query'] = $p['query'][0] === '(' ? trim($p['query'], '()') : $p['query']; + $prefix = $p['all'] ? 'ALL ' : ''; + if ($this->_orderedUnion) { + return "{$prefix}({$p['query']})"; + } + + return $prefix . $p['query']; + }, $parts); + + if ($this->_orderedUnion) { + return sprintf(")\nUNION %s", implode("\nUNION ", $parts)); + } + + return sprintf("\nUNION %s", implode("\nUNION ", $parts)); + } + + /** + * Builds the SQL fragment for INSERT INTO. + * + * @param array $parts The insert parts. + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string SQL fragment. + */ + protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string + { + if (!isset($parts[0])) { + throw new DatabaseException( + 'Could not compile insert query. No table was specified. ' . + 'Use `into()` to define a table.' + ); + } + $table = $parts[0]; + $columns = $this->_stringifyExpressions($parts[1], $binder); + $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder); + + return sprintf('INSERT%s INTO %s (%s)', $modifiers, $table, implode(', ', $columns)); + } + + /** + * Builds the SQL fragment for INSERT INTO. + * + * @param array $parts The values parts. + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string SQL fragment. + */ + protected function _buildValuesPart(array $parts, Query $query, ValueBinder $binder): string + { + return implode('', $this->_stringifyExpressions($parts, $binder)); + } + + /** + * Builds the SQL fragment for UPDATE. + * + * @param array $parts The update parts. + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string SQL fragment. + */ + protected function _buildUpdatePart(array $parts, Query $query, ValueBinder $binder): string + { + $table = $this->_stringifyExpressions($parts, $binder); + $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder); + + return sprintf('UPDATE%s %s', $modifiers, implode(',', $table)); + } + + /** + * Builds the SQL modifier fragment + * + * @param array $parts The query modifier parts + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string SQL fragment. + */ + protected function _buildModifierPart(array $parts, Query $query, ValueBinder $binder): string + { + if ($parts === []) { + return ''; + } + + return ' ' . implode(' ', $this->_stringifyExpressions($parts, $binder, false)); + } + + /** + * Helper function used to covert ExpressionInterface objects inside an array + * into their string representation. + * + * @param array $expressions list of strings and ExpressionInterface objects + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @param bool $wrap Whether to wrap each expression object with parenthesis + * @return array + */ + protected function _stringifyExpressions(array $expressions, ValueBinder $binder, bool $wrap = true): array + { + $result = []; + foreach ($expressions as $k => $expression) { + if ($expression instanceof ExpressionInterface) { + $value = $expression->sql($binder); + $expression = $wrap ? '(' . $value . ')' : $value; + } + $result[$k] = $expression; + } + + return $result; + } +} diff --git a/vendor/cakephp/database/README.md b/vendor/cakephp/database/README.md new file mode 100644 index 0000000..877c7b6 --- /dev/null +++ b/vendor/cakephp/database/README.md @@ -0,0 +1,364 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/database.svg?style=flat-square)](https://packagist.org/packages/cakephp/database) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# A flexible and lightweight Database Library for PHP + +This library abstracts and provides help with most aspects of dealing with relational +databases such as keeping connections to the server, building queries, +preventing SQL injections, inspecting and altering schemas, and with debugging and +profiling queries sent to the database. + +It adopts the API from the native PDO extension in PHP for familiarity, but solves many of the +inconsistencies PDO has, while also providing several features that extend PDO's capabilities. + +A distinguishing factor of this library when compared to similar database connection packages, +is that it takes the concept of "data types" to its core. It lets you work with complex PHP objects +or structures that can be passed as query conditions or to be inserted in the database. + +The typing system will intelligently convert the PHP structures when passing them to the database, and +convert them back when retrieving. + + +## Connecting to the database + +This library is able to work with the following databases: + +* MySQL +* Postgres +* SQLite +* Microsoft SQL Server (2008 and above) + +The first thing you need to do when using this library is create a connection object. +Before performing any operations with the connection, you need to specify a driver +to use: + +```php +use Cake\Database\Connection; +use Cake\Database\Driver\Mysql; + +$driver = new Mysql([ + 'database' => 'test', + 'username' => 'root', + 'password' => 'secret' +]); +$connection = new Connection([ + 'driver' => $driver +]); +``` + +Drivers are classes responsible for actually executing the commands to the database and +correctly building the SQL according to the database specific dialect. Drivers can also +be specified by passing a class name. In that case, include all the connection details +directly in the options array: + +```php +use Cake\Database\Connection; + +$connection = new Connection([ + 'driver' => Cake\Database\Driver\Sqlite::class, + 'database' => '/path/to/file.db' +]); +``` + +### Connection options + +This is a list of possible options that can be passed when creating a connection: + +* `persistent`: Creates a persistent connection +* `host`: The server host +* `database`: The database name +* `username`: Login credential +* `password`: Connection secret +* `encoding`: The connection encoding (or charset) +* `timezone`: The connection timezone or time offset + +## Using connections + +After creating a connection, you can immediately interact with the database. You can choose +either to use the shorthand methods `execute()`, `insert()`, `update()`, `delete()` or use the +`newQuery()` for using a query builder. + +The easiest way of executing queries is by using the `execute()` method, it will return a +`Cake\Database\StatementInterface` that you can use to get the data back: + +```php +$statement = $connection->execute('SELECT * FROM articles'); + +while($row = $statement->fetch('assoc')) { + echo $row['title'] . PHP_EOL; +} +``` +Binding values to parametrized arguments is also possible with the execute function: + +```php +$statement = $connection->execute('SELECT * FROM articles WHERE id = :id', ['id' => 1], ['id' => 'integer']); +$results = $statement->fetch('assoc'); +``` + +The third parameter is the types the passed values should be converted to when passed to the database. If +no types are passed, all arguments will be interpreted as a string. + +Alternatively you can construct a statement manually and then fetch rows from it: + +```php +$statement = $connection->prepare('SELECT * from articles WHERE id != :id'); +$statement->bind(['id' => 1], ['id' => 'integer']); +$results = $statement->fetchAll('assoc'); +``` + +The default types that are understood by this library and can be passed to the `bind()` function or to `execute()` +are: + +* biginteger +* binary +* date +* float +* decimal +* integer +* time +* datetime +* timestamp +* uuid + +More types can be added dynamically in a bit. + +Statements can be reused by binding new values to the parameters in the query: + +```php +$statement = $connection->prepare('SELECT * from articles WHERE id = :id'); +$statement->bind(['id' => 1], ['id' => 'integer']); +$results = $statement->fetchAll('assoc'); + +$statement->bind(['id' => 1], ['id' => 'integer']); +$results = $statement->fetchAll('assoc'); +``` + +### Updating Rows + +Updating can be done using the `update()` function in the connection object. In the following +example we will update the title of the article with id = 1: + +```php +$connection->update('articles', ['title' => 'New title'], ['id' => 1]); +``` + +The concept of data types is central to this library, so you can use the last parameter of the function +to specify what types should be used: + +```php +$connection->update( + 'articles', + ['title' => 'New title'], + ['created >=' => new DateTime('-3 day'), 'created <' => new DateTime('now')], + ['created' => 'datetime'] +); +``` + +The example above will execute the following SQL: + +```sql +UPDATE articles SET title = 'New Title' WHERE created >= '2014-10-10 00:00:00' AND created < '2014-10-13 00:00:00'; +``` + +More on creating complex where conditions or more complex update queries later. + +### Deleting Rows + +Similarly, the `delete()` method is used to delete rows from the database: + +```php +$connection->delete('articles', ['created <' => DateTime('now')], ['created' => 'date']); +``` + +Will generate the following SQL + +```sql +DELETE FROM articles where created < '2014-10-10' +``` + +### Inserting Rows + +Rows can be inserted using the `insert()` method: + +```php +$connection->insert( + 'articles', + ['title' => 'My Title', 'body' => 'Some paragraph', 'created' => new DateTime()], + ['created' => 'datetime'] +); +``` + +More complex updates, deletes and insert queries can be generated using the `Query` class. + +## Query Builder + +One of the goals of this library is to allow the generation of both simple and complex queries with +ease. The query builder can be accessed by getting a new instance of a query: + +```php +$query = $connection->newQuery(); +``` + +### Selecting Fields + +Adding fields to the `SELECT` clause: + +```php +$query->select(['id', 'title', 'body']); + +// Results in SELECT id AS pk, title AS aliased_title, body ... +$query->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); + +// Use a closure +$query->select(function ($query) { + return ['id', 'title', 'body']; +}); +``` + +### Where Conditions + +Generating conditions: + +```php +// WHERE id = 1 +$query->where(['id' => 1]); + +// WHERE id > 2 +$query->where(['id >' => 1]); +``` + +As you can see you can use any operator by placing it with a space after the field name. +Adding multiple conditions is easy as well: + +```php +$query->where(['id >' => 1])->andWhere(['title' => 'My Title']); + +// Equivalent to +$query->where(['id >' => 1, 'title' => 'My title']); +``` + +It is possible to generate `OR` conditions as well + +```php +$query->where(['OR' => ['id >' => 1, 'title' => 'My title']]); +``` + +For even more complex conditions you can use closures and expression objects: + +```php +$query->where(function ($exp) { + return $exp + ->eq('author_id', 2) + ->eq('published', true) + ->notEq('spam', true) + ->gt('view_count', 10); + }); +``` + +Which results in: + +```sql +SELECT * FROM articles +WHERE + author_id = 2 + AND published = 1 + AND spam != 1 + AND view_count > 10 +``` + +Combining expressions is also possible: + +```php +$query->where(function ($exp) { + $orConditions = $exp->or(['author_id' => 2]) + ->eq('author_id', 5); + return $exp + ->not($orConditions) + ->lte('view_count', 10); + }); +``` + +That generates: + +```sql +SELECT * +FROM articles +WHERE + NOT (author_id = 2 OR author_id = 5) + AND view_count <= 10 +``` + +When using the expression objects you can use the following methods to create conditions: + +* `eq()` Creates an equality condition. +* `notEq()` Create an inequality condition +* `like()` Create a condition using the LIKE operator. +* `notLike()` Create a negated LIKE condition. +* `in()` Create a condition using IN. +* `notIn()` Create a negated condition using IN. +* `gt()` Create a > condition. +* `gte()` Create a >= condition. +* `lt()` Create a < condition. +* `lte()` Create a <= condition. +* `isNull()` Create an IS NULL condition. +* `isNotNull()` Create a negated IS NULL condition. + +### Aggregates and SQL Functions + +```php +// Results in SELECT COUNT(*) count FROM ... +$query->select(['count' => $query->func()->count('*')]); +``` + +A number of commonly used functions can be created with the func() method: + +* `sum()` Calculate a sum. The arguments will be treated as literal values. +* `avg()` Calculate an average. The arguments will be treated as literal values. +* `min()` Calculate the min of a column. The arguments will be treated as literal values. +* `max()` Calculate the max of a column. The arguments will be treated as literal values. +* `count()` Calculate the count. The arguments will be treated as literal values. +* `concat()` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. +* `coalesce()` Coalesce values. The arguments are treated as bound parameters unless marked as literal. +* `dateDiff()` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. +* `now()` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. + +When providing arguments for SQL functions, there are two kinds of parameters you can use, literal arguments and bound parameters. Literal +parameters allow you to reference columns or other SQL literals. Bound parameters can be used to safely add user data to SQL functions. +For example: + +```php +$concat = $query->func()->concat([ + 'title' => 'literal', + ' NEW' +]); +$query->select(['title' => $concat]); +``` + +The above generates: + +```sql +SELECT CONCAT(title, :c0) ...; +``` + +### Other SQL Clauses + +Read of all other SQL clauses that the builder is capable of generating in the [official API docs](https://api.cakephp.org/4.x/class-Cake.Database.Query.html) + +### Getting Results out of a Query + +Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: + +```php +// Iterate the query +foreach ($query as $row) { + // Do stuff. +} + +// Get the statement and fetch all results +$results = $query->execute()->fetchAll('assoc'); +``` + +## Official API + +You can read the official [official API docs](https://api.cakephp.org/4.x/namespace-Cake.Database.html) to learn more of what this library +has to offer. diff --git a/vendor/cakephp/database/Retry/ErrorCodeWaitStrategy.php b/vendor/cakephp/database/Retry/ErrorCodeWaitStrategy.php new file mode 100644 index 0000000..010a531 --- /dev/null +++ b/vendor/cakephp/database/Retry/ErrorCodeWaitStrategy.php @@ -0,0 +1,69 @@ + + */ + protected $errorCodes; + + /** + * @var int + */ + protected $retryInterval; + + /** + * @param array $errorCodes DB-specific error codes that allow retrying + * @param int $retryInterval Seconds to wait before allowing next retry, 0 for no wait. + */ + public function __construct(array $errorCodes, int $retryInterval) + { + $this->errorCodes = $errorCodes; + $this->retryInterval = $retryInterval; + } + + /** + * @inheritDoc + */ + public function shouldRetry(Exception $exception, int $retryCount): bool + { + if ( + $exception instanceof PDOException && + $exception->errorInfo && + in_array($exception->errorInfo[1], $this->errorCodes) + ) { + if ($this->retryInterval > 0) { + sleep($this->retryInterval); + } + + return true; + } + + return false; + } +} diff --git a/vendor/cakephp/database/Retry/ReconnectStrategy.php b/vendor/cakephp/database/Retry/ReconnectStrategy.php new file mode 100644 index 0000000..2cdd098 --- /dev/null +++ b/vendor/cakephp/database/Retry/ReconnectStrategy.php @@ -0,0 +1,122 @@ + + */ + protected static $causes = [ + 'gone away', + 'Lost connection', + 'Transaction() on null', + 'closed the connection unexpectedly', + 'closed unexpectedly', + 'deadlock avoided', + 'decryption failed or bad record mac', + 'is dead or not enabled', + 'no connection to the server', + 'query_wait_timeout', + 'reset by peer', + 'terminate due to client_idle_limit', + 'while sending', + 'writing data to the connection', + ]; + + /** + * The connection to check for validity + * + * @var \Cake\Database\Connection + */ + protected $connection; + + /** + * Creates the ReconnectStrategy object by storing a reference to the + * passed connection. This reference will be used to automatically + * reconnect to the server in case of failure. + * + * @param \Cake\Database\Connection $connection The connection to check + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * {@inheritDoc} + * + * Checks whether the exception was caused by a lost connection, + * and returns true if it was able to successfully reconnect. + */ + public function shouldRetry(Exception $exception, int $retryCount): bool + { + $message = $exception->getMessage(); + + foreach (static::$causes as $cause) { + if (strstr($message, $cause) !== false) { + return $this->reconnect(); + } + } + + return false; + } + + /** + * Tries to re-establish the connection to the server, if it is safe to do so + * + * @return bool Whether the connection was re-established + */ + protected function reconnect(): bool + { + if ($this->connection->inTransaction()) { + // It is not safe to blindly reconnect in the middle of a transaction + return false; + } + + try { + // Make sure we free any resources associated with the old connection + $this->connection->disconnect(); + } catch (Exception $e) { + } + + try { + $this->connection->connect(); + $this->connection->log('[RECONNECT]'); + + return true; + } catch (Exception $e) { + // If there was an error connecting again, don't report it back, + // let the retry handler do it. + return false; + } + } +} diff --git a/vendor/cakephp/database/Schema/BaseSchema.php b/vendor/cakephp/database/Schema/BaseSchema.php new file mode 100644 index 0000000..4651317 --- /dev/null +++ b/vendor/cakephp/database/Schema/BaseSchema.php @@ -0,0 +1,5 @@ +collection = $collection; + $this->prefix = $prefix; + $this->cacher = $cacher; + } + + /** + * @inheritDoc + */ + public function listTables(): array + { + return $this->collection->listTables(); + } + + /** + * @inheritDoc + */ + public function describe(string $name, array $options = []): TableSchemaInterface + { + $options += ['forceRefresh' => false]; + $cacheKey = $this->cacheKey($name); + + if (!$options['forceRefresh']) { + $cached = $this->cacher->get($cacheKey); + if ($cached !== null) { + return $cached; + } + } + + $table = $this->collection->describe($name, $options); + $this->cacher->set($cacheKey, $table); + + return $table; + } + + /** + * Get the cache key for a given name. + * + * @param string $name The name to get a cache key for. + * @return string The cache key. + */ + public function cacheKey(string $name): string + { + return $this->prefix . '_' . $name; + } + + /** + * Set a cacher. + * + * @param \Psr\SimpleCache\CacheInterface $cacher Cacher object + * @return $this + */ + public function setCacher(CacheInterface $cacher) + { + $this->cacher = $cacher; + + return $this; + } + + /** + * Get a cacher. + * + * @return \Psr\SimpleCache\CacheInterface $cacher Cacher object + */ + public function getCacher(): CacheInterface + { + return $this->cacher; + } +} diff --git a/vendor/cakephp/database/Schema/Collection.php b/vendor/cakephp/database/Schema/Collection.php new file mode 100644 index 0000000..62775e2 --- /dev/null +++ b/vendor/cakephp/database/Schema/Collection.php @@ -0,0 +1,150 @@ +_connection = $connection; + $this->_dialect = $connection->getDriver()->schemaDialect(); + } + + /** + * Get the list of tables available in the current connection. + * + * @return array The list of tables in the connected database/schema. + */ + public function listTables(): array + { + [$sql, $params] = $this->_dialect->listTablesSql($this->_connection->config()); + $result = []; + $statement = $this->_connection->execute($sql, $params); + while ($row = $statement->fetch()) { + $result[] = $row[0]; + } + $statement->closeCursor(); + + return $result; + } + + /** + * Get the column metadata for a table. + * + * The name can include a database schema name in the form 'schema.table'. + * + * Caching will be applied if `cacheMetadata` key is present in the Connection + * configuration options. Defaults to _cake_model_ when true. + * + * ### Options + * + * - `forceRefresh` - Set to true to force rebuilding the cached metadata. + * Defaults to false. + * + * @param string $name The name of the table to describe. + * @param array $options The options to use, see above. + * @return \Cake\Database\Schema\TableSchema Object with column metadata. + * @throws \Cake\Database\Exception\DatabaseException when table cannot be described. + */ + public function describe(string $name, array $options = []): TableSchemaInterface + { + $config = $this->_connection->config(); + if (strpos($name, '.')) { + [$config['schema'], $name] = explode('.', $name); + } + $table = $this->_connection->getDriver()->newTableSchema($name); + + $this->_reflect('Column', $name, $config, $table); + if (count($table->columns()) === 0) { + throw new DatabaseException(sprintf('Cannot describe %s. It has 0 columns.', $name)); + } + + $this->_reflect('Index', $name, $config, $table); + $this->_reflect('ForeignKey', $name, $config, $table); + $this->_reflect('Options', $name, $config, $table); + + return $table; + } + + /** + * Helper method for running each step of the reflection process. + * + * @param string $stage The stage name. + * @param string $name The table name. + * @param array $config The config data. + * @param \Cake\Database\Schema\TableSchema $schema The table schema instance. + * @return void + * @throws \Cake\Database\Exception\DatabaseException on query failure. + * @uses \Cake\Database\Schema\SchemaDialect::describeColumnSql + * @uses \Cake\Database\Schema\SchemaDialect::describeIndexSql + * @uses \Cake\Database\Schema\SchemaDialect::describeForeignKeySql + * @uses \Cake\Database\Schema\SchemaDialect::describeOptionsSql + * @uses \Cake\Database\Schema\SchemaDialect::convertColumnDescription + * @uses \Cake\Database\Schema\SchemaDialect::convertIndexDescription + * @uses \Cake\Database\Schema\SchemaDialect::convertForeignKeyDescription + * @uses \Cake\Database\Schema\SchemaDialect::convertOptionsDescription + */ + protected function _reflect(string $stage, string $name, array $config, TableSchema $schema): void + { + $describeMethod = "describe{$stage}Sql"; + $convertMethod = "convert{$stage}Description"; + + [$sql, $params] = $this->_dialect->{$describeMethod}($name, $config); + if (empty($sql)) { + return; + } + try { + $statement = $this->_connection->execute($sql, $params); + } catch (PDOException $e) { + throw new DatabaseException($e->getMessage(), 500, $e); + } + /** @psalm-suppress PossiblyFalseIterator */ + foreach ($statement->fetchAll('assoc') as $row) { + $this->_dialect->{$convertMethod}($schema, $row); + } + $statement->closeCursor(); + } +} diff --git a/vendor/cakephp/database/Schema/CollectionInterface.php b/vendor/cakephp/database/Schema/CollectionInterface.php new file mode 100644 index 0000000..a5c6198 --- /dev/null +++ b/vendor/cakephp/database/Schema/CollectionInterface.php @@ -0,0 +1,51 @@ + The list of tables in the connected database/schema. + */ + public function listTables(): array; + + /** + * Get the column metadata for a table. + * + * Caching will be applied if `cacheMetadata` key is present in the Connection + * configuration options. Defaults to _cake_model_ when true. + * + * ### Options + * + * - `forceRefresh` - Set to true to force rebuilding the cached metadata. + * Defaults to false. + * + * @param string $name The name of the table to describe. + * @param array $options The options to use, see above. + * @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata. + * @throws \Cake\Database\Exception\DatabaseException when table cannot be described. + */ + public function describe(string $name, array $options = []): TableSchemaInterface; +} diff --git a/vendor/cakephp/database/Schema/MysqlSchema.php b/vendor/cakephp/database/Schema/MysqlSchema.php new file mode 100644 index 0000000..237d636 --- /dev/null +++ b/vendor/cakephp/database/Schema/MysqlSchema.php @@ -0,0 +1,5 @@ +_driver->quoteIdentifier($config['database']), []]; + } + + /** + * @inheritDoc + */ + public function describeColumnSql(string $tableName, array $config): array + { + return ['SHOW FULL COLUMNS FROM ' . $this->_driver->quoteIdentifier($tableName), []]; + } + + /** + * @inheritDoc + */ + public function describeIndexSql(string $tableName, array $config): array + { + return ['SHOW INDEXES FROM ' . $this->_driver->quoteIdentifier($tableName), []]; + } + + /** + * @inheritDoc + */ + public function describeOptionsSql(string $tableName, array $config): array + { + return ['SHOW TABLE STATUS WHERE Name = ?', [$tableName]]; + } + + /** + * @inheritDoc + */ + public function convertOptionsDescription(TableSchema $schema, array $row): void + { + $schema->setOptions([ + 'engine' => $row['Engine'], + 'collation' => $row['Collation'], + ]); + } + + /** + * Convert a MySQL column type into an abstract type. + * + * The returned type will be a type that Cake\Database\TypeFactory can handle. + * + * @param string $column The column type + length + * @return array Array of column information. + * @throws \Cake\Database\Exception\DatabaseException When column type cannot be parsed. + */ + protected function _convertColumn(string $column): array + { + preg_match('/([a-z]+)(?:\(([0-9,]+)\))?\s*([a-z]+)?/i', $column, $matches); + if (empty($matches)) { + throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column)); + } + + $col = strtolower($matches[1]); + $length = $precision = $scale = null; + if (isset($matches[2]) && strlen($matches[2])) { + $length = $matches[2]; + if (strpos($matches[2], ',') !== false) { + [$length, $precision] = explode(',', $length); + } + $length = (int)$length; + $precision = (int)$precision; + } + + $type = $this->_applyTypeSpecificColumnConversion( + $col, + compact('length', 'precision', 'scale') + ); + if ($type !== null) { + return $type; + } + + if (in_array($col, ['date', 'time'])) { + return ['type' => $col, 'length' => null]; + } + if (in_array($col, ['datetime', 'timestamp'])) { + $typeName = $col; + if ($length > 0) { + $typeName = $col . 'fractional'; + } + + return ['type' => $typeName, 'length' => null, 'precision' => $length]; + } + + if (($col === 'tinyint' && $length === 1) || $col === 'boolean') { + return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null]; + } + + $unsigned = (isset($matches[3]) && strtolower($matches[3]) === 'unsigned'); + if (strpos($col, 'bigint') !== false || $col === 'bigint') { + return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => null, 'unsigned' => $unsigned]; + } + if ($col === 'tinyint') { + return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => null, 'unsigned' => $unsigned]; + } + if ($col === 'smallint') { + return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => null, 'unsigned' => $unsigned]; + } + if (in_array($col, ['int', 'integer', 'mediumint'])) { + return ['type' => TableSchema::TYPE_INTEGER, 'length' => null, 'unsigned' => $unsigned]; + } + if ($col === 'char' && $length === 36) { + return ['type' => TableSchema::TYPE_UUID, 'length' => null]; + } + if ($col === 'char') { + return ['type' => TableSchema::TYPE_CHAR, 'length' => $length]; + } + if (strpos($col, 'char') !== false) { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length]; + } + if (strpos($col, 'text') !== false) { + $lengthName = substr($col, 0, -4); + $length = TableSchema::$columnLengths[$lengthName] ?? null; + + return ['type' => TableSchema::TYPE_TEXT, 'length' => $length]; + } + if ($col === 'binary' && $length === 16) { + return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null]; + } + if (strpos($col, 'blob') !== false || in_array($col, ['binary', 'varbinary'])) { + $lengthName = substr($col, 0, -4); + $length = TableSchema::$columnLengths[$lengthName] ?? $length; + + return ['type' => TableSchema::TYPE_BINARY, 'length' => $length]; + } + if (strpos($col, 'float') !== false || strpos($col, 'double') !== false) { + return [ + 'type' => TableSchema::TYPE_FLOAT, + 'length' => $length, + 'precision' => $precision, + 'unsigned' => $unsigned, + ]; + } + if (strpos($col, 'decimal') !== false) { + return [ + 'type' => TableSchema::TYPE_DECIMAL, + 'length' => $length, + 'precision' => $precision, + 'unsigned' => $unsigned, + ]; + } + + if (strpos($col, 'json') !== false) { + return ['type' => TableSchema::TYPE_JSON, 'length' => null]; + } + + return ['type' => TableSchema::TYPE_STRING, 'length' => null]; + } + + /** + * @inheritDoc + */ + public function convertColumnDescription(TableSchema $schema, array $row): void + { + $field = $this->_convertColumn($row['Type']); + $field += [ + 'null' => $row['Null'] === 'YES', + 'default' => $row['Default'], + 'collate' => $row['Collation'], + 'comment' => $row['Comment'], + ]; + if (isset($row['Extra']) && $row['Extra'] === 'auto_increment') { + $field['autoIncrement'] = true; + } + $schema->addColumn($row['Field'], $field); + } + + /** + * @inheritDoc + */ + public function convertIndexDescription(TableSchema $schema, array $row): void + { + $type = null; + $columns = $length = []; + + $name = $row['Key_name']; + if ($name === 'PRIMARY') { + $name = $type = TableSchema::CONSTRAINT_PRIMARY; + } + + $columns[] = $row['Column_name']; + + if ($row['Index_type'] === 'FULLTEXT') { + $type = TableSchema::INDEX_FULLTEXT; + } elseif ((int)$row['Non_unique'] === 0 && $type !== 'primary') { + $type = TableSchema::CONSTRAINT_UNIQUE; + } elseif ($type !== 'primary') { + $type = TableSchema::INDEX_INDEX; + } + + if (!empty($row['Sub_part'])) { + $length[$row['Column_name']] = $row['Sub_part']; + } + $isIndex = ( + $type === TableSchema::INDEX_INDEX || + $type === TableSchema::INDEX_FULLTEXT + ); + if ($isIndex) { + $existing = $schema->getIndex($name); + } else { + $existing = $schema->getConstraint($name); + } + + // MySQL multi column indexes come back as multiple rows. + if (!empty($existing)) { + $columns = array_merge($existing['columns'], $columns); + $length = array_merge($existing['length'], $length); + } + if ($isIndex) { + $schema->addIndex($name, [ + 'type' => $type, + 'columns' => $columns, + 'length' => $length, + ]); + } else { + $schema->addConstraint($name, [ + 'type' => $type, + 'columns' => $columns, + 'length' => $length, + ]); + } + } + + /** + * @inheritDoc + */ + public function describeForeignKeySql(string $tableName, array $config): array + { + $sql = 'SELECT * FROM information_schema.key_column_usage AS kcu + INNER JOIN information_schema.referential_constraints AS rc + ON ( + kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA + ) + WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND rc.TABLE_NAME = ? + ORDER BY kcu.ORDINAL_POSITION ASC'; + + return [$sql, [$config['database'], $tableName, $tableName]]; + } + + /** + * @inheritDoc + */ + public function convertForeignKeyDescription(TableSchema $schema, array $row): void + { + $data = [ + 'type' => TableSchema::CONSTRAINT_FOREIGN, + 'columns' => [$row['COLUMN_NAME']], + 'references' => [$row['REFERENCED_TABLE_NAME'], $row['REFERENCED_COLUMN_NAME']], + 'update' => $this->_convertOnClause($row['UPDATE_RULE']), + 'delete' => $this->_convertOnClause($row['DELETE_RULE']), + ]; + $name = $row['CONSTRAINT_NAME']; + $schema->addConstraint($name, $data); + } + + /** + * @inheritDoc + */ + public function truncateTableSql(TableSchema $schema): array + { + return [sprintf('TRUNCATE TABLE `%s`', $schema->name())]; + } + + /** + * @inheritDoc + */ + public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array + { + $content = implode(",\n", array_merge($columns, $constraints, $indexes)); + $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' '; + $content = sprintf("CREATE%sTABLE `%s` (\n%s\n)", $temporary, $schema->name(), $content); + $options = $schema->getOptions(); + if (isset($options['engine'])) { + $content .= sprintf(' ENGINE=%s', $options['engine']); + } + if (isset($options['charset'])) { + $content .= sprintf(' DEFAULT CHARSET=%s', $options['charset']); + } + if (isset($options['collate'])) { + $content .= sprintf(' COLLATE=%s', $options['collate']); + } + + return [$content]; + } + + /** + * @inheritDoc + */ + public function columnSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getColumn($name); + + $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name); + if ($sql !== null) { + return $sql; + } + + $out = $this->_driver->quoteIdentifier($name); + $nativeJson = $this->_driver->supports(DriverInterface::FEATURE_JSON); + + $typeMap = [ + TableSchema::TYPE_TINYINTEGER => ' TINYINT', + TableSchema::TYPE_SMALLINTEGER => ' SMALLINT', + TableSchema::TYPE_INTEGER => ' INTEGER', + TableSchema::TYPE_BIGINTEGER => ' BIGINT', + TableSchema::TYPE_BINARY_UUID => ' BINARY(16)', + TableSchema::TYPE_BOOLEAN => ' BOOLEAN', + TableSchema::TYPE_FLOAT => ' FLOAT', + TableSchema::TYPE_DECIMAL => ' DECIMAL', + TableSchema::TYPE_DATE => ' DATE', + TableSchema::TYPE_TIME => ' TIME', + TableSchema::TYPE_DATETIME => ' DATETIME', + TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIME', + TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP', + TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMP', + TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMP', + TableSchema::TYPE_CHAR => ' CHAR', + TableSchema::TYPE_UUID => ' CHAR(36)', + TableSchema::TYPE_JSON => $nativeJson ? ' JSON' : ' LONGTEXT', + ]; + $specialMap = [ + 'string' => true, + 'text' => true, + 'char' => true, + 'binary' => true, + ]; + if (isset($typeMap[$data['type']])) { + $out .= $typeMap[$data['type']]; + } + if (isset($specialMap[$data['type']])) { + switch ($data['type']) { + case TableSchema::TYPE_STRING: + $out .= ' VARCHAR'; + if (!isset($data['length'])) { + $data['length'] = 255; + } + break; + case TableSchema::TYPE_TEXT: + $isKnownLength = in_array($data['length'], TableSchema::$columnLengths); + if (empty($data['length']) || !$isKnownLength) { + $out .= ' TEXT'; + break; + } + + /** @var string $length */ + $length = array_search($data['length'], TableSchema::$columnLengths); + $out .= ' ' . strtoupper($length) . 'TEXT'; + + break; + case TableSchema::TYPE_BINARY: + $isKnownLength = in_array($data['length'], TableSchema::$columnLengths); + if ($isKnownLength) { + /** @var string $length */ + $length = array_search($data['length'], TableSchema::$columnLengths); + $out .= ' ' . strtoupper($length) . 'BLOB'; + break; + } + + if (empty($data['length'])) { + $out .= ' BLOB'; + break; + } + + if ($data['length'] > 2) { + $out .= ' VARBINARY(' . $data['length'] . ')'; + } else { + $out .= ' BINARY(' . $data['length'] . ')'; + } + break; + } + } + $hasLength = [ + TableSchema::TYPE_INTEGER, + TableSchema::TYPE_CHAR, + TableSchema::TYPE_SMALLINTEGER, + TableSchema::TYPE_TINYINTEGER, + TableSchema::TYPE_STRING, + ]; + if (in_array($data['type'], $hasLength, true) && isset($data['length'])) { + $out .= '(' . $data['length'] . ')'; + } + + $lengthAndPrecisionTypes = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL]; + if (in_array($data['type'], $lengthAndPrecisionTypes, true) && isset($data['length'])) { + if (isset($data['precision'])) { + $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')'; + } else { + $out .= '(' . (int)$data['length'] . ')'; + } + } + + $precisionTypes = [TableSchema::TYPE_DATETIME_FRACTIONAL, TableSchema::TYPE_TIMESTAMP_FRACTIONAL]; + if (in_array($data['type'], $precisionTypes, true) && isset($data['precision'])) { + $out .= '(' . (int)$data['precision'] . ')'; + } + + $hasUnsigned = [ + TableSchema::TYPE_TINYINTEGER, + TableSchema::TYPE_SMALLINTEGER, + TableSchema::TYPE_INTEGER, + TableSchema::TYPE_BIGINTEGER, + TableSchema::TYPE_FLOAT, + TableSchema::TYPE_DECIMAL, + ]; + if ( + in_array($data['type'], $hasUnsigned, true) && + isset($data['unsigned']) && + $data['unsigned'] === true + ) { + $out .= ' UNSIGNED'; + } + + $hasCollate = [ + TableSchema::TYPE_TEXT, + TableSchema::TYPE_CHAR, + TableSchema::TYPE_STRING, + ]; + if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') { + $out .= ' COLLATE ' . $data['collate']; + } + + if (isset($data['null']) && $data['null'] === false) { + $out .= ' NOT NULL'; + } + $addAutoIncrement = ( + $schema->getPrimaryKey() === [$name] && + !$schema->hasAutoincrement() && + !isset($data['autoIncrement']) + ); + if ( + in_array($data['type'], [TableSchema::TYPE_INTEGER, TableSchema::TYPE_BIGINTEGER]) && + ( + $data['autoIncrement'] === true || + $addAutoIncrement + ) + ) { + $out .= ' AUTO_INCREMENT'; + } + + $timestampTypes = [ + TableSchema::TYPE_TIMESTAMP, + TableSchema::TYPE_TIMESTAMP_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP_TIMEZONE, + ]; + if (isset($data['null']) && $data['null'] === true && in_array($data['type'], $timestampTypes, true)) { + $out .= ' NULL'; + unset($data['default']); + } + + $dateTimeTypes = [ + TableSchema::TYPE_DATETIME, + TableSchema::TYPE_DATETIME_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP, + TableSchema::TYPE_TIMESTAMP_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP_TIMEZONE, + ]; + if ( + isset($data['default']) && + in_array($data['type'], $dateTimeTypes) && + strpos(strtolower($data['default']), 'current_timestamp') !== false + ) { + $out .= ' DEFAULT CURRENT_TIMESTAMP'; + if (isset($data['precision'])) { + $out .= '(' . $data['precision'] . ')'; + } + unset($data['default']); + } + if (isset($data['default'])) { + $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']); + unset($data['default']); + } + if (isset($data['comment']) && $data['comment'] !== '') { + $out .= ' COMMENT ' . $this->_driver->schemaValue($data['comment']); + } + + return $out; + } + + /** + * @inheritDoc + */ + public function constraintSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getConstraint($name); + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) { + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf('PRIMARY KEY (%s)', implode(', ', $columns)); + } + + $out = ''; + if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) { + $out = 'UNIQUE KEY '; + } + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $out = 'CONSTRAINT '; + } + $out .= $this->_driver->quoteIdentifier($name); + + return $this->_keySql($out, $data); + } + + /** + * @inheritDoc + */ + public function addConstraintSql(TableSchema $schema): array + { + $sqlPattern = 'ALTER TABLE %s ADD %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + /** @var array $constraint */ + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name)); + } + } + + return $sql; + } + + /** + * @inheritDoc + */ + public function dropConstraintSql(TableSchema $schema): array + { + $sqlPattern = 'ALTER TABLE %s DROP FOREIGN KEY %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + /** @var array $constraint */ + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $constraintName = $this->_driver->quoteIdentifier($name); + $sql[] = sprintf($sqlPattern, $tableName, $constraintName); + } + } + + return $sql; + } + + /** + * @inheritDoc + */ + public function indexSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getIndex($name); + $out = ''; + if ($data['type'] === TableSchema::INDEX_INDEX) { + $out = 'KEY '; + } + if ($data['type'] === TableSchema::INDEX_FULLTEXT) { + $out = 'FULLTEXT KEY '; + } + $out .= $this->_driver->quoteIdentifier($name); + + return $this->_keySql($out, $data); + } + + /** + * Helper method for generating key SQL snippets. + * + * @param string $prefix The key prefix + * @param array $data Key data. + * @return string + */ + protected function _keySql(string $prefix, array $data): string + { + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + foreach ($data['columns'] as $i => $column) { + if (isset($data['length'][$column])) { + $columns[$i] .= sprintf('(%d)', $data['length'][$column]); + } + } + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + return $prefix . sprintf( + ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s', + implode(', ', $columns), + $this->_driver->quoteIdentifier($data['references'][0]), + $this->_convertConstraintColumns($data['references'][1]), + $this->_foreignOnClause($data['update']), + $this->_foreignOnClause($data['delete']) + ); + } + + return $prefix . ' (' . implode(', ', $columns) . ')'; + } +} + +// phpcs:disable +// Add backwards compatible alias. +class_alias('Cake\Database\Schema\MysqlSchemaDialect', 'Cake\Database\Schema\MysqlSchema'); +// phpcs:enable diff --git a/vendor/cakephp/database/Schema/PostgresSchema.php b/vendor/cakephp/database/Schema/PostgresSchema.php new file mode 100644 index 0000000..c746465 --- /dev/null +++ b/vendor/cakephp/database/Schema/PostgresSchema.php @@ -0,0 +1,5 @@ + Array of column information. + */ + protected function _convertColumn(string $column): array + { + preg_match('/([a-z\s]+)(?:\(([0-9,]+)\))?/i', $column, $matches); + if (empty($matches)) { + throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column)); + } + + $col = strtolower($matches[1]); + $length = $precision = $scale = null; + if (isset($matches[2])) { + $length = (int)$matches[2]; + } + + $type = $this->_applyTypeSpecificColumnConversion( + $col, + compact('length', 'precision', 'scale') + ); + if ($type !== null) { + return $type; + } + + if (in_array($col, ['date', 'time', 'boolean'], true)) { + return ['type' => $col, 'length' => null]; + } + if (in_array($col, ['timestamptz', 'timestamp with time zone'], true)) { + return ['type' => TableSchema::TYPE_TIMESTAMP_TIMEZONE, 'length' => null]; + } + if (strpos($col, 'timestamp') !== false) { + return ['type' => TableSchema::TYPE_TIMESTAMP_FRACTIONAL, 'length' => null]; + } + if (strpos($col, 'time') !== false) { + return ['type' => TableSchema::TYPE_TIME, 'length' => null]; + } + if ($col === 'serial' || $col === 'integer') { + return ['type' => TableSchema::TYPE_INTEGER, 'length' => 10]; + } + if ($col === 'bigserial' || $col === 'bigint') { + return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => 20]; + } + if ($col === 'smallint') { + return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => 5]; + } + if ($col === 'inet') { + return ['type' => TableSchema::TYPE_STRING, 'length' => 39]; + } + if ($col === 'uuid') { + return ['type' => TableSchema::TYPE_UUID, 'length' => null]; + } + if ($col === 'char') { + return ['type' => TableSchema::TYPE_CHAR, 'length' => $length]; + } + if (strpos($col, 'character') !== false) { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length]; + } + // money is 'string' as it includes arbitrary text content + // before the number value. + if (strpos($col, 'money') !== false || $col === 'string') { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length]; + } + if (strpos($col, 'text') !== false) { + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + if ($col === 'bytea') { + return ['type' => TableSchema::TYPE_BINARY, 'length' => null]; + } + if ($col === 'real' || strpos($col, 'double') !== false) { + return ['type' => TableSchema::TYPE_FLOAT, 'length' => null]; + } + if ( + strpos($col, 'numeric') !== false || + strpos($col, 'decimal') !== false + ) { + return ['type' => TableSchema::TYPE_DECIMAL, 'length' => null]; + } + + if (strpos($col, 'json') !== false) { + return ['type' => TableSchema::TYPE_JSON, 'length' => null]; + } + + $length = is_numeric($length) ? $length : null; + + return ['type' => TableSchema::TYPE_STRING, 'length' => $length]; + } + + /** + * @inheritDoc + */ + public function convertColumnDescription(TableSchema $schema, array $row): void + { + $field = $this->_convertColumn($row['type']); + + if ($field['type'] === TableSchema::TYPE_BOOLEAN) { + if ($row['default'] === 'true') { + $row['default'] = 1; + } + if ($row['default'] === 'false') { + $row['default'] = 0; + } + } + if (!empty($row['has_serial'])) { + $field['autoIncrement'] = true; + } + + $field += [ + 'default' => $this->_defaultValue($row['default']), + 'null' => $row['null'] === 'YES', + 'collate' => $row['collation_name'], + 'comment' => $row['comment'], + ]; + $field['length'] = $row['char_length'] ?: $field['length']; + + if ($field['type'] === 'numeric' || $field['type'] === 'decimal') { + $field['length'] = $row['column_precision']; + $field['precision'] = $row['column_scale'] ?: null; + } + + if ($field['type'] === TableSchema::TYPE_TIMESTAMP_FRACTIONAL) { + $field['precision'] = $row['datetime_precision']; + if ($field['precision'] === 0) { + $field['type'] = TableSchema::TYPE_TIMESTAMP; + } + } + + if ($field['type'] === TableSchema::TYPE_TIMESTAMP_TIMEZONE) { + $field['precision'] = $row['datetime_precision']; + } + + $schema->addColumn($row['name'], $field); + } + + /** + * Manipulate the default value. + * + * Postgres includes sequence data and casting information in default values. + * We need to remove those. + * + * @param string|int|null $default The default value. + * @return string|int|null + */ + protected function _defaultValue($default) + { + if (is_numeric($default) || $default === null) { + return $default; + } + // Sequences + if (strpos($default, 'nextval') === 0) { + return null; + } + + if (strpos($default, 'NULL::') === 0) { + return null; + } + + // Remove quotes and postgres casts + return preg_replace( + "/^'(.*)'(?:::.*)$/", + '$1', + $default + ); + } + + /** + * @inheritDoc + */ + public function describeIndexSql(string $tableName, array $config): array + { + $sql = 'SELECT + c2.relname, + a.attname, + i.indisprimary, + i.indisunique + FROM pg_catalog.pg_namespace n + INNER JOIN pg_catalog.pg_class c ON (n.oid = c.relnamespace) + INNER JOIN pg_catalog.pg_index i ON (c.oid = i.indrelid) + INNER JOIN pg_catalog.pg_class c2 ON (c2.oid = i.indexrelid) + INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = c.oid AND i.indrelid::regclass = a.attrelid::regclass) + WHERE n.nspname = ? + AND a.attnum = ANY(i.indkey) + AND c.relname = ? + ORDER BY i.indisprimary DESC, i.indisunique DESC, c.relname, a.attnum'; + + $schema = 'public'; + if (!empty($config['schema'])) { + $schema = $config['schema']; + } + + return [$sql, [$schema, $tableName]]; + } + + /** + * @inheritDoc + */ + public function convertIndexDescription(TableSchema $schema, array $row): void + { + $type = TableSchema::INDEX_INDEX; + $name = $row['relname']; + if ($row['indisprimary']) { + $name = $type = TableSchema::CONSTRAINT_PRIMARY; + } + if ($row['indisunique'] && $type === TableSchema::INDEX_INDEX) { + $type = TableSchema::CONSTRAINT_UNIQUE; + } + if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) { + $this->_convertConstraint($schema, $name, $type, $row); + + return; + } + $index = $schema->getIndex($name); + if (!$index) { + $index = [ + 'type' => $type, + 'columns' => [], + ]; + } + $index['columns'][] = $row['attname']; + $schema->addIndex($name, $index); + } + + /** + * Add/update a constraint into the schema object. + * + * @param \Cake\Database\Schema\TableSchema $schema The table to update. + * @param string $name The index name. + * @param string $type The index type. + * @param array $row The metadata record to update with. + * @return void + */ + protected function _convertConstraint(TableSchema $schema, string $name, string $type, array $row): void + { + $constraint = $schema->getConstraint($name); + if (!$constraint) { + $constraint = [ + 'type' => $type, + 'columns' => [], + ]; + } + $constraint['columns'][] = $row['attname']; + $schema->addConstraint($name, $constraint); + } + + /** + * @inheritDoc + */ + public function describeForeignKeySql(string $tableName, array $config): array + { + // phpcs:disable Generic.Files.LineLength + $sql = 'SELECT + c.conname AS name, + c.contype AS type, + a.attname AS column_name, + c.confmatchtype AS match_type, + c.confupdtype AS on_update, + c.confdeltype AS on_delete, + c.confrelid::regclass AS references_table, + ab.attname AS references_field + FROM pg_catalog.pg_namespace n + INNER JOIN pg_catalog.pg_class cl ON (n.oid = cl.relnamespace) + INNER JOIN pg_catalog.pg_constraint c ON (n.oid = c.connamespace) + INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = cl.oid AND c.conrelid = a.attrelid AND a.attnum = ANY(c.conkey)) + INNER JOIN pg_catalog.pg_attribute ab ON (a.attrelid = cl.oid AND c.confrelid = ab.attrelid AND ab.attnum = ANY(c.confkey)) + WHERE n.nspname = ? + AND cl.relname = ? + ORDER BY name, a.attnum, ab.attnum DESC'; + // phpcs:enable Generic.Files.LineLength + + $schema = empty($config['schema']) ? 'public' : $config['schema']; + + return [$sql, [$schema, $tableName]]; + } + + /** + * @inheritDoc + */ + public function convertForeignKeyDescription(TableSchema $schema, array $row): void + { + $data = [ + 'type' => TableSchema::CONSTRAINT_FOREIGN, + 'columns' => $row['column_name'], + 'references' => [$row['references_table'], $row['references_field']], + 'update' => $this->_convertOnClause($row['on_update']), + 'delete' => $this->_convertOnClause($row['on_delete']), + ]; + $schema->addConstraint($row['name'], $data); + } + + /** + * @inheritDoc + */ + protected function _convertOnClause(string $clause): string + { + if ($clause === 'r') { + return TableSchema::ACTION_RESTRICT; + } + if ($clause === 'a') { + return TableSchema::ACTION_NO_ACTION; + } + if ($clause === 'c') { + return TableSchema::ACTION_CASCADE; + } + + return TableSchema::ACTION_SET_NULL; + } + + /** + * @inheritDoc + */ + public function columnSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getColumn($name); + + $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name); + if ($sql !== null) { + return $sql; + } + + $out = $this->_driver->quoteIdentifier($name); + $typeMap = [ + TableSchema::TYPE_TINYINTEGER => ' SMALLINT', + TableSchema::TYPE_SMALLINTEGER => ' SMALLINT', + TableSchema::TYPE_BINARY_UUID => ' UUID', + TableSchema::TYPE_BOOLEAN => ' BOOLEAN', + TableSchema::TYPE_FLOAT => ' FLOAT', + TableSchema::TYPE_DECIMAL => ' DECIMAL', + TableSchema::TYPE_DATE => ' DATE', + TableSchema::TYPE_TIME => ' TIME', + TableSchema::TYPE_DATETIME => ' TIMESTAMP', + TableSchema::TYPE_DATETIME_FRACTIONAL => ' TIMESTAMP', + TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP', + TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMP', + TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMPTZ', + TableSchema::TYPE_UUID => ' UUID', + TableSchema::TYPE_CHAR => ' CHAR', + TableSchema::TYPE_JSON => ' JSONB', + ]; + + if (isset($typeMap[$data['type']])) { + $out .= $typeMap[$data['type']]; + } + + if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) { + $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' INTEGER' : ' BIGINT'; + if ($schema->getPrimaryKey() === [$name] || $data['autoIncrement'] === true) { + $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' SERIAL' : ' BIGSERIAL'; + unset($data['null'], $data['default']); + } + $out .= $type; + } + + if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) { + $out .= ' TEXT'; + } + if ($data['type'] === TableSchema::TYPE_BINARY) { + $out .= ' BYTEA'; + } + + if ($data['type'] === TableSchema::TYPE_CHAR) { + $out .= '(' . $data['length'] . ')'; + } + + if ( + $data['type'] === TableSchema::TYPE_STRING || + ( + $data['type'] === TableSchema::TYPE_TEXT && + $data['length'] === TableSchema::LENGTH_TINY + ) + ) { + $out .= ' VARCHAR'; + if (isset($data['length']) && $data['length'] !== '') { + $out .= '(' . $data['length'] . ')'; + } + } + + $hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING, TableSchema::TYPE_CHAR]; + if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') { + $out .= ' COLLATE "' . $data['collate'] . '"'; + } + + $hasPrecision = [ + TableSchema::TYPE_FLOAT, + TableSchema::TYPE_DATETIME, + TableSchema::TYPE_DATETIME_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP, + TableSchema::TYPE_TIMESTAMP_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP_TIMEZONE, + ]; + if (in_array($data['type'], $hasPrecision) && isset($data['precision'])) { + $out .= '(' . $data['precision'] . ')'; + } + + if ( + $data['type'] === TableSchema::TYPE_DECIMAL && + ( + isset($data['length']) || + isset($data['precision']) + ) + ) { + $out .= '(' . $data['length'] . ',' . (int)$data['precision'] . ')'; + } + + if (isset($data['null']) && $data['null'] === false) { + $out .= ' NOT NULL'; + } + + $datetimeTypes = [ + TableSchema::TYPE_DATETIME, + TableSchema::TYPE_DATETIME_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP, + TableSchema::TYPE_TIMESTAMP_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP_TIMEZONE, + ]; + if ( + isset($data['default']) && + in_array($data['type'], $datetimeTypes) && + strtolower($data['default']) === 'current_timestamp' + ) { + $out .= ' DEFAULT CURRENT_TIMESTAMP'; + } elseif (isset($data['default'])) { + $defaultValue = $data['default']; + if ($data['type'] === 'boolean') { + $defaultValue = (bool)$defaultValue; + } + $out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue); + } elseif (isset($data['null']) && $data['null'] !== false) { + $out .= ' DEFAULT NULL'; + } + + return $out; + } + + /** + * @inheritDoc + */ + public function addConstraintSql(TableSchema $schema): array + { + $sqlPattern = 'ALTER TABLE %s ADD %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + /** @var array $constraint */ + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name)); + } + } + + return $sql; + } + + /** + * @inheritDoc + */ + public function dropConstraintSql(TableSchema $schema): array + { + $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + /** @var array $constraint */ + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $constraintName = $this->_driver->quoteIdentifier($name); + $sql[] = sprintf($sqlPattern, $tableName, $constraintName); + } + } + + return $sql; + } + + /** + * @inheritDoc + */ + public function indexSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getIndex($name); + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf( + 'CREATE INDEX %s ON %s (%s)', + $this->_driver->quoteIdentifier($name), + $this->_driver->quoteIdentifier($schema->name()), + implode(', ', $columns) + ); + } + + /** + * @inheritDoc + */ + public function constraintSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getConstraint($name); + $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name); + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) { + $out = 'PRIMARY KEY'; + } + if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) { + $out .= ' UNIQUE'; + } + + return $this->_keySql($out, $data); + } + + /** + * Helper method for generating key SQL snippets. + * + * @param string $prefix The key prefix + * @param array $data Key data. + * @return string + */ + protected function _keySql(string $prefix, array $data): string + { + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + return $prefix . sprintf( + ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s DEFERRABLE INITIALLY IMMEDIATE', + implode(', ', $columns), + $this->_driver->quoteIdentifier($data['references'][0]), + $this->_convertConstraintColumns($data['references'][1]), + $this->_foreignOnClause($data['update']), + $this->_foreignOnClause($data['delete']) + ); + } + + return $prefix . ' (' . implode(', ', $columns) . ')'; + } + + /** + * @inheritDoc + */ + public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array + { + $content = array_merge($columns, $constraints); + $content = implode(",\n", array_filter($content)); + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' '; + $out = []; + $out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content); + foreach ($indexes as $index) { + $out[] = $index; + } + foreach ($schema->columns() as $column) { + $columnData = $schema->getColumn($column); + if (isset($columnData['comment'])) { + $out[] = sprintf( + 'COMMENT ON COLUMN %s.%s IS %s', + $tableName, + $this->_driver->quoteIdentifier($column), + $this->_driver->schemaValue($columnData['comment']) + ); + } + } + + return $out; + } + + /** + * @inheritDoc + */ + public function truncateTableSql(TableSchema $schema): array + { + $name = $this->_driver->quoteIdentifier($schema->name()); + + return [ + sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name), + ]; + } + + /** + * Generate the SQL to drop a table. + * + * @param \Cake\Database\Schema\TableSchema $schema Table instance + * @return array SQL statements to drop a table. + */ + public function dropTableSql(TableSchema $schema): array + { + $sql = sprintf( + 'DROP TABLE %s CASCADE', + $this->_driver->quoteIdentifier($schema->name()) + ); + + return [$sql]; + } +} + +// phpcs:disable +// Add backwards compatible alias. +class_alias('Cake\Database\Schema\PostgresSchemaDialect', 'Cake\Database\Schema\PostgresSchema'); +// phpcs:enable diff --git a/vendor/cakephp/database/Schema/SchemaDialect.php b/vendor/cakephp/database/Schema/SchemaDialect.php new file mode 100644 index 0000000..1d887d7 --- /dev/null +++ b/vendor/cakephp/database/Schema/SchemaDialect.php @@ -0,0 +1,342 @@ +connect(); + $this->_driver = $driver; + } + + /** + * Generate an ON clause for a foreign key. + * + * @param string $on The on clause + * @return string + */ + protected function _foreignOnClause(string $on): string + { + if ($on === TableSchema::ACTION_SET_NULL) { + return 'SET NULL'; + } + if ($on === TableSchema::ACTION_SET_DEFAULT) { + return 'SET DEFAULT'; + } + if ($on === TableSchema::ACTION_CASCADE) { + return 'CASCADE'; + } + if ($on === TableSchema::ACTION_RESTRICT) { + return 'RESTRICT'; + } + if ($on === TableSchema::ACTION_NO_ACTION) { + return 'NO ACTION'; + } + + throw new InvalidArgumentException('Invalid value for "on": ' . $on); + } + + /** + * Convert string on clauses to the abstract ones. + * + * @param string $clause The on clause to convert. + * @return string + */ + protected function _convertOnClause(string $clause): string + { + if ($clause === 'CASCADE' || $clause === 'RESTRICT') { + return strtolower($clause); + } + if ($clause === 'NO ACTION') { + return TableSchema::ACTION_NO_ACTION; + } + + return TableSchema::ACTION_SET_NULL; + } + + /** + * Convert foreign key constraints references to a valid + * stringified list + * + * @param array|string $references The referenced columns of a foreign key constraint statement + * @return string + */ + protected function _convertConstraintColumns($references): string + { + if (is_string($references)) { + return $this->_driver->quoteIdentifier($references); + } + + return implode(', ', array_map( + [$this->_driver, 'quoteIdentifier'], + $references + )); + } + + /** + * Tries to use a matching database type to generate the SQL + * fragment for a single column in a table. + * + * @param string $columnType The column type. + * @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema instance the column is in. + * @param string $column The name of the column. + * @return string|null An SQL fragment, or `null` in case no corresponding type was found or the type didn't provide + * custom column SQL. + */ + protected function _getTypeSpecificColumnSql( + string $columnType, + TableSchemaInterface $schema, + string $column + ): ?string { + if (!TypeFactory::getMap($columnType)) { + return null; + } + + $type = TypeFactory::build($columnType); + if (!($type instanceof ColumnSchemaAwareInterface)) { + return null; + } + + return $type->getColumnSql($schema, $column, $this->_driver); + } + + /** + * Tries to use a matching database type to convert a SQL column + * definition to an abstract type definition. + * + * @param string $columnType The column type. + * @param array $definition The column definition. + * @return array|null Array of column information, or `null` in case no corresponding type was found or the type + * didn't provide custom column information. + */ + protected function _applyTypeSpecificColumnConversion(string $columnType, array $definition): ?array + { + if (!TypeFactory::getMap($columnType)) { + return null; + } + + $type = TypeFactory::build($columnType); + if (!($type instanceof ColumnSchemaAwareInterface)) { + return null; + } + + return $type->convertColumnDefinition($definition, $this->_driver); + } + + /** + * Generate the SQL to drop a table. + * + * @param \Cake\Database\Schema\TableSchema $schema Schema instance + * @return array SQL statements to drop a table. + */ + public function dropTableSql(TableSchema $schema): array + { + $sql = sprintf( + 'DROP TABLE %s', + $this->_driver->quoteIdentifier($schema->name()) + ); + + return [$sql]; + } + + /** + * Generate the SQL to list the tables. + * + * @param array $config The connection configuration to use for + * getting tables from. + * @return array An array of (sql, params) to execute. + */ + abstract public function listTablesSql(array $config): array; + + /** + * Generate the SQL to describe a table. + * + * @param string $tableName The table name to get information on. + * @param array $config The connection configuration. + * @return array An array of (sql, params) to execute. + */ + abstract public function describeColumnSql(string $tableName, array $config): array; + + /** + * Generate the SQL to describe the indexes in a table. + * + * @param string $tableName The table name to get information on. + * @param array $config The connection configuration. + * @return array An array of (sql, params) to execute. + */ + abstract public function describeIndexSql(string $tableName, array $config): array; + + /** + * Generate the SQL to describe the foreign keys in a table. + * + * @param string $tableName The table name to get information on. + * @param array $config The connection configuration. + * @return array An array of (sql, params) to execute. + */ + abstract public function describeForeignKeySql(string $tableName, array $config): array; + + /** + * Generate the SQL to describe table options + * + * @param string $tableName Table name. + * @param array $config The connection configuration. + * @return array SQL statements to get options for a table. + */ + public function describeOptionsSql(string $tableName, array $config): array + { + return ['', '']; + } + + /** + * Convert field description results into abstract schema fields. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object to append fields to. + * @param array $row The row data from `describeColumnSql`. + * @return void + */ + abstract public function convertColumnDescription(TableSchema $schema, array $row): void; + + /** + * Convert an index description results into abstract schema indexes or constraints. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object to append + * an index or constraint to. + * @param array $row The row data from `describeIndexSql`. + * @return void + */ + abstract public function convertIndexDescription(TableSchema $schema, array $row): void; + + /** + * Convert a foreign key description into constraints on the Table object. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object to append + * a constraint to. + * @param array $row The row data from `describeForeignKeySql`. + * @return void + */ + abstract public function convertForeignKeyDescription(TableSchema $schema, array $row): void; + + /** + * Convert options data into table options. + * + * @param \Cake\Database\Schema\TableSchema $schema Table instance. + * @param array $row The row of data. + * @return void + */ + public function convertOptionsDescription(TableSchema $schema, array $row): void + { + } + + /** + * Generate the SQL to create a table. + * + * @param \Cake\Database\Schema\TableSchema $schema Table instance. + * @param array $columns The columns to go inside the table. + * @param array $constraints The constraints for the table. + * @param array $indexes The indexes for the table. + * @return array SQL statements to create a table. + */ + abstract public function createTableSql( + TableSchema $schema, + array $columns, + array $constraints, + array $indexes + ): array; + + /** + * Generate the SQL fragment for a single column in a table. + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in. + * @param string $name The name of the column. + * @return string SQL fragment. + */ + abstract public function columnSql(TableSchema $schema, string $name): string; + + /** + * Generate the SQL queries needed to add foreign key constraints to the table + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are. + * @return array SQL fragment. + */ + abstract public function addConstraintSql(TableSchema $schema): array; + + /** + * Generate the SQL queries needed to drop foreign key constraints from the table + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are. + * @return array SQL fragment. + */ + abstract public function dropConstraintSql(TableSchema $schema): array; + + /** + * Generate the SQL fragments for defining table constraints. + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in. + * @param string $name The name of the column. + * @return string SQL fragment. + */ + abstract public function constraintSql(TableSchema $schema, string $name): string; + + /** + * Generate the SQL fragment for a single index in a table. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object the column is in. + * @param string $name The name of the column. + * @return string SQL fragment. + */ + abstract public function indexSql(TableSchema $schema, string $name): string; + + /** + * Generate the SQL to truncate a table. + * + * @param \Cake\Database\Schema\TableSchema $schema Table instance. + * @return array SQL statements to truncate a table. + */ + abstract public function truncateTableSql(TableSchema $schema): array; +} + +// phpcs:disable +// Add backwards compatible alias. +class_alias('Cake\Database\Schema\SchemaDialect', 'Cake\Database\Schema\BaseSchema'); +// phpcs:enable diff --git a/vendor/cakephp/database/Schema/SqlGeneratorInterface.php b/vendor/cakephp/database/Schema/SqlGeneratorInterface.php new file mode 100644 index 0000000..3acaf27 --- /dev/null +++ b/vendor/cakephp/database/Schema/SqlGeneratorInterface.php @@ -0,0 +1,72 @@ + + */ + protected $_constraintsIdMap = []; + + /** + * Whether there is any table in this connection to SQLite containing sequences. + * + * @var bool + */ + protected $_hasSequences; + + /** + * Convert a column definition to the abstract types. + * + * The returned type will be a type that + * Cake\Database\TypeFactory can handle. + * + * @param string $column The column type + length + * @throws \Cake\Database\Exception\DatabaseException when unable to parse column type + * @return array Array of column information. + */ + protected function _convertColumn(string $column): array + { + if ($column === '') { + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + + preg_match('/(unsigned)?\s*([a-z]+)(?:\(([0-9,]+)\))?/i', $column, $matches); + if (empty($matches)) { + throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column)); + } + + $unsigned = false; + if (strtolower($matches[1]) === 'unsigned') { + $unsigned = true; + } + + $col = strtolower($matches[2]); + $length = $precision = $scale = null; + if (isset($matches[3])) { + $length = $matches[3]; + if (strpos($length, ',') !== false) { + [$length, $precision] = explode(',', $length); + } + $length = (int)$length; + $precision = (int)$precision; + } + + $type = $this->_applyTypeSpecificColumnConversion( + $col, + compact('length', 'precision', 'scale') + ); + if ($type !== null) { + return $type; + } + + if ($col === 'bigint') { + return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if ($col === 'smallint') { + return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if ($col === 'tinyint') { + return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if (strpos($col, 'int') !== false) { + return ['type' => TableSchema::TYPE_INTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if (strpos($col, 'decimal') !== false) { + return [ + 'type' => TableSchema::TYPE_DECIMAL, + 'length' => $length, + 'precision' => $precision, + 'unsigned' => $unsigned, + ]; + } + if (in_array($col, ['float', 'real', 'double'])) { + return [ + 'type' => TableSchema::TYPE_FLOAT, + 'length' => $length, + 'precision' => $precision, + 'unsigned' => $unsigned, + ]; + } + + if (strpos($col, 'boolean') !== false) { + return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null]; + } + + if ($col === 'char' && $length === 36) { + return ['type' => TableSchema::TYPE_UUID, 'length' => null]; + } + if ($col === 'char') { + return ['type' => TableSchema::TYPE_CHAR, 'length' => $length]; + } + if (strpos($col, 'char') !== false) { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length]; + } + + if ($col === 'binary' && $length === 16) { + return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null]; + } + if (in_array($col, ['blob', 'clob', 'binary', 'varbinary'])) { + return ['type' => TableSchema::TYPE_BINARY, 'length' => $length]; + } + + $datetimeTypes = [ + 'date', + 'time', + 'timestamp', + 'timestampfractional', + 'timestamptimezone', + 'datetime', + 'datetimefractional', + ]; + if (in_array($col, $datetimeTypes)) { + return ['type' => $col, 'length' => null]; + } + + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + + /** + * @inheritDoc + */ + public function listTablesSql(array $config): array + { + return [ + 'SELECT name FROM sqlite_master WHERE type="table" ' . + 'AND name != "sqlite_sequence" ORDER BY name', + [], + ]; + } + + /** + * @inheritDoc + */ + public function describeColumnSql(string $tableName, array $config): array + { + $sql = sprintf( + 'PRAGMA table_info(%s)', + $this->_driver->quoteIdentifier($tableName) + ); + + return [$sql, []]; + } + + /** + * @inheritDoc + */ + public function convertColumnDescription(TableSchema $schema, array $row): void + { + $field = $this->_convertColumn($row['type']); + $field += [ + 'null' => !$row['notnull'], + 'default' => $this->_defaultValue($row['dflt_value']), + ]; + $primary = $schema->getConstraint('primary'); + + if ($row['pk'] && empty($primary)) { + $field['null'] = false; + $field['autoIncrement'] = true; + } + + // SQLite does not support autoincrement on composite keys. + if ($row['pk'] && !empty($primary)) { + $existingColumn = $primary['columns'][0]; + /** @psalm-suppress PossiblyNullOperand */ + $schema->addColumn($existingColumn, ['autoIncrement' => null] + $schema->getColumn($existingColumn)); + } + + $schema->addColumn($row['name'], $field); + if ($row['pk']) { + $constraint = (array)$schema->getConstraint('primary') + [ + 'type' => TableSchema::CONSTRAINT_PRIMARY, + 'columns' => [], + ]; + $constraint['columns'] = array_merge($constraint['columns'], [$row['name']]); + $schema->addConstraint('primary', $constraint); + } + } + + /** + * Manipulate the default value. + * + * Sqlite includes quotes and bared NULLs in default values. + * We need to remove those. + * + * @param string|int|null $default The default value. + * @return string|int|null + */ + protected function _defaultValue($default) + { + if ($default === 'NULL' || $default === null) { + return null; + } + + // Remove quotes + if (is_string($default) && preg_match("/^'(.*)'$/", $default, $matches)) { + return str_replace("''", "'", $matches[1]); + } + + return $default; + } + + /** + * @inheritDoc + */ + public function describeIndexSql(string $tableName, array $config): array + { + $sql = sprintf( + 'PRAGMA index_list(%s)', + $this->_driver->quoteIdentifier($tableName) + ); + + return [$sql, []]; + } + + /** + * {@inheritDoc} + * + * Since SQLite does not have a way to get metadata about all indexes at once, + * additional queries are done here. Sqlite constraint names are not + * stable, and the names for constraints will not match those used to create + * the table. This is a limitation in Sqlite's metadata features. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object to append + * an index or constraint to. + * @param array $row The row data from `describeIndexSql`. + * @return void + */ + public function convertIndexDescription(TableSchema $schema, array $row): void + { + $sql = sprintf( + 'PRAGMA index_info(%s)', + $this->_driver->quoteIdentifier($row['name']) + ); + $statement = $this->_driver->prepare($sql); + $statement->execute(); + $columns = []; + /** @psalm-suppress PossiblyFalseIterator */ + foreach ($statement->fetchAll('assoc') as $column) { + $columns[] = $column['name']; + } + $statement->closeCursor(); + if ($row['unique']) { + $schema->addConstraint($row['name'], [ + 'type' => TableSchema::CONSTRAINT_UNIQUE, + 'columns' => $columns, + ]); + } else { + $schema->addIndex($row['name'], [ + 'type' => TableSchema::INDEX_INDEX, + 'columns' => $columns, + ]); + } + } + + /** + * @inheritDoc + */ + public function describeForeignKeySql(string $tableName, array $config): array + { + $sql = sprintf('PRAGMA foreign_key_list(%s)', $this->_driver->quoteIdentifier($tableName)); + + return [$sql, []]; + } + + /** + * @inheritDoc + */ + public function convertForeignKeyDescription(TableSchema $schema, array $row): void + { + $name = $row['from'] . '_fk'; + + $update = $row['on_update'] ?? ''; + $delete = $row['on_delete'] ?? ''; + $data = [ + 'type' => TableSchema::CONSTRAINT_FOREIGN, + 'columns' => [$row['from']], + 'references' => [$row['table'], $row['to']], + 'update' => $this->_convertOnClause($update), + 'delete' => $this->_convertOnClause($delete), + ]; + + if (isset($this->_constraintsIdMap[$schema->name()][$row['id']])) { + $name = $this->_constraintsIdMap[$schema->name()][$row['id']]; + } else { + $this->_constraintsIdMap[$schema->name()][$row['id']] = $name; + } + + $schema->addConstraint($name, $data); + } + + /** + * {@inheritDoc} + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in. + * @param string $name The name of the column. + * @return string SQL fragment. + * @throws \Cake\Database\Exception\DatabaseException when the column type is unknown + */ + public function columnSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getColumn($name); + + $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name); + if ($sql !== null) { + return $sql; + } + + $typeMap = [ + TableSchema::TYPE_BINARY_UUID => ' BINARY(16)', + TableSchema::TYPE_UUID => ' CHAR(36)', + TableSchema::TYPE_CHAR => ' CHAR', + TableSchema::TYPE_TINYINTEGER => ' TINYINT', + TableSchema::TYPE_SMALLINTEGER => ' SMALLINT', + TableSchema::TYPE_INTEGER => ' INTEGER', + TableSchema::TYPE_BIGINTEGER => ' BIGINT', + TableSchema::TYPE_BOOLEAN => ' BOOLEAN', + TableSchema::TYPE_FLOAT => ' FLOAT', + TableSchema::TYPE_DECIMAL => ' DECIMAL', + TableSchema::TYPE_DATE => ' DATE', + TableSchema::TYPE_TIME => ' TIME', + TableSchema::TYPE_DATETIME => ' DATETIME', + TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIMEFRACTIONAL', + TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP', + TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMPFRACTIONAL', + TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMPTIMEZONE', + TableSchema::TYPE_JSON => ' TEXT', + ]; + + $out = $this->_driver->quoteIdentifier($name); + $hasUnsigned = [ + TableSchema::TYPE_TINYINTEGER, + TableSchema::TYPE_SMALLINTEGER, + TableSchema::TYPE_INTEGER, + TableSchema::TYPE_BIGINTEGER, + TableSchema::TYPE_FLOAT, + TableSchema::TYPE_DECIMAL, + ]; + + if ( + in_array($data['type'], $hasUnsigned, true) && + isset($data['unsigned']) && + $data['unsigned'] === true + ) { + if ($data['type'] !== TableSchema::TYPE_INTEGER || $schema->getPrimaryKey() !== [$name]) { + $out .= ' UNSIGNED'; + } + } + + if (isset($typeMap[$data['type']])) { + $out .= $typeMap[$data['type']]; + } + + if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) { + $out .= ' TEXT'; + } + + if ($data['type'] === TableSchema::TYPE_CHAR) { + $out .= '(' . $data['length'] . ')'; + } + + if ( + $data['type'] === TableSchema::TYPE_STRING || + ( + $data['type'] === TableSchema::TYPE_TEXT && + $data['length'] === TableSchema::LENGTH_TINY + ) + ) { + $out .= ' VARCHAR'; + + if (isset($data['length'])) { + $out .= '(' . $data['length'] . ')'; + } + } + + if ($data['type'] === TableSchema::TYPE_BINARY) { + if (isset($data['length'])) { + $out .= ' BLOB(' . $data['length'] . ')'; + } else { + $out .= ' BLOB'; + } + } + + $integerTypes = [ + TableSchema::TYPE_TINYINTEGER, + TableSchema::TYPE_SMALLINTEGER, + TableSchema::TYPE_INTEGER, + ]; + if ( + in_array($data['type'], $integerTypes, true) && + isset($data['length']) && + $schema->getPrimaryKey() !== [$name] + ) { + $out .= '(' . (int)$data['length'] . ')'; + } + + $hasPrecision = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL]; + if ( + in_array($data['type'], $hasPrecision, true) && + ( + isset($data['length']) || + isset($data['precision']) + ) + ) { + $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')'; + } + + if (isset($data['null']) && $data['null'] === false) { + $out .= ' NOT NULL'; + } + + if ($data['type'] === TableSchema::TYPE_INTEGER && $schema->getPrimaryKey() === [$name]) { + $out .= ' PRIMARY KEY AUTOINCREMENT'; + } + + $timestampTypes = [ + TableSchema::TYPE_DATETIME, + TableSchema::TYPE_DATETIME_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP, + TableSchema::TYPE_TIMESTAMP_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP_TIMEZONE, + ]; + if (isset($data['null']) && $data['null'] === true && in_array($data['type'], $timestampTypes, true)) { + $out .= ' DEFAULT NULL'; + } + if (isset($data['default'])) { + $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']); + } + + return $out; + } + + /** + * {@inheritDoc} + * + * Note integer primary keys will return ''. This is intentional as Sqlite requires + * that integer primary keys be defined in the column definition. + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in. + * @param string $name The name of the column. + * @return string SQL fragment. + */ + public function constraintSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getConstraint($name); + /** @psalm-suppress PossiblyNullArrayAccess */ + if ( + $data['type'] === TableSchema::CONSTRAINT_PRIMARY && + count($data['columns']) === 1 && + $schema->getColumn($data['columns'][0])['type'] === TableSchema::TYPE_INTEGER + ) { + return ''; + } + $clause = ''; + $type = ''; + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) { + $type = 'PRIMARY KEY'; + } + if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) { + $type = 'UNIQUE'; + } + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $type = 'FOREIGN KEY'; + + $clause = sprintf( + ' REFERENCES %s (%s) ON UPDATE %s ON DELETE %s', + $this->_driver->quoteIdentifier($data['references'][0]), + $this->_convertConstraintColumns($data['references'][1]), + $this->_foreignOnClause($data['update']), + $this->_foreignOnClause($data['delete']) + ); + } + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf( + 'CONSTRAINT %s %s (%s)%s', + $this->_driver->quoteIdentifier($name), + $type, + implode(', ', $columns), + $clause + ); + } + + /** + * {@inheritDoc} + * + * SQLite can not properly handle adding a constraint to an existing table. + * This method is no-op + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are. + * @return array SQL fragment. + */ + public function addConstraintSql(TableSchema $schema): array + { + return []; + } + + /** + * {@inheritDoc} + * + * SQLite can not properly handle dropping a constraint to an existing table. + * This method is no-op + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are. + * @return array SQL fragment. + */ + public function dropConstraintSql(TableSchema $schema): array + { + return []; + } + + /** + * @inheritDoc + */ + public function indexSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getIndex($name); + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf( + 'CREATE INDEX %s ON %s (%s)', + $this->_driver->quoteIdentifier($name), + $this->_driver->quoteIdentifier($schema->name()), + implode(', ', $columns) + ); + } + + /** + * @inheritDoc + */ + public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array + { + $lines = array_merge($columns, $constraints); + $content = implode(",\n", array_filter($lines)); + $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' '; + $table = sprintf("CREATE%sTABLE \"%s\" (\n%s\n)", $temporary, $schema->name(), $content); + $out = [$table]; + foreach ($indexes as $index) { + $out[] = $index; + } + + return $out; + } + + /** + * @inheritDoc + */ + public function truncateTableSql(TableSchema $schema): array + { + $name = $schema->name(); + $sql = []; + if ($this->hasSequences()) { + $sql[] = sprintf('DELETE FROM sqlite_sequence WHERE name="%s"', $name); + } + + $sql[] = sprintf('DELETE FROM "%s"', $name); + + return $sql; + } + + /** + * Returns whether there is any table in this connection to SQLite containing + * sequences + * + * @return bool + */ + public function hasSequences(): bool + { + $result = $this->_driver->prepare( + 'SELECT 1 FROM sqlite_master WHERE name = "sqlite_sequence"' + ); + $result->execute(); + $this->_hasSequences = (bool)$result->rowCount(); + $result->closeCursor(); + + return $this->_hasSequences; + } +} + +// phpcs:disable +// Add backwards compatible alias. +class_alias('Cake\Database\Schema\SqliteSchemaDialect', 'Cake\Database\Schema\SqliteSchema'); +// phpcs:enable diff --git a/vendor/cakephp/database/Schema/SqlserverSchema.php b/vendor/cakephp/database/Schema/SqlserverSchema.php new file mode 100644 index 0000000..dbe584e --- /dev/null +++ b/vendor/cakephp/database/Schema/SqlserverSchema.php @@ -0,0 +1,5 @@ + Array of column information. + * @link https://technet.microsoft.com/en-us/library/ms187752.aspx + */ + protected function _convertColumn( + string $col, + ?int $length = null, + ?int $precision = null, + ?int $scale = null + ): array { + $col = strtolower($col); + + $type = $this->_applyTypeSpecificColumnConversion( + $col, + compact('length', 'precision', 'scale') + ); + if ($type !== null) { + return $type; + } + + if (in_array($col, ['date', 'time'])) { + return ['type' => $col, 'length' => null]; + } + + if ($col === 'datetime') { + // datetime cannot parse more than 3 digits of precision and isn't accurate + return ['type' => TableSchema::TYPE_DATETIME, 'length' => null]; + } + if (strpos($col, 'datetime') !== false) { + $typeName = TableSchema::TYPE_DATETIME; + if ($scale > 0) { + $typeName = TableSchema::TYPE_DATETIME_FRACTIONAL; + } + + return ['type' => $typeName, 'length' => null, 'precision' => $scale]; + } + + if ($col === 'char') { + return ['type' => TableSchema::TYPE_CHAR, 'length' => $length]; + } + + if ($col === 'tinyint') { + return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $precision ?: 3]; + } + if ($col === 'smallint') { + return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $precision ?: 5]; + } + if ($col === 'int' || $col === 'integer') { + return ['type' => TableSchema::TYPE_INTEGER, 'length' => $precision ?: 10]; + } + if ($col === 'bigint') { + return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $precision ?: 20]; + } + if ($col === 'bit') { + return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null]; + } + if ( + strpos($col, 'numeric') !== false || + strpos($col, 'money') !== false || + strpos($col, 'decimal') !== false + ) { + return ['type' => TableSchema::TYPE_DECIMAL, 'length' => $precision, 'precision' => $scale]; + } + + if ($col === 'real' || $col === 'float') { + return ['type' => TableSchema::TYPE_FLOAT, 'length' => null]; + } + // SqlServer schema reflection returns double length for unicode + // columns because internally it uses UTF16/UCS2 + if ($col === 'nvarchar' || $col === 'nchar' || $col === 'ntext') { + $length /= 2; + } + if (strpos($col, 'varchar') !== false && $length < 0) { + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + + if (strpos($col, 'varchar') !== false) { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length ?: 255]; + } + + if (strpos($col, 'char') !== false) { + return ['type' => TableSchema::TYPE_CHAR, 'length' => $length]; + } + + if (strpos($col, 'text') !== false) { + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + + if ($col === 'image' || strpos($col, 'binary') !== false) { + // -1 is the value for MAX which we treat as a 'long' binary + if ($length == -1) { + $length = TableSchema::LENGTH_LONG; + } + + return ['type' => TableSchema::TYPE_BINARY, 'length' => $length]; + } + + if ($col === 'uniqueidentifier') { + return ['type' => TableSchema::TYPE_UUID]; + } + + return ['type' => TableSchema::TYPE_STRING, 'length' => null]; + } + + /** + * @inheritDoc + */ + public function convertColumnDescription(TableSchema $schema, array $row): void + { + $field = $this->_convertColumn( + $row['type'], + $row['char_length'] !== null ? (int)$row['char_length'] : null, + $row['precision'] !== null ? (int)$row['precision'] : null, + $row['scale'] !== null ? (int)$row['scale'] : null + ); + + if (!empty($row['autoincrement'])) { + $field['autoIncrement'] = true; + } + + $field += [ + 'null' => $row['null'] === '1', + 'default' => $this->_defaultValue($field['type'], $row['default']), + 'collate' => $row['collation_name'], + ]; + $schema->addColumn($row['name'], $field); + } + + /** + * Manipulate the default value. + * + * Removes () wrapping default values, extracts strings from + * N'' wrappers and collation text and converts NULL strings. + * + * @param string $type The schema type + * @param string|null $default The default value. + * @return string|int|null + */ + protected function _defaultValue($type, $default) + { + if ($default === null) { + return null; + } + + // remove () surrounding value (NULL) but leave () at the end of functions + // integers might have two ((0)) wrapping value + if (preg_match('/^\(+(.*?(\(\))?)\)+$/', $default, $matches)) { + $default = $matches[1]; + } + + if ($default === 'NULL') { + return null; + } + + if ($type === TableSchema::TYPE_BOOLEAN) { + return (int)$default; + } + + // Remove quotes + if (preg_match("/^\(?N?'(.*)'\)?/", $default, $matches)) { + return str_replace("''", "'", $matches[1]); + } + + return $default; + } + + /** + * @inheritDoc + */ + public function describeIndexSql(string $tableName, array $config): array + { + $sql = "SELECT + I.[name] AS [index_name], + IC.[index_column_id] AS [index_order], + AC.[name] AS [column_name], + I.[is_unique], I.[is_primary_key], + I.[is_unique_constraint] + FROM sys.[tables] AS T + INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id] + INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id] + INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id] AND I.[index_id] = IC.[index_id] + INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] AND IC.[column_id] = AC.[column_id] + WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP' AND T.[name] = ? AND S.[name] = ? + ORDER BY I.[index_id], IC.[index_column_id]"; + + $schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema']; + + return [$sql, [$tableName, $schema]]; + } + + /** + * @inheritDoc + */ + public function convertIndexDescription(TableSchema $schema, array $row): void + { + $type = TableSchema::INDEX_INDEX; + $name = $row['index_name']; + if ($row['is_primary_key']) { + $name = $type = TableSchema::CONSTRAINT_PRIMARY; + } + if ($row['is_unique_constraint'] && $type === TableSchema::INDEX_INDEX) { + $type = TableSchema::CONSTRAINT_UNIQUE; + } + + if ($type === TableSchema::INDEX_INDEX) { + $existing = $schema->getIndex($name); + } else { + $existing = $schema->getConstraint($name); + } + + $columns = [$row['column_name']]; + if (!empty($existing)) { + $columns = array_merge($existing['columns'], $columns); + } + + if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) { + $schema->addConstraint($name, [ + 'type' => $type, + 'columns' => $columns, + ]); + + return; + } + $schema->addIndex($name, [ + 'type' => $type, + 'columns' => $columns, + ]); + } + + /** + * @inheritDoc + */ + public function describeForeignKeySql(string $tableName, array $config): array + { + // phpcs:disable Generic.Files.LineLength + $sql = 'SELECT FK.[name] AS [foreign_key_name], FK.[delete_referential_action_desc] AS [delete_type], + FK.[update_referential_action_desc] AS [update_type], C.name AS [column], RT.name AS [reference_table], + RC.name AS [reference_column] + FROM sys.foreign_keys FK + INNER JOIN sys.foreign_key_columns FKC ON FKC.constraint_object_id = FK.object_id + INNER JOIN sys.tables T ON T.object_id = FKC.parent_object_id + INNER JOIN sys.tables RT ON RT.object_id = FKC.referenced_object_id + INNER JOIN sys.schemas S ON S.schema_id = T.schema_id AND S.schema_id = RT.schema_id + INNER JOIN sys.columns C ON C.column_id = FKC.parent_column_id AND C.object_id = FKC.parent_object_id + INNER JOIN sys.columns RC ON RC.column_id = FKC.referenced_column_id AND RC.object_id = FKC.referenced_object_id + WHERE FK.is_ms_shipped = 0 AND T.name = ? AND S.name = ? + ORDER BY FKC.constraint_column_id'; + // phpcs:enable Generic.Files.LineLength + + $schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema']; + + return [$sql, [$tableName, $schema]]; + } + + /** + * @inheritDoc + */ + public function convertForeignKeyDescription(TableSchema $schema, array $row): void + { + $data = [ + 'type' => TableSchema::CONSTRAINT_FOREIGN, + 'columns' => [$row['column']], + 'references' => [$row['reference_table'], $row['reference_column']], + 'update' => $this->_convertOnClause($row['update_type']), + 'delete' => $this->_convertOnClause($row['delete_type']), + ]; + $name = $row['foreign_key_name']; + $schema->addConstraint($name, $data); + } + + /** + * @inheritDoc + */ + protected function _foreignOnClause(string $on): string + { + $parent = parent::_foreignOnClause($on); + + return $parent === 'RESTRICT' ? parent::_foreignOnClause(TableSchema::ACTION_NO_ACTION) : $parent; + } + + /** + * @inheritDoc + */ + protected function _convertOnClause(string $clause): string + { + switch ($clause) { + case 'NO_ACTION': + return TableSchema::ACTION_NO_ACTION; + case 'CASCADE': + return TableSchema::ACTION_CASCADE; + case 'SET_NULL': + return TableSchema::ACTION_SET_NULL; + case 'SET_DEFAULT': + return TableSchema::ACTION_SET_DEFAULT; + } + + return TableSchema::ACTION_SET_NULL; + } + + /** + * @inheritDoc + */ + public function columnSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getColumn($name); + + $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name); + if ($sql !== null) { + return $sql; + } + + $out = $this->_driver->quoteIdentifier($name); + $typeMap = [ + TableSchema::TYPE_TINYINTEGER => ' TINYINT', + TableSchema::TYPE_SMALLINTEGER => ' SMALLINT', + TableSchema::TYPE_INTEGER => ' INTEGER', + TableSchema::TYPE_BIGINTEGER => ' BIGINT', + TableSchema::TYPE_BINARY_UUID => ' UNIQUEIDENTIFIER', + TableSchema::TYPE_BOOLEAN => ' BIT', + TableSchema::TYPE_CHAR => ' NCHAR', + TableSchema::TYPE_FLOAT => ' FLOAT', + TableSchema::TYPE_DECIMAL => ' DECIMAL', + TableSchema::TYPE_DATE => ' DATE', + TableSchema::TYPE_TIME => ' TIME', + TableSchema::TYPE_DATETIME => ' DATETIME2', + TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIME2', + TableSchema::TYPE_TIMESTAMP => ' DATETIME2', + TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' DATETIME2', + TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' DATETIME2', + TableSchema::TYPE_UUID => ' UNIQUEIDENTIFIER', + TableSchema::TYPE_JSON => ' NVARCHAR(MAX)', + ]; + + if (isset($typeMap[$data['type']])) { + $out .= $typeMap[$data['type']]; + } + + if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) { + if ($schema->getPrimaryKey() === [$name] || $data['autoIncrement'] === true) { + unset($data['null'], $data['default']); + $out .= ' IDENTITY(1, 1)'; + } + } + + if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) { + $out .= ' NVARCHAR(MAX)'; + } + + if ($data['type'] === TableSchema::TYPE_CHAR) { + $out .= '(' . $data['length'] . ')'; + } + + if ($data['type'] === TableSchema::TYPE_BINARY) { + if ( + !isset($data['length']) + || in_array($data['length'], [TableSchema::LENGTH_MEDIUM, TableSchema::LENGTH_LONG], true) + ) { + $data['length'] = 'MAX'; + } + + if ($data['length'] === 1) { + $out .= ' BINARY(1)'; + } else { + $out .= ' VARBINARY'; + + $out .= sprintf('(%s)', $data['length']); + } + } + + if ( + $data['type'] === TableSchema::TYPE_STRING || + ( + $data['type'] === TableSchema::TYPE_TEXT && + $data['length'] === TableSchema::LENGTH_TINY + ) + ) { + $type = ' NVARCHAR'; + $length = $data['length'] ?? TableSchema::LENGTH_TINY; + $out .= sprintf('%s(%d)', $type, $length); + } + + $hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING, TableSchema::TYPE_CHAR]; + if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') { + $out .= ' COLLATE ' . $data['collate']; + } + + $precisionTypes = [ + TableSchema::TYPE_FLOAT, + TableSchema::TYPE_DATETIME, + TableSchema::TYPE_DATETIME_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP, + TableSchema::TYPE_TIMESTAMP_FRACTIONAL, + ]; + if (in_array($data['type'], $precisionTypes, true) && isset($data['precision'])) { + $out .= '(' . (int)$data['precision'] . ')'; + } + + if ( + $data['type'] === TableSchema::TYPE_DECIMAL && + ( + isset($data['length']) || + isset($data['precision']) + ) + ) { + $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')'; + } + + if (isset($data['null']) && $data['null'] === false) { + $out .= ' NOT NULL'; + } + + $dateTimeTypes = [ + TableSchema::TYPE_DATETIME, + TableSchema::TYPE_DATETIME_FRACTIONAL, + TableSchema::TYPE_TIMESTAMP, + TableSchema::TYPE_TIMESTAMP_FRACTIONAL, + ]; + $dateTimeDefaults = [ + 'current_timestamp', + 'getdate()', + 'getutcdate()', + 'sysdatetime()', + 'sysutcdatetime()', + 'sysdatetimeoffset()', + ]; + if ( + isset($data['default']) && + in_array($data['type'], $dateTimeTypes, true) && + in_array(strtolower($data['default']), $dateTimeDefaults, true) + ) { + $out .= ' DEFAULT ' . strtoupper($data['default']); + } elseif (isset($data['default'])) { + $default = is_bool($data['default']) + ? (int)$data['default'] + : $this->_driver->schemaValue($data['default']); + $out .= ' DEFAULT ' . $default; + } elseif (isset($data['null']) && $data['null'] !== false) { + $out .= ' DEFAULT NULL'; + } + + return $out; + } + + /** + * @inheritDoc + */ + public function addConstraintSql(TableSchema $schema): array + { + $sqlPattern = 'ALTER TABLE %s ADD %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + /** @var array $constraint */ + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name)); + } + } + + return $sql; + } + + /** + * @inheritDoc + */ + public function dropConstraintSql(TableSchema $schema): array + { + $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + /** @var array $constraint */ + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $constraintName = $this->_driver->quoteIdentifier($name); + $sql[] = sprintf($sqlPattern, $tableName, $constraintName); + } + } + + return $sql; + } + + /** + * @inheritDoc + */ + public function indexSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getIndex($name); + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf( + 'CREATE INDEX %s ON %s (%s)', + $this->_driver->quoteIdentifier($name), + $this->_driver->quoteIdentifier($schema->name()), + implode(', ', $columns) + ); + } + + /** + * @inheritDoc + */ + public function constraintSql(TableSchema $schema, string $name): string + { + /** @var array $data */ + $data = $schema->getConstraint($name); + $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name); + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) { + $out = 'PRIMARY KEY'; + } + if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) { + $out .= ' UNIQUE'; + } + + return $this->_keySql($out, $data); + } + + /** + * Helper method for generating key SQL snippets. + * + * @param string $prefix The key prefix + * @param array $data Key data. + * @return string + */ + protected function _keySql(string $prefix, array $data): string + { + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + return $prefix . sprintf( + ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s', + implode(', ', $columns), + $this->_driver->quoteIdentifier($data['references'][0]), + $this->_convertConstraintColumns($data['references'][1]), + $this->_foreignOnClause($data['update']), + $this->_foreignOnClause($data['delete']) + ); + } + + return $prefix . ' (' . implode(', ', $columns) . ')'; + } + + /** + * @inheritDoc + */ + public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array + { + $content = array_merge($columns, $constraints); + $content = implode(",\n", array_filter($content)); + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $out = []; + $out[] = sprintf("CREATE TABLE %s (\n%s\n)", $tableName, $content); + foreach ($indexes as $index) { + $out[] = $index; + } + + return $out; + } + + /** + * @inheritDoc + */ + public function truncateTableSql(TableSchema $schema): array + { + $name = $this->_driver->quoteIdentifier($schema->name()); + $queries = [ + sprintf('DELETE FROM %s', $name), + ]; + + // Restart identity sequences + $pk = $schema->getPrimaryKey(); + if (count($pk) === 1) { + /** @var array $column */ + $column = $schema->getColumn($pk[0]); + if (in_array($column['type'], ['integer', 'biginteger'])) { + $queries[] = sprintf( + "IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = '%s' AND " . + "last_value IS NOT NULL) DBCC CHECKIDENT('%s', RESEED, 0)", + $schema->name(), + $schema->name() + ); + } + } + + return $queries; + } +} + +// phpcs:disable +// Add backwards compatible alias. +class_alias('Cake\Database\Schema\SqlserverSchemaDialect', 'Cake\Database\Schema\SqlserverSchema'); +// phpcs:enable diff --git a/vendor/cakephp/database/Schema/TableSchema.php b/vendor/cakephp/database/Schema/TableSchema.php new file mode 100644 index 0000000..f9ad3dc --- /dev/null +++ b/vendor/cakephp/database/Schema/TableSchema.php @@ -0,0 +1,799 @@ + + */ + protected $_columns = []; + + /** + * A map with columns to types + * + * @var array + */ + protected $_typeMap = []; + + /** + * Indexes in the table. + * + * @var array + */ + protected $_indexes = []; + + /** + * Constraints in the table. + * + * @var array> + */ + protected $_constraints = []; + + /** + * Options for the table. + * + * @var array + */ + protected $_options = []; + + /** + * Whether the table is temporary + * + * @var bool + */ + protected $_temporary = false; + + /** + * Column length when using a `tiny` column type + * + * @var int + */ + public const LENGTH_TINY = 255; + + /** + * Column length when using a `medium` column type + * + * @var int + */ + public const LENGTH_MEDIUM = 16777215; + + /** + * Column length when using a `long` column type + * + * @var int + */ + public const LENGTH_LONG = 4294967295; + + /** + * Valid column length that can be used with text type columns + * + * @var array + */ + public static $columnLengths = [ + 'tiny' => self::LENGTH_TINY, + 'medium' => self::LENGTH_MEDIUM, + 'long' => self::LENGTH_LONG, + ]; + + /** + * The valid keys that can be used in a column + * definition. + * + * @var array + */ + protected static $_columnKeys = [ + 'type' => null, + 'baseType' => null, + 'length' => null, + 'precision' => null, + 'null' => null, + 'default' => null, + 'comment' => null, + ]; + + /** + * Additional type specific properties. + * + * @var array> + */ + protected static $_columnExtras = [ + 'string' => [ + 'collate' => null, + ], + 'char' => [ + 'collate' => null, + ], + 'text' => [ + 'collate' => null, + ], + 'tinyinteger' => [ + 'unsigned' => null, + ], + 'smallinteger' => [ + 'unsigned' => null, + ], + 'integer' => [ + 'unsigned' => null, + 'autoIncrement' => null, + ], + 'biginteger' => [ + 'unsigned' => null, + 'autoIncrement' => null, + ], + 'decimal' => [ + 'unsigned' => null, + ], + 'float' => [ + 'unsigned' => null, + ], + ]; + + /** + * The valid keys that can be used in an index + * definition. + * + * @var array + */ + protected static $_indexKeys = [ + 'type' => null, + 'columns' => [], + 'length' => [], + 'references' => [], + 'update' => 'restrict', + 'delete' => 'restrict', + ]; + + /** + * Names of the valid index types. + * + * @var array + */ + protected static $_validIndexTypes = [ + self::INDEX_INDEX, + self::INDEX_FULLTEXT, + ]; + + /** + * Names of the valid constraint types. + * + * @var array + */ + protected static $_validConstraintTypes = [ + self::CONSTRAINT_PRIMARY, + self::CONSTRAINT_UNIQUE, + self::CONSTRAINT_FOREIGN, + ]; + + /** + * Names of the valid foreign key actions. + * + * @var array + */ + protected static $_validForeignKeyActions = [ + self::ACTION_CASCADE, + self::ACTION_SET_NULL, + self::ACTION_SET_DEFAULT, + self::ACTION_NO_ACTION, + self::ACTION_RESTRICT, + ]; + + /** + * Primary constraint type + * + * @var string + */ + public const CONSTRAINT_PRIMARY = 'primary'; + + /** + * Unique constraint type + * + * @var string + */ + public const CONSTRAINT_UNIQUE = 'unique'; + + /** + * Foreign constraint type + * + * @var string + */ + public const CONSTRAINT_FOREIGN = 'foreign'; + + /** + * Index - index type + * + * @var string + */ + public const INDEX_INDEX = 'index'; + + /** + * Fulltext index type + * + * @var string + */ + public const INDEX_FULLTEXT = 'fulltext'; + + /** + * Foreign key cascade action + * + * @var string + */ + public const ACTION_CASCADE = 'cascade'; + + /** + * Foreign key set null action + * + * @var string + */ + public const ACTION_SET_NULL = 'setNull'; + + /** + * Foreign key no action + * + * @var string + */ + public const ACTION_NO_ACTION = 'noAction'; + + /** + * Foreign key restrict action + * + * @var string + */ + public const ACTION_RESTRICT = 'restrict'; + + /** + * Foreign key restrict default + * + * @var string + */ + public const ACTION_SET_DEFAULT = 'setDefault'; + + /** + * Constructor. + * + * @param string $table The table name. + * @param array $columns The list of columns for the schema. + */ + public function __construct(string $table, array $columns = []) + { + $this->_table = $table; + foreach ($columns as $field => $definition) { + $this->addColumn($field, $definition); + } + } + + /** + * @inheritDoc + */ + public function name(): string + { + return $this->_table; + } + + /** + * @inheritDoc + */ + public function addColumn(string $name, $attrs) + { + if (is_string($attrs)) { + $attrs = ['type' => $attrs]; + } + $valid = static::$_columnKeys; + if (isset(static::$_columnExtras[$attrs['type']])) { + $valid += static::$_columnExtras[$attrs['type']]; + } + $attrs = array_intersect_key($attrs, $valid); + $this->_columns[$name] = $attrs + $valid; + $this->_typeMap[$name] = $this->_columns[$name]['type']; + + return $this; + } + + /** + * @inheritDoc + */ + public function removeColumn(string $name) + { + unset($this->_columns[$name], $this->_typeMap[$name]); + + return $this; + } + + /** + * @inheritDoc + */ + public function columns(): array + { + return array_keys($this->_columns); + } + + /** + * @inheritDoc + */ + public function getColumn(string $name): ?array + { + if (!isset($this->_columns[$name])) { + return null; + } + $column = $this->_columns[$name]; + unset($column['baseType']); + + return $column; + } + + /** + * @inheritDoc + */ + public function getColumnType(string $name): ?string + { + if (!isset($this->_columns[$name])) { + return null; + } + + return $this->_columns[$name]['type']; + } + + /** + * @inheritDoc + */ + public function setColumnType(string $name, string $type) + { + if (!isset($this->_columns[$name])) { + return $this; + } + + $this->_columns[$name]['type'] = $type; + $this->_typeMap[$name] = $type; + + return $this; + } + + /** + * @inheritDoc + */ + public function hasColumn(string $name): bool + { + return isset($this->_columns[$name]); + } + + /** + * @inheritDoc + */ + public function baseColumnType(string $column): ?string + { + if (isset($this->_columns[$column]['baseType'])) { + return $this->_columns[$column]['baseType']; + } + + $type = $this->getColumnType($column); + + if ($type === null) { + return null; + } + + if (TypeFactory::getMap($type)) { + $type = TypeFactory::build($type)->getBaseType(); + } + + return $this->_columns[$column]['baseType'] = $type; + } + + /** + * @inheritDoc + */ + public function typeMap(): array + { + return $this->_typeMap; + } + + /** + * @inheritDoc + */ + public function isNullable(string $name): bool + { + if (!isset($this->_columns[$name])) { + return true; + } + + return $this->_columns[$name]['null'] === true; + } + + /** + * @inheritDoc + */ + public function defaultValues(): array + { + $defaults = []; + foreach ($this->_columns as $name => $data) { + if (!array_key_exists('default', $data)) { + continue; + } + if ($data['default'] === null && $data['null'] !== true) { + continue; + } + $defaults[$name] = $data['default']; + } + + return $defaults; + } + + /** + * @inheritDoc + */ + public function addIndex(string $name, $attrs) + { + if (is_string($attrs)) { + $attrs = ['type' => $attrs]; + } + $attrs = array_intersect_key($attrs, static::$_indexKeys); + $attrs += static::$_indexKeys; + unset($attrs['references'], $attrs['update'], $attrs['delete']); + + if (!in_array($attrs['type'], static::$_validIndexTypes, true)) { + throw new DatabaseException(sprintf( + 'Invalid index type "%s" in index "%s" in table "%s".', + $attrs['type'], + $name, + $this->_table + )); + } + if (empty($attrs['columns'])) { + throw new DatabaseException(sprintf( + 'Index "%s" in table "%s" must have at least one column.', + $name, + $this->_table + )); + } + $attrs['columns'] = (array)$attrs['columns']; + foreach ($attrs['columns'] as $field) { + if (empty($this->_columns[$field])) { + $msg = sprintf( + 'Columns used in index "%s" in table "%s" must be added to the Table schema first. ' . + 'The column "%s" was not found.', + $name, + $this->_table, + $field + ); + throw new DatabaseException($msg); + } + } + $this->_indexes[$name] = $attrs; + + return $this; + } + + /** + * @inheritDoc + */ + public function indexes(): array + { + return array_keys($this->_indexes); + } + + /** + * @inheritDoc + */ + public function getIndex(string $name): ?array + { + if (!isset($this->_indexes[$name])) { + return null; + } + + return $this->_indexes[$name]; + } + + /** + * Get the column(s) used for the primary key. + * + * @return array Column name(s) for the primary key. An + * empty list will be returned when the table has no primary key. + * @deprecated 4.0.0 Renamed to {@link getPrimaryKey()}. + */ + public function primaryKey(): array + { + deprecationWarning('`TableSchema::primaryKey()` is deprecated. Use `TableSchema::getPrimaryKey()`.'); + + return $this->getPrimarykey(); + } + + /** + * @inheritDoc + */ + public function getPrimaryKey(): array + { + foreach ($this->_constraints as $data) { + if ($data['type'] === static::CONSTRAINT_PRIMARY) { + return $data['columns']; + } + } + + return []; + } + + /** + * @inheritDoc + */ + public function addConstraint(string $name, $attrs) + { + if (is_string($attrs)) { + $attrs = ['type' => $attrs]; + } + $attrs = array_intersect_key($attrs, static::$_indexKeys); + $attrs += static::$_indexKeys; + if (!in_array($attrs['type'], static::$_validConstraintTypes, true)) { + throw new DatabaseException(sprintf( + 'Invalid constraint type "%s" in table "%s".', + $attrs['type'], + $this->_table + )); + } + if (empty($attrs['columns'])) { + throw new DatabaseException(sprintf( + 'Constraints in table "%s" must have at least one column.', + $this->_table + )); + } + $attrs['columns'] = (array)$attrs['columns']; + foreach ($attrs['columns'] as $field) { + if (empty($this->_columns[$field])) { + $msg = sprintf( + 'Columns used in constraints must be added to the Table schema first. ' . + 'The column "%s" was not found in table "%s".', + $field, + $this->_table + ); + throw new DatabaseException($msg); + } + } + + if ($attrs['type'] === static::CONSTRAINT_FOREIGN) { + $attrs = $this->_checkForeignKey($attrs); + + if (isset($this->_constraints[$name])) { + $this->_constraints[$name]['columns'] = array_unique(array_merge( + $this->_constraints[$name]['columns'], + $attrs['columns'] + )); + + if (isset($this->_constraints[$name]['references'])) { + $this->_constraints[$name]['references'][1] = array_unique(array_merge( + (array)$this->_constraints[$name]['references'][1], + [$attrs['references'][1]] + )); + } + + return $this; + } + } else { + unset($attrs['references'], $attrs['update'], $attrs['delete']); + } + + $this->_constraints[$name] = $attrs; + + return $this; + } + + /** + * @inheritDoc + */ + public function dropConstraint(string $name) + { + if (isset($this->_constraints[$name])) { + unset($this->_constraints[$name]); + } + + return $this; + } + + /** + * Check whether a table has an autoIncrement column defined. + * + * @return bool + */ + public function hasAutoincrement(): bool + { + foreach ($this->_columns as $column) { + if (isset($column['autoIncrement']) && $column['autoIncrement']) { + return true; + } + } + + return false; + } + + /** + * Helper method to check/validate foreign keys. + * + * @param array $attrs Attributes to set. + * @return array + * @throws \Cake\Database\Exception\DatabaseException When foreign key definition is not valid. + */ + protected function _checkForeignKey(array $attrs): array + { + if (count($attrs['references']) < 2) { + throw new DatabaseException('References must contain a table and column.'); + } + if (!in_array($attrs['update'], static::$_validForeignKeyActions)) { + throw new DatabaseException(sprintf( + 'Update action is invalid. Must be one of %s', + implode(',', static::$_validForeignKeyActions) + )); + } + if (!in_array($attrs['delete'], static::$_validForeignKeyActions)) { + throw new DatabaseException(sprintf( + 'Delete action is invalid. Must be one of %s', + implode(',', static::$_validForeignKeyActions) + )); + } + + return $attrs; + } + + /** + * @inheritDoc + */ + public function constraints(): array + { + return array_keys($this->_constraints); + } + + /** + * @inheritDoc + */ + public function getConstraint(string $name): ?array + { + return $this->_constraints[$name] ?? null; + } + + /** + * @inheritDoc + */ + public function setOptions(array $options) + { + $this->_options = $options + $this->_options; + + return $this; + } + + /** + * @inheritDoc + */ + public function getOptions(): array + { + return $this->_options; + } + + /** + * @inheritDoc + */ + public function setTemporary(bool $temporary) + { + $this->_temporary = $temporary; + + return $this; + } + + /** + * @inheritDoc + */ + public function isTemporary(): bool + { + return $this->_temporary; + } + + /** + * @inheritDoc + */ + public function createSql(Connection $connection): array + { + $dialect = $connection->getDriver()->schemaDialect(); + $columns = $constraints = $indexes = []; + foreach (array_keys($this->_columns) as $name) { + $columns[] = $dialect->columnSql($this, $name); + } + foreach (array_keys($this->_constraints) as $name) { + $constraints[] = $dialect->constraintSql($this, $name); + } + foreach (array_keys($this->_indexes) as $name) { + $indexes[] = $dialect->indexSql($this, $name); + } + + return $dialect->createTableSql($this, $columns, $constraints, $indexes); + } + + /** + * @inheritDoc + */ + public function dropSql(Connection $connection): array + { + $dialect = $connection->getDriver()->schemaDialect(); + + return $dialect->dropTableSql($this); + } + + /** + * @inheritDoc + */ + public function truncateSql(Connection $connection): array + { + $dialect = $connection->getDriver()->schemaDialect(); + + return $dialect->truncateTableSql($this); + } + + /** + * @inheritDoc + */ + public function addConstraintSql(Connection $connection): array + { + $dialect = $connection->getDriver()->schemaDialect(); + + return $dialect->addConstraintSql($this); + } + + /** + * @inheritDoc + */ + public function dropConstraintSql(Connection $connection): array + { + $dialect = $connection->getDriver()->schemaDialect(); + + return $dialect->dropConstraintSql($this); + } + + /** + * Returns an array of the table schema. + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'table' => $this->_table, + 'columns' => $this->_columns, + 'indexes' => $this->_indexes, + 'constraints' => $this->_constraints, + 'options' => $this->_options, + 'typeMap' => $this->_typeMap, + 'temporary' => $this->_temporary, + ]; + } +} diff --git a/vendor/cakephp/database/Schema/TableSchemaAwareInterface.php b/vendor/cakephp/database/Schema/TableSchemaAwareInterface.php new file mode 100644 index 0000000..f08e363 --- /dev/null +++ b/vendor/cakephp/database/Schema/TableSchemaAwareInterface.php @@ -0,0 +1,38 @@ + Column name(s) for the primary key. An + * empty list will be returned when the table has no primary key. + */ + public function getPrimaryKey(): array; + + /** + * Add an index. + * + * Used to add indexes, and full text indexes in platforms that support + * them. + * + * ### Attributes + * + * - `type` The type of index being added. + * - `columns` The columns in the index. + * + * @param string $name The name of the index. + * @param array|string $attrs The attributes for the index. + * If string it will be used as `type`. + * @return $this + * @throws \Cake\Database\Exception\DatabaseException + */ + public function addIndex(string $name, $attrs); + + /** + * Read information about an index based on name. + * + * @param string $name The name of the index. + * @return array|null Array of index data, or null + */ + public function getIndex(string $name): ?array; + + /** + * Get the names of all the indexes in the table. + * + * @return array + */ + public function indexes(): array; + + /** + * Add a constraint. + * + * Used to add constraints to a table. For example primary keys, unique + * keys and foreign keys. + * + * ### Attributes + * + * - `type` The type of constraint being added. + * - `columns` The columns in the index. + * - `references` The table, column a foreign key references. + * - `update` The behavior on update. Options are 'restrict', 'setNull', 'cascade', 'noAction'. + * - `delete` The behavior on delete. Options are 'restrict', 'setNull', 'cascade', 'noAction'. + * + * The default for 'update' & 'delete' is 'cascade'. + * + * @param string $name The name of the constraint. + * @param array|string $attrs The attributes for the constraint. + * If string it will be used as `type`. + * @return $this + * @throws \Cake\Database\Exception\DatabaseException + */ + public function addConstraint(string $name, $attrs); + + /** + * Read information about a constraint based on name. + * + * @param string $name The name of the constraint. + * @return array|null Array of constraint data, or null + */ + public function getConstraint(string $name): ?array; + + /** + * Remove a constraint. + * + * @param string $name Name of the constraint to remove + * @return $this + */ + public function dropConstraint(string $name); + + /** + * Get the names of all the constraints in the table. + * + * @return array + */ + public function constraints(): array; +} diff --git a/vendor/cakephp/database/SchemaCache.php b/vendor/cakephp/database/SchemaCache.php new file mode 100644 index 0000000..4305304 --- /dev/null +++ b/vendor/cakephp/database/SchemaCache.php @@ -0,0 +1,116 @@ +_schema = $this->getSchema($connection); + } + + /** + * Build metadata. + * + * @param string|null $name The name of the table to build cache data for. + * @return array Returns a list build table caches + */ + public function build(?string $name = null): array + { + if ($name) { + $tables = [$name]; + } else { + $tables = $this->_schema->listTables(); + } + + foreach ($tables as $table) { + /** @psalm-suppress PossiblyNullArgument */ + $this->_schema->describe($table, ['forceRefresh' => true]); + } + + return $tables; + } + + /** + * Clear metadata. + * + * @param string|null $name The name of the table to clear cache data for. + * @return array Returns a list of cleared table caches + */ + public function clear(?string $name = null): array + { + if ($name) { + $tables = [$name]; + } else { + $tables = $this->_schema->listTables(); + } + + $cacher = $this->_schema->getCacher(); + + foreach ($tables as $table) { + /** @psalm-suppress PossiblyNullArgument */ + $key = $this->_schema->cacheKey($table); + $cacher->delete($key); + } + + return $tables; + } + + /** + * Helper method to get the schema collection. + * + * @param \Cake\Database\Connection $connection Connection object + * @return \Cake\Database\Schema\CachedCollection + * @throws \RuntimeException If given connection object is not compatible with schema caching + */ + public function getSchema(Connection $connection): CachedCollection + { + $config = $connection->config(); + if (empty($config['cacheMetadata'])) { + $connection->cacheMetadata(true); + } + + /** @var \Cake\Database\Schema\CachedCollection $schemaCollection */ + $schemaCollection = $connection->getSchemaCollection(); + + return $schemaCollection; + } +} diff --git a/vendor/cakephp/database/SqlDialectTrait.php b/vendor/cakephp/database/SqlDialectTrait.php new file mode 100644 index 0000000..38ef7bc --- /dev/null +++ b/vendor/cakephp/database/SqlDialectTrait.php @@ -0,0 +1,5 @@ + 'DELETE', + 'where' => ' WHERE %s', + 'group' => ' GROUP BY %s', + 'order' => ' %s', + 'offset' => ' OFFSET %s ROWS', + 'epilog' => ' %s', + ]; + + /** + * @inheritDoc + */ + protected $_selectParts = [ + 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order', + 'offset', 'limit', 'union', 'epilog', + ]; + + /** + * Helper function used to build the string representation of a `WITH` clause, + * it constructs the CTE definitions list without generating the `RECURSIVE` + * keyword that is neither required nor valid. + * + * @param array $parts List of CTEs to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string + { + $expressions = []; + foreach ($parts as $cte) { + $expressions[] = $cte->sql($binder); + } + + return sprintf('WITH %s ', implode(', ', $expressions)); + } + + /** + * Generates the INSERT part of a SQL query + * + * To better handle concurrency and low transaction isolation levels, + * we also include an OUTPUT clause so we can ensure we get the inserted + * row's data back. + * + * @param array $parts The parts to build + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string + { + if (!isset($parts[0])) { + throw new DatabaseException( + 'Could not compile insert query. No table was specified. ' . + 'Use `into()` to define a table.' + ); + } + $table = $parts[0]; + $columns = $this->_stringifyExpressions($parts[1], $binder); + $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder); + + return sprintf( + 'INSERT%s INTO %s (%s) OUTPUT INSERTED.*', + $modifiers, + $table, + implode(', ', $columns) + ); + } + + /** + * Generates the LIMIT part of a SQL query + * + * @param int $limit the limit clause + * @param \Cake\Database\Query $query The query that is being compiled + * @return string + */ + protected function _buildLimitPart(int $limit, Query $query): string + { + if ($query->clause('offset') === null) { + return ''; + } + + return sprintf(' FETCH FIRST %d ROWS ONLY', $limit); + } + + /** + * Helper function used to build the string representation of a HAVING clause, + * it constructs the field list taking care of aliasing and + * converting expression objects to string. + * + * @param array $parts list of fields to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder + * @return string + */ + protected function _buildHavingPart($parts, $query, $binder) + { + $selectParts = $query->clause('select'); + + foreach ($selectParts as $selectKey => $selectPart) { + if (!$selectPart instanceof FunctionExpression) { + continue; + } + foreach ($parts as $k => $p) { + if (!is_string($p)) { + continue; + } + preg_match_all( + '/\b' . trim($selectKey, '[]') . '\b/i', + $p, + $matches + ); + + if (empty($matches[0])) { + continue; + } + + $parts[$k] = preg_replace( + ['/\[|\]/', '/\b' . trim($selectKey, '[]') . '\b/i'], + ['', $selectPart->sql($binder)], + $p + ); + } + } + + return sprintf(' HAVING %s', implode(', ', $parts)); + } +} diff --git a/vendor/cakephp/database/Statement/BufferResultsTrait.php b/vendor/cakephp/database/Statement/BufferResultsTrait.php new file mode 100644 index 0000000..b6a5e57 --- /dev/null +++ b/vendor/cakephp/database/Statement/BufferResultsTrait.php @@ -0,0 +1,45 @@ +_bufferResults = $buffer; + + return $this; + } +} diff --git a/vendor/cakephp/database/Statement/BufferedStatement.php b/vendor/cakephp/database/Statement/BufferedStatement.php new file mode 100644 index 0000000..7f7f462 --- /dev/null +++ b/vendor/cakephp/database/Statement/BufferedStatement.php @@ -0,0 +1,349 @@ + + */ + protected $buffer = []; + + /** + * Whether this statement has already been executed + * + * @var bool + */ + protected $_hasExecuted = false; + + /** + * The current iterator index. + * + * @var int + */ + protected $index = 0; + + /** + * Constructor + * + * @param \Cake\Database\StatementInterface $statement Statement implementation such as PDOStatement + * @param \Cake\Database\DriverInterface $driver Driver instance + */ + public function __construct(StatementInterface $statement, DriverInterface $driver) + { + $this->statement = $statement; + $this->_driver = $driver; + } + + /** + * Magic getter to return $queryString as read-only. + * + * @param string $property internal property to get + * @return string|null + */ + public function __get(string $property) + { + if ($property === 'queryString') { + /** @psalm-suppress NoInterfaceProperties */ + return $this->statement->queryString; + } + + return null; + } + + /** + * @inheritDoc + */ + public function bindValue($column, $value, $type = 'string'): void + { + $this->statement->bindValue($column, $value, $type); + } + + /** + * @inheritDoc + */ + public function closeCursor(): void + { + $this->statement->closeCursor(); + } + + /** + * @inheritDoc + */ + public function columnCount(): int + { + return $this->statement->columnCount(); + } + + /** + * @inheritDoc + */ + public function errorCode() + { + return $this->statement->errorCode(); + } + + /** + * @inheritDoc + */ + public function errorInfo(): array + { + return $this->statement->errorInfo(); + } + + /** + * @inheritDoc + */ + public function execute(?array $params = null): bool + { + $this->_reset(); + $this->_hasExecuted = true; + + return $this->statement->execute($params); + } + + /** + * @inheritDoc + */ + public function fetchColumn(int $position) + { + $result = $this->fetch(static::FETCH_TYPE_NUM); + if ($result !== false && isset($result[$position])) { + return $result[$position]; + } + + return false; + } + + /** + * Statements can be passed as argument for count() to return the number + * for affected rows from last execution. + * + * @return int + */ + public function count(): int + { + return $this->rowCount(); + } + + /** + * @inheritDoc + */ + public function bind(array $params, array $types): void + { + $this->statement->bind($params, $types); + } + + /** + * @inheritDoc + */ + public function lastInsertId(?string $table = null, ?string $column = null) + { + return $this->statement->lastInsertId($table, $column); + } + + /** + * {@inheritDoc} + * + * @param string|int $type The type to fetch. + * @return array|false + */ + public function fetch($type = self::FETCH_TYPE_NUM) + { + if ($this->_allFetched) { + $row = false; + if (isset($this->buffer[$this->index])) { + $row = $this->buffer[$this->index]; + } + $this->index += 1; + + if ($row && $type === static::FETCH_TYPE_NUM) { + return array_values($row); + } + + return $row; + } + + $record = $this->statement->fetch($type); + if ($record === false) { + $this->_allFetched = true; + $this->statement->closeCursor(); + + return false; + } + $this->buffer[] = $record; + + return $record; + } + + /** + * @return array + */ + public function fetchAssoc(): array + { + $result = $this->fetch(static::FETCH_TYPE_ASSOC); + + return $result ?: []; + } + + /** + * @inheritDoc + */ + public function fetchAll($type = self::FETCH_TYPE_NUM) + { + if ($this->_allFetched) { + return $this->buffer; + } + $results = $this->statement->fetchAll($type); + if ($results !== false) { + $this->buffer = array_merge($this->buffer, $results); + } + $this->_allFetched = true; + $this->statement->closeCursor(); + + return $this->buffer; + } + + /** + * @inheritDoc + */ + public function rowCount(): int + { + if (!$this->_allFetched) { + $this->fetchAll(static::FETCH_TYPE_ASSOC); + } + + return count($this->buffer); + } + + /** + * Reset all properties + * + * @return void + */ + protected function _reset(): void + { + $this->buffer = []; + $this->_allFetched = false; + $this->index = 0; + } + + /** + * Returns the current key in the iterator + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->index; + } + + /** + * Returns the current record in the iterator + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->buffer[$this->index]; + } + + /** + * Rewinds the collection + * + * @return void + */ + public function rewind(): void + { + $this->index = 0; + } + + /** + * Returns whether the iterator has more elements + * + * @return bool + */ + public function valid(): bool + { + $old = $this->index; + $row = $this->fetch(self::FETCH_TYPE_ASSOC); + + // Restore the index as fetch() increments during + // the cache scenario. + $this->index = $old; + + return $row !== false; + } + + /** + * Advances the iterator pointer to the next element + * + * @return void + */ + public function next(): void + { + $this->index += 1; + } + + /** + * Get the wrapped statement + * + * @return \Cake\Database\StatementInterface + */ + public function getInnerStatement(): StatementInterface + { + return $this->statement; + } +} diff --git a/vendor/cakephp/database/Statement/CallbackStatement.php b/vendor/cakephp/database/Statement/CallbackStatement.php new file mode 100644 index 0000000..a0b59c3 --- /dev/null +++ b/vendor/cakephp/database/Statement/CallbackStatement.php @@ -0,0 +1,77 @@ +_callback = $callback; + } + + /** + * Fetch a row from the statement. + * + * The result will be processed by the callback when it is not `false`. + * + * @param string|int $type Either 'num' or 'assoc' to indicate the result format you would like. + * @return array|false + */ + public function fetch($type = parent::FETCH_TYPE_NUM) + { + $callback = $this->_callback; + $row = $this->_statement->fetch($type); + + return $row === false ? $row : $callback($row); + } + + /** + * {@inheritDoc} + * + * Each row in the result will be processed by the callback when it is not `false. + */ + public function fetchAll($type = parent::FETCH_TYPE_NUM) + { + $results = $this->_statement->fetchAll($type); + + return $results !== false ? array_map($this->_callback, $results) : false; + } +} diff --git a/vendor/cakephp/database/Statement/MysqlStatement.php b/vendor/cakephp/database/Statement/MysqlStatement.php new file mode 100644 index 0000000..2ad4ec0 --- /dev/null +++ b/vendor/cakephp/database/Statement/MysqlStatement.php @@ -0,0 +1,46 @@ +_driver->getConnection(); + + try { + $connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $this->_bufferResults); + $result = $this->_statement->execute($params); + } finally { + $connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); + } + + return $result; + } +} diff --git a/vendor/cakephp/database/Statement/PDOStatement.php b/vendor/cakephp/database/Statement/PDOStatement.php new file mode 100644 index 0000000..5e7309a --- /dev/null +++ b/vendor/cakephp/database/Statement/PDOStatement.php @@ -0,0 +1,176 @@ +_statement = $statement; + $this->_driver = $driver; + } + + /** + * Magic getter to return PDOStatement::$queryString as read-only. + * + * @param string $property internal property to get + * @return string|null + */ + public function __get(string $property) + { + if ($property === 'queryString' && isset($this->_statement->queryString)) { + /** @psalm-suppress NoInterfaceProperties */ + return $this->_statement->queryString; + } + + return null; + } + + /** + * Assign a value to a positional or named variable in prepared query. If using + * positional variables you need to start with index one, if using named params then + * just use the name in any order. + * + * You can pass PDO compatible constants for binding values with a type or optionally + * any type name registered in the Type class. Any value will be converted to the valid type + * representation if needed. + * + * It is not allowed to combine positional and named variables in the same statement + * + * ### Examples: + * + * ``` + * $statement->bindValue(1, 'a title'); + * $statement->bindValue(2, 5, PDO::INT); + * $statement->bindValue('active', true, 'boolean'); + * $statement->bindValue(5, new \DateTime(), 'date'); + * ``` + * + * @param string|int $column name or param position to be bound + * @param mixed $value The value to bind to variable in query + * @param string|int|null $type PDO type or name of configured Type class + * @return void + */ + public function bindValue($column, $value, $type = 'string'): void + { + if ($type === null) { + $type = 'string'; + } + if (!is_int($type)) { + [$value, $type] = $this->cast($value, $type); + } + $this->_statement->bindValue($column, $value, $type); + } + + /** + * Returns the next row for the result set after executing this statement. + * Rows can be fetched to contain columns as names or positions. If no + * rows are left in result set, this method will return false + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] + * ``` + * + * @param string|int $type 'num' for positional columns, assoc for named columns + * @return mixed Result array containing columns and values or false if no results + * are left + */ + public function fetch($type = parent::FETCH_TYPE_NUM) + { + if ($type === static::FETCH_TYPE_NUM) { + return $this->_statement->fetch(PDO::FETCH_NUM); + } + if ($type === static::FETCH_TYPE_ASSOC) { + return $this->_statement->fetch(PDO::FETCH_ASSOC); + } + if ($type === static::FETCH_TYPE_OBJ) { + return $this->_statement->fetch(PDO::FETCH_OBJ); + } + + if (!is_int($type)) { + throw new CakeException(sprintf( + 'Fetch type for PDOStatement must be an integer, found `%s` instead', + getTypeName($type) + )); + } + + return $this->_statement->fetch($type); + } + + /** + * Returns an array with all rows resulting from executing this statement + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']] + * ``` + * + * @param string|int $type num for fetching columns as positional keys or assoc for column names as keys + * @return array|false list of all results from database for this statement, false on failure + * @psalm-assert string $type + */ + public function fetchAll($type = parent::FETCH_TYPE_NUM) + { + if ($type === static::FETCH_TYPE_NUM) { + return $this->_statement->fetchAll(PDO::FETCH_NUM); + } + if ($type === static::FETCH_TYPE_ASSOC) { + return $this->_statement->fetchAll(PDO::FETCH_ASSOC); + } + if ($type === static::FETCH_TYPE_OBJ) { + return $this->_statement->fetchAll(PDO::FETCH_OBJ); + } + + if (!is_int($type)) { + throw new CakeException(sprintf( + 'Fetch type for PDOStatement must be an integer, found `%s` instead', + getTypeName($type) + )); + } + + return $this->_statement->fetchAll($type); + } +} diff --git a/vendor/cakephp/database/Statement/SqliteStatement.php b/vendor/cakephp/database/Statement/SqliteStatement.php new file mode 100644 index 0000000..cc965a8 --- /dev/null +++ b/vendor/cakephp/database/Statement/SqliteStatement.php @@ -0,0 +1,70 @@ +_statement instanceof BufferedStatement) { + $this->_statement = $this->_statement->getInnerStatement(); + } + + if ($this->_bufferResults) { + $this->_statement = new BufferedStatement($this->_statement, $this->_driver); + } + + return $this->_statement->execute($params); + } + + /** + * Returns the number of rows returned of affected by last execution + * + * @return int + */ + public function rowCount(): int + { + /** @psalm-suppress NoInterfaceProperties */ + if ( + $this->_statement->queryString && + preg_match('/^(?:DELETE|UPDATE|INSERT)/i', $this->_statement->queryString) + ) { + $changes = $this->_driver->prepare('SELECT CHANGES()'); + $changes->execute(); + $row = $changes->fetch(); + $changes->closeCursor(); + + if (!$row) { + return 0; + } + + return (int)$row[0]; + } + + return parent::rowCount(); + } +} diff --git a/vendor/cakephp/database/Statement/SqlserverStatement.php b/vendor/cakephp/database/Statement/SqlserverStatement.php new file mode 100644 index 0000000..efd3b6a --- /dev/null +++ b/vendor/cakephp/database/Statement/SqlserverStatement.php @@ -0,0 +1,54 @@ +cast($value, $type); + } + if ($type === PDO::PARAM_LOB) { + /** @psalm-suppress UndefinedConstant */ + $this->_statement->bindParam($column, $value, $type, 0, PDO::SQLSRV_ENCODING_BINARY); + } else { + $this->_statement->bindValue($column, $value, $type); + } + } +} diff --git a/vendor/cakephp/database/Statement/StatementDecorator.php b/vendor/cakephp/database/Statement/StatementDecorator.php new file mode 100644 index 0000000..2b313cd --- /dev/null +++ b/vendor/cakephp/database/Statement/StatementDecorator.php @@ -0,0 +1,363 @@ +_statement = $statement; + $this->_driver = $driver; + } + + /** + * Magic getter to return $queryString as read-only. + * + * @param string $property internal property to get + * @return string|null + */ + public function __get(string $property) + { + if ($property === 'queryString') { + /** @psalm-suppress NoInterfaceProperties */ + return $this->_statement->queryString; + } + + return null; + } + + /** + * Assign a value to a positional or named variable in prepared query. If using + * positional variables you need to start with index one, if using named params then + * just use the name in any order. + * + * It is not allowed to combine positional and named variables in the same statement. + * + * ### Examples: + * + * ``` + * $statement->bindValue(1, 'a title'); + * $statement->bindValue('active', true, 'boolean'); + * $statement->bindValue(5, new \DateTime(), 'date'); + * ``` + * + * @param string|int $column name or param position to be bound + * @param mixed $value The value to bind to variable in query + * @param string|int|null $type name of configured Type class + * @return void + */ + public function bindValue($column, $value, $type = 'string'): void + { + $this->_statement->bindValue($column, $value, $type); + } + + /** + * Closes a cursor in the database, freeing up any resources and memory + * allocated to it. In most cases you don't need to call this method, as it is + * automatically called after fetching all results from the result set. + * + * @return void + */ + public function closeCursor(): void + { + $this->_statement->closeCursor(); + } + + /** + * Returns the number of columns this statement's results will contain. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * echo $statement->columnCount(); // outputs 2 + * ``` + * + * @return int + */ + public function columnCount(): int + { + return $this->_statement->columnCount(); + } + + /** + * Returns the error code for the last error that occurred when executing this statement. + * + * @return string|int + */ + public function errorCode() + { + return $this->_statement->errorCode(); + } + + /** + * Returns the error information for the last error that occurred when executing + * this statement. + * + * @return array + */ + public function errorInfo(): array + { + return $this->_statement->errorInfo(); + } + + /** + * Executes the statement by sending the SQL query to the database. It can optionally + * take an array or arguments to be bound to the query variables. Please note + * that binding parameters from this method will not perform any custom type conversion + * as it would normally happen when calling `bindValue`. + * + * @param array|null $params list of values to be bound to query + * @return bool true on success, false otherwise + */ + public function execute(?array $params = null): bool + { + $this->_hasExecuted = true; + + return $this->_statement->execute($params); + } + + /** + * Returns the next row for the result set after executing this statement. + * Rows can be fetched to contain columns as names or positions. If no + * rows are left in result set, this method will return false. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] + * ``` + * + * @param string|int $type 'num' for positional columns, assoc for named columns + * @return mixed Result array containing columns and values or false if no results + * are left + */ + public function fetch($type = self::FETCH_TYPE_NUM) + { + return $this->_statement->fetch($type); + } + + /** + * Returns the next row in a result set as an associative array. Calling this function is the same as calling + * $statement->fetch(StatementDecorator::FETCH_TYPE_ASSOC). If no results are found an empty array is returned. + * + * @return array + */ + public function fetchAssoc(): array + { + $result = $this->fetch(static::FETCH_TYPE_ASSOC); + + return $result ?: []; + } + + /** + * Returns the value of the result at position. + * + * @param int $position The numeric position of the column to retrieve in the result + * @return mixed Returns the specific value of the column designated at $position + */ + public function fetchColumn(int $position) + { + $result = $this->fetch(static::FETCH_TYPE_NUM); + if ($result && isset($result[$position])) { + return $result[$position]; + } + + return false; + } + + /** + * Returns an array with all rows resulting from executing this statement. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']] + * ``` + * + * @param string|int $type num for fetching columns as positional keys or assoc for column names as keys + * @return array|false List of all results from database for this statement. False on failure. + */ + public function fetchAll($type = self::FETCH_TYPE_NUM) + { + return $this->_statement->fetchAll($type); + } + + /** + * Returns the number of rows affected by this SQL statement. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->rowCount()); // will show 1 + * ``` + * + * @return int + */ + public function rowCount(): int + { + return $this->_statement->rowCount(); + } + + /** + * Statements are iterable as arrays, this method will return + * the iterator object for traversing all items in the result. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * foreach ($statement as $row) { + * //do stuff + * } + * ``` + * + * @return \Cake\Database\StatementInterface + * @psalm-suppress ImplementedReturnTypeMismatch + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + if (!$this->_hasExecuted) { + $this->execute(); + } + + return $this->_statement; + } + + /** + * Statements can be passed as argument for count() to return the number + * for affected rows from last execution. + * + * @return int + */ + public function count(): int + { + return $this->rowCount(); + } + + /** + * Binds a set of values to statement object with corresponding type. + * + * @param array $params list of values to be bound + * @param array $types list of types to be used, keys should match those in $params + * @return void + */ + public function bind(array $params, array $types): void + { + if (empty($params)) { + return; + } + + $anonymousParams = is_int(key($params)) ? true : false; + $offset = 1; + foreach ($params as $index => $value) { + $type = $types[$index] ?? null; + if ($anonymousParams) { + /** @psalm-suppress InvalidOperand */ + $index += $offset; + } + /** @psalm-suppress InvalidScalarArgument */ + $this->bindValue($index, $value, $type); + } + } + + /** + * Returns the latest primary inserted using this statement. + * + * @param string|null $table table name or sequence to get last insert value from + * @param string|null $column the name of the column representing the primary key + * @return string|int + */ + public function lastInsertId(?string $table = null, ?string $column = null) + { + if ($column && $this->columnCount()) { + $row = $this->fetch(static::FETCH_TYPE_ASSOC); + + if ($row && isset($row[$column])) { + return $row[$column]; + } + } + + return $this->_driver->lastInsertId($table, $column); + } + + /** + * Returns the statement object that was decorated by this class. + * + * @return \Cake\Database\StatementInterface + */ + public function getInnerStatement() + { + return $this->_statement; + } +} diff --git a/vendor/cakephp/database/StatementInterface.php b/vendor/cakephp/database/StatementInterface.php new file mode 100644 index 0000000..dc5bbc6 --- /dev/null +++ b/vendor/cakephp/database/StatementInterface.php @@ -0,0 +1,203 @@ +bindValue(1, 'a title'); + * $statement->bindValue('active', true, 'boolean'); + * $statement->bindValue(5, new \DateTime(), 'date'); + * ``` + * + * @param string|int $column name or param position to be bound + * @param mixed $value The value to bind to variable in query + * @param string|int|null $type name of configured Type class, or PDO type constant. + * @return void + */ + public function bindValue($column, $value, $type = 'string'): void; + + /** + * Closes a cursor in the database, freeing up any resources and memory + * allocated to it. In most cases you don't need to call this method, as it is + * automatically called after fetching all results from the result set. + * + * @return void + */ + public function closeCursor(): void; + + /** + * Returns the number of columns this statement's results will contain + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * echo $statement->columnCount(); // outputs 2 + * ``` + * + * @return int + */ + public function columnCount(): int; + + /** + * Returns the error code for the last error that occurred when executing this statement + * + * @return string|int + */ + public function errorCode(); + + /** + * Returns the error information for the last error that occurred when executing + * this statement + * + * @return array + */ + public function errorInfo(): array; + + /** + * Executes the statement by sending the SQL query to the database. It can optionally + * take an array or arguments to be bound to the query variables. Please note + * that binding parameters from this method will not perform any custom type conversion + * as it would normally happen when calling `bindValue` + * + * @param array|null $params list of values to be bound to query + * @return bool true on success, false otherwise + */ + public function execute(?array $params = null): bool; + + /** + * Returns the next row for the result set after executing this statement. + * Rows can be fetched to contain columns as names or positions. If no + * rows are left in result set, this method will return false + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] + * ``` + * + * @param string|int $type 'num' for positional columns, assoc for named columns, or PDO fetch mode constants. + * @return mixed Result array containing columns and values or false if no results + * are left + */ + public function fetch($type = 'num'); + + /** + * Returns an array with all rows resulting from executing this statement + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']] + * ``` + * + * @param string|int $type num for fetching columns as positional keys or assoc for column names as keys + * @return array|false list of all results from database for this statement or false on failure. + */ + public function fetchAll($type = 'num'); + + /** + * Returns the value of the result at position. + * + * @param int $position The numeric position of the column to retrieve in the result + * @return mixed Returns the specific value of the column designated at $position + */ + public function fetchColumn(int $position); + + /** + * Returns the number of rows affected by this SQL statement + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->rowCount()); // will show 1 + * ``` + * + * @return int + */ + public function rowCount(): int; + + /** + * Statements can be passed as argument for count() + * to return the number for affected rows from last execution + * + * @return int + */ + public function count(): int; + + /** + * Binds a set of values to statement object with corresponding type + * + * @param array $params list of values to be bound + * @param array $types list of types to be used, keys should match those in $params + * @return void + */ + public function bind(array $params, array $types): void; + + /** + * Returns the latest primary inserted using this statement + * + * @param string|null $table table name or sequence to get last insert value from + * @param string|null $column the name of the column representing the primary key + * @return string|int + */ + public function lastInsertId(?string $table = null, ?string $column = null); +} diff --git a/vendor/cakephp/database/Type.php b/vendor/cakephp/database/Type.php new file mode 100644 index 0000000..466b8d5 --- /dev/null +++ b/vendor/cakephp/database/Type.php @@ -0,0 +1,5 @@ +_name = $name; + } + + /** + * @inheritDoc + */ + public function getName(): ?string + { + return $this->_name; + } + + /** + * @inheritDoc + */ + public function getBaseType(): ?string + { + return $this->_name; + } + + /** + * @inheritDoc + */ + public function toStatement($value, DriverInterface $driver) + { + if ($value === null) { + return PDO::PARAM_NULL; + } + + return PDO::PARAM_STR; + } + + /** + * @inheritDoc + */ + public function newId() + { + return null; + } +} diff --git a/vendor/cakephp/database/Type/BatchCastingInterface.php b/vendor/cakephp/database/Type/BatchCastingInterface.php new file mode 100644 index 0000000..35f046b --- /dev/null +++ b/vendor/cakephp/database/Type/BatchCastingInterface.php @@ -0,0 +1,37 @@ + $fields The field keys to cast + * @param \Cake\Database\DriverInterface $driver Object from which database preferences and configuration will be extracted. + * @return array + */ + public function manyToPHP(array $values, array $fields, DriverInterface $driver): array; +} diff --git a/vendor/cakephp/database/Type/BinaryType.php b/vendor/cakephp/database/Type/BinaryType.php new file mode 100644 index 0000000..e570f53 --- /dev/null +++ b/vendor/cakephp/database/Type/BinaryType.php @@ -0,0 +1,92 @@ +convertStringToBinaryUuid($value); + } + + /** + * Generate a new binary UUID + * + * @return string A new primary key value. + */ + public function newId(): string + { + return Text::uuid(); + } + + /** + * Convert binary uuid into resource handles + * + * @param mixed $value The value to convert. + * @param \Cake\Database\DriverInterface $driver The driver instance to convert with. + * @return resource|string|null + * @throws \Cake\Core\Exception\CakeException + */ + public function toPHP($value, DriverInterface $driver) + { + if ($value === null) { + return null; + } + if (is_string($value)) { + return $this->convertBinaryUuidToString($value); + } + if (is_resource($value)) { + return $value; + } + + throw new CakeException(sprintf('Unable to convert %s into binary uuid.', gettype($value))); + } + + /** + * Get the correct PDO binding type for Binary data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\DriverInterface $driver The driver. + * @return int + */ + public function toStatement($value, DriverInterface $driver): int + { + return PDO::PARAM_LOB; + } + + /** + * Marshals flat data into PHP objects. + * + * Most useful for converting request data into PHP objects + * that make sense for the rest of the ORM/Database layers. + * + * @param mixed $value The value to convert. + * @return mixed Converted value. + */ + public function marshal($value) + { + return $value; + } + + /** + * Converts a binary uuid to a string representation + * + * @param mixed $binary The value to convert. + * @return string Converted value. + */ + protected function convertBinaryUuidToString($binary): string + { + $string = unpack('H*', $binary); + + $string = preg_replace( + '/([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})/', + '$1-$2-$3-$4-$5', + $string + ); + + return $string[1]; + } + + /** + * Converts a string UUID (36 or 32 char) to a binary representation. + * + * @param string $string The value to convert. + * @return string Converted value. + */ + protected function convertStringToBinaryUuid($string): string + { + $string = str_replace('-', '', $string); + + return pack('H*', $string); + } +} diff --git a/vendor/cakephp/database/Type/BoolType.php b/vendor/cakephp/database/Type/BoolType.php new file mode 100644 index 0000000..eeb1c88 --- /dev/null +++ b/vendor/cakephp/database/Type/BoolType.php @@ -0,0 +1,135 @@ +|null Array of column information, or `null` in case the column isn't processed by this type. + */ + public function convertColumnDefinition(array $definition, DriverInterface $driver): ?array; +} diff --git a/vendor/cakephp/database/Type/DateTimeFractionalType.php b/vendor/cakephp/database/Type/DateTimeFractionalType.php new file mode 100644 index 0000000..3cc149d --- /dev/null +++ b/vendor/cakephp/database/Type/DateTimeFractionalType.php @@ -0,0 +1,42 @@ + + */ + protected $_marshalFormats = [ + 'Y-m-d H:i', + 'Y-m-d H:i:s', + 'Y-m-d\TH:i', + 'Y-m-d\TH:i:s', + 'Y-m-d\TH:i:sP', + ]; + + /** + * Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`. + * + * @var bool + */ + protected $_useLocaleMarshal = false; + + /** + * The locale-aware format `marshal()` uses when `_useLocaleParser` is true. + * + * See `Cake\I18n\Time::parseDateTime()` for accepted formats. + * + * @var array|string|int + */ + protected $_localeMarshalFormat; + + /** + * The classname to use when creating objects. + * + * @var string + * @psalm-var class-string<\DateTime>|class-string<\DateTimeImmutable> + */ + protected $_className; + + /** + * Database time zone. + * + * @var \DateTimeZone|null + */ + protected $dbTimezone; + + /** + * User time zone. + * + * @var \DateTimeZone|null + */ + protected $userTimezone; + + /** + * Default time zone. + * + * @var \DateTimeZone + */ + protected $defaultTimezone; + + /** + * Whether database time zone is kept when converting + * + * @var bool + */ + protected $keepDatabaseTimezone = false; + + /** + * {@inheritDoc} + * + * @param string|null $name The name identifying this type + */ + public function __construct(?string $name = null) + { + parent::__construct($name); + + $this->defaultTimezone = new DateTimeZone(date_default_timezone_get()); + $this->_setClassName(FrozenTime::class, DateTimeImmutable::class); + } + + /** + * Convert DateTime instance into strings. + * + * @param mixed $value The value to convert. + * @param \Cake\Database\DriverInterface $driver The driver instance to convert with. + * @return string|null + */ + public function toDatabase($value, DriverInterface $driver): ?string + { + if ($value === null || is_string($value)) { + return $value; + } + if (is_int($value)) { + $class = $this->_className; + $value = new $class('@' . $value); + } + + if ( + $this->dbTimezone !== null + && $this->dbTimezone->getName() !== $value->getTimezone()->getName() + ) { + if (!$value instanceof DateTimeImmutable) { + $value = clone $value; + } + $value = $value->setTimezone($this->dbTimezone); + } + + return $value->format($this->_format); + } + + /** + * Alias for `setDatabaseTimezone()`. + * + * @param \DateTimeZone|string|null $timezone Database timezone. + * @return $this + * @deprecated 4.1.0 Use {@link setDatabaseTimezone()} instead. + */ + public function setTimezone($timezone) + { + deprecationWarning('DateTimeType::setTimezone() is deprecated. Use setDatabaseTimezone() instead.'); + + return $this->setDatabaseTimezone($timezone); + } + + /** + * Set database timezone. + * + * This is the time zone used when converting database strings to DateTime + * instances and converting DateTime instances to database strings. + * + * @see DateTimeType::setKeepDatabaseTimezone + * @param \DateTimeZone|string|null $timezone Database timezone. + * @return $this + */ + public function setDatabaseTimezone($timezone) + { + if (is_string($timezone)) { + $timezone = new DateTimeZone($timezone); + } + $this->dbTimezone = $timezone; + + return $this; + } + + /** + * Set user timezone. + * + * This is the time zone used when marshalling strings to DateTime instances. + * + * @param \DateTimeZone|string|null $timezone User timezone. + * @return $this + */ + public function setUserTimezone($timezone) + { + if (is_string($timezone)) { + $timezone = new DateTimeZone($timezone); + } + $this->userTimezone = $timezone; + + return $this; + } + + /** + * {@inheritDoc} + * + * @param mixed $value Value to be converted to PHP equivalent + * @param \Cake\Database\DriverInterface $driver Object from which database preferences and configuration will be extracted + * @return \DateTimeInterface|null + */ + public function toPHP($value, DriverInterface $driver) + { + if ($value === null) { + return null; + } + + $class = $this->_className; + if (is_int($value)) { + $instance = new $class('@' . $value); + } else { + if (strpos($value, '0000-00-00') === 0) { + return null; + } + $instance = new $class($value, $this->dbTimezone); + } + + if ( + !$this->keepDatabaseTimezone && + $instance->getTimezone()->getName() !== $this->defaultTimezone->getName() + ) { + $instance = $instance->setTimezone($this->defaultTimezone); + } + + if ($this->setToDateStart) { + $instance = $instance->setTime(0, 0, 0); + } + + return $instance; + } + + /** + * Set whether DateTime object created from database string is converted + * to default time zone. + * + * If your database date times are in a specific time zone that you want + * to keep in the DateTime instance then set this to true. + * + * When false, datetime timezones are converted to default time zone. + * This is default behavior. + * + * @param bool $keep If true, database time zone is kept when converting + * to DateTime instances. + * @return $this + */ + public function setKeepDatabaseTimezone(bool $keep) + { + $this->keepDatabaseTimezone = $keep; + + return $this; + } + + /** + * @inheritDoc + */ + public function manyToPHP(array $values, array $fields, DriverInterface $driver): array + { + foreach ($fields as $field) { + if (!isset($values[$field])) { + continue; + } + + $value = $values[$field]; + if (strpos($value, '0000-00-00') === 0) { + $values[$field] = null; + continue; + } + + $class = $this->_className; + if (is_int($value)) { + $instance = new $class('@' . $value); + } else { + $instance = new $class($value, $this->dbTimezone); + } + + if ( + !$this->keepDatabaseTimezone && + $instance->getTimezone()->getName() !== $this->defaultTimezone->getName() + ) { + $instance = $instance->setTimezone($this->defaultTimezone); + } + + if ($this->setToDateStart) { + $instance = $instance->setTime(0, 0, 0); + } + + $values[$field] = $instance; + } + + return $values; + } + + /** + * Convert request data into a datetime object. + * + * @param mixed $value Request data + * @return \DateTimeInterface|null + */ + public function marshal($value): ?DateTimeInterface + { + if ($value instanceof DateTimeInterface) { + if ($value instanceof DateTime) { + $value = clone $value; + } + + /** @var \Datetime|\DateTimeImmutable $value */ + return $value->setTimezone($this->defaultTimezone); + } + + /** @var class-string<\DatetimeInterface> $class */ + $class = $this->_className; + try { + if ($value === '' || $value === null || is_bool($value)) { + return null; + } + + if (is_int($value) || (is_string($value) && ctype_digit($value))) { + /** @var \Datetime|\DateTimeImmutable $dateTime */ + $dateTime = new $class('@' . $value); + + return $dateTime->setTimezone($this->defaultTimezone); + } + + if (is_string($value)) { + if ($this->_useLocaleMarshal) { + $dateTime = $this->_parseLocaleValue($value); + } else { + $dateTime = $this->_parseValue($value); + } + + /** @var \Datetime|\DateTimeImmutable $dateTime */ + if ($dateTime !== null) { + $dateTime = $dateTime->setTimezone($this->defaultTimezone); + } + + return $dateTime; + } + } catch (Exception $e) { + return null; + } + + if (is_array($value) && implode('', $value) === '') { + return null; + } + $value += ['hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0]; + + $format = ''; + if ( + isset($value['year'], $value['month'], $value['day']) && + ( + is_numeric($value['year']) && + is_numeric($value['month']) && + is_numeric($value['day']) + ) + ) { + $format .= sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']); + } + + if (isset($value['meridian']) && (int)$value['hour'] === 12) { + $value['hour'] = 0; + } + if (isset($value['meridian'])) { + $value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12; + } + $format .= sprintf( + '%s%02d:%02d:%02d.%06d', + empty($format) ? '' : ' ', + $value['hour'], + $value['minute'], + $value['second'], + $value['microsecond'] + ); + + /** @var \Datetime|\DateTimeImmutable $dateTime */ + $dateTime = new $class($format, $value['timezone'] ?? $this->userTimezone); + + return $dateTime->setTimezone($this->defaultTimezone); + } + + /** + * Sets whether to parse strings passed to `marshal()` using + * the locale-aware format set by `setLocaleFormat()`. + * + * @param bool $enable Whether to enable + * @return $this + */ + public function useLocaleParser(bool $enable = true) + { + if ($enable === false) { + $this->_useLocaleMarshal = $enable; + + return $this; + } + if (is_subclass_of($this->_className, I18nDateTimeInterface::class)) { + $this->_useLocaleMarshal = $enable; + + return $this; + } + throw new RuntimeException( + sprintf('Cannot use locale parsing with the %s class', $this->_className) + ); + } + + /** + * Sets the locale-aware format used by `marshal()` when parsing strings. + * + * See `Cake\I18n\Time::parseDateTime()` for accepted formats. + * + * @param array|string $format The locale-aware format + * @see \Cake\I18n\Time::parseDateTime() + * @return $this + */ + public function setLocaleFormat($format) + { + $this->_localeMarshalFormat = $format; + + return $this; + } + + /** + * Change the preferred class name to the FrozenTime implementation. + * + * @return $this + * @deprecated 4.3.0 This method is no longer needed as using immutable datetime class is the default behavior. + */ + public function useImmutable() + { + deprecationWarning( + 'Configuring immutable or mutable classes is deprecated and immutable' + . ' classes will be the permanent configuration in 5.0. Calling `useImmutable()` is unnecessary.' + ); + + $this->_setClassName(FrozenTime::class, DateTimeImmutable::class); + + return $this; + } + + /** + * Set the classname to use when building objects. + * + * @param string $class The classname to use. + * @param string $fallback The classname to use when the preferred class does not exist. + * @return void + * @psalm-param class-string<\DateTime>|class-string<\DateTimeImmutable> $class + * @psalm-param class-string<\DateTime>|class-string<\DateTimeImmutable> $fallback + */ + protected function _setClassName(string $class, string $fallback): void + { + if (!class_exists($class)) { + $class = $fallback; + } + $this->_className = $class; + } + + /** + * Get the classname used for building objects. + * + * @return string + * @psalm-return class-string<\DateTime>|class-string<\DateTimeImmutable> + */ + public function getDateTimeClassName(): string + { + return $this->_className; + } + + /** + * Change the preferred class name to the mutable Time implementation. + * + * @return $this + * @deprecated 4.3.0 Using mutable datetime objects is deprecated. + */ + public function useMutable() + { + deprecationWarning( + 'Configuring immutable or mutable classes is deprecated and immutable' + . ' classes will be the permanent configuration in 5.0. Calling `useImmutable()` is unnecessary.' + ); + + $this->_setClassName(Time::class, DateTime::class); + + return $this; + } + + /** + * Converts a string into a DateTime object after parsing it using the locale + * aware parser with the format set by `setLocaleFormat()`. + * + * @param string $value The value to parse and convert to an object. + * @return \Cake\I18n\I18nDateTimeInterface|null + */ + protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface + { + /** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $class */ + $class = $this->_className; + + return $class::parseDateTime($value, $this->_localeMarshalFormat, $this->userTimezone); + } + + /** + * Converts a string into a DateTime object after parsing it using the + * formats in `_marshalFormats`. + * + * @param string $value The value to parse and convert to an object. + * @return \DateTimeInterface|null + */ + protected function _parseValue(string $value): ?DateTimeInterface + { + $class = $this->_className; + + foreach ($this->_marshalFormats as $format) { + try { + $dateTime = $class::createFromFormat($format, $value, $this->userTimezone); + // Check for false in case DateTime is used directly + if ($dateTime !== false) { + return $dateTime; + } + } catch (InvalidArgumentException $e) { + // Chronos wraps DateTime::createFromFormat and throws + // exception if parse fails. + continue; + } + } + + return null; + } + + /** + * Casts given value to Statement equivalent + * + * @param mixed $value value to be converted to PDO statement + * @param \Cake\Database\DriverInterface $driver object from which database preferences and configuration will be extracted + * @return mixed + */ + public function toStatement($value, DriverInterface $driver) + { + return PDO::PARAM_STR; + } +} diff --git a/vendor/cakephp/database/Type/DateType.php b/vendor/cakephp/database/Type/DateType.php new file mode 100644 index 0000000..19d5453 --- /dev/null +++ b/vendor/cakephp/database/Type/DateType.php @@ -0,0 +1,125 @@ +_setClassName(FrozenDate::class, DateTimeImmutable::class); + } + + /** + * Change the preferred class name to the FrozenDate implementation. + * + * @return $this + * @deprecated 4.3.0 This method is no longer needed as using immutable datetime class is the default behavior. + */ + public function useImmutable() + { + deprecationWarning( + 'Configuring immutable or mutable classes is deprecated and immutable' + . ' classes will be the permanent configuration in 5.0. Calling `useImmutable()` is unnecessary.' + ); + + $this->_setClassName(FrozenDate::class, DateTimeImmutable::class); + + return $this; + } + + /** + * Change the preferred class name to the mutable Date implementation. + * + * @return $this + * @deprecated 4.3.0 Using mutable datetime objects is deprecated. + */ + public function useMutable() + { + deprecationWarning( + 'Configuring immutable or mutable classes is deprecated and immutable' + . ' classes will be the permanent configuration in 5.0. Calling `useImmutable()` is unnecessary.' + ); + + $this->_setClassName(Date::class, DateTime::class); + + return $this; + } + + /** + * Convert request data into a datetime object. + * + * @param mixed $value Request data + * @return \DateTimeInterface|null + */ + public function marshal($value): ?DateTimeInterface + { + $date = parent::marshal($value); + /** @psalm-var \DateTime|\DateTimeImmutable|null $date */ + if ($date && !$date instanceof I18nDateTimeInterface) { + // Clear time manually when I18n types aren't available and raw DateTime used + $date = $date->setTime(0, 0, 0); + } + + return $date; + } + + /** + * @inheritDoc + */ + protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface + { + /** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $class */ + $class = $this->_className; + + return $class::parseDate($value, $this->_localeMarshalFormat); + } +} diff --git a/vendor/cakephp/database/Type/DecimalType.php b/vendor/cakephp/database/Type/DecimalType.php new file mode 100644 index 0000000..cb332fd --- /dev/null +++ b/vendor/cakephp/database/Type/DecimalType.php @@ -0,0 +1,189 @@ +_useLocaleParser) { + return $this->_parseValue($value); + } + if (is_numeric($value)) { + return (string)$value; + } + if (is_string($value) && preg_match('/^[0-9,. ]+$/', $value)) { + return $value; + } + + return null; + } + + /** + * Sets whether to parse numbers passed to the marshal() function + * by using a locale aware parser. + * + * @param bool $enable Whether to enable + * @return $this + * @throws \RuntimeException + */ + public function useLocaleParser(bool $enable = true) + { + if ($enable === false) { + $this->_useLocaleParser = $enable; + + return $this; + } + if ( + static::$numberClass === Number::class || + is_subclass_of(static::$numberClass, Number::class) + ) { + $this->_useLocaleParser = $enable; + + return $this; + } + throw new RuntimeException( + sprintf('Cannot use locale parsing with the %s class', static::$numberClass) + ); + } + + /** + * Converts localized string into a decimal string after parsing it using + * the locale aware parser. + * + * @param string $value The value to parse and convert to an float. + * @return string + */ + protected function _parseValue(string $value): string + { + /** @var \Cake\I18n\Number $class */ + $class = static::$numberClass; + + return (string)$class::parseFloat($value); + } +} diff --git a/vendor/cakephp/database/Type/ExpressionTypeCasterTrait.php b/vendor/cakephp/database/Type/ExpressionTypeCasterTrait.php new file mode 100644 index 0000000..621bb9d --- /dev/null +++ b/vendor/cakephp/database/Type/ExpressionTypeCasterTrait.php @@ -0,0 +1,80 @@ +toExpression($value); + } + + /** + * Returns an array with the types that require values to + * be casted to expressions, out of the list of type names + * passed as parameter. + * + * @param array $types List of type names + * @return array + */ + protected function _requiresToExpressionCasting(array $types): array + { + $result = []; + $types = array_filter($types); + foreach ($types as $k => $type) { + $object = TypeFactory::build($type); + if ($object instanceof ExpressionTypeInterface) { + $result[$k] = $object; + } + } + + return $result; + } +} diff --git a/vendor/cakephp/database/Type/ExpressionTypeInterface.php b/vendor/cakephp/database/Type/ExpressionTypeInterface.php new file mode 100644 index 0000000..7615cf9 --- /dev/null +++ b/vendor/cakephp/database/Type/ExpressionTypeInterface.php @@ -0,0 +1,36 @@ +_useLocaleParser) { + return $this->_parseValue($value); + } + if (is_numeric($value)) { + return (float)$value; + } + if (is_string($value) && preg_match('/^[0-9,. ]+$/', $value)) { + return $value; + } + + return null; + } + + /** + * Sets whether to parse numbers passed to the marshal() function + * by using a locale aware parser. + * + * @param bool $enable Whether to enable + * @return $this + */ + public function useLocaleParser(bool $enable = true) + { + if ($enable === false) { + $this->_useLocaleParser = $enable; + + return $this; + } + if ( + static::$numberClass === Number::class || + is_subclass_of(static::$numberClass, Number::class) + ) { + $this->_useLocaleParser = $enable; + + return $this; + } + throw new RuntimeException( + sprintf('Cannot use locale parsing with the %s class', static::$numberClass) + ); + } + + /** + * Converts a string into a float point after parsing it using the locale + * aware parser. + * + * @param string $value The value to parse and convert to an float. + * @return float + */ + protected function _parseValue(string $value): float + { + $class = static::$numberClass; + + return $class::parseFloat($value); + } +} diff --git a/vendor/cakephp/database/Type/IntegerType.php b/vendor/cakephp/database/Type/IntegerType.php new file mode 100644 index 0000000..212b1aa --- /dev/null +++ b/vendor/cakephp/database/Type/IntegerType.php @@ -0,0 +1,128 @@ +checkNumeric($value); + + return (int)$value; + } + + /** + * {@inheritDoc} + * + * @param mixed $value The value to convert. + * @param \Cake\Database\DriverInterface $driver The driver instance to convert with. + * @return int|null + */ + public function toPHP($value, DriverInterface $driver): ?int + { + if ($value === null) { + return null; + } + + return (int)$value; + } + + /** + * @inheritDoc + */ + public function manyToPHP(array $values, array $fields, DriverInterface $driver): array + { + foreach ($fields as $field) { + if (!isset($values[$field])) { + continue; + } + + $this->checkNumeric($values[$field]); + + $values[$field] = (int)$values[$field]; + } + + return $values; + } + + /** + * Get the correct PDO binding type for integer data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\DriverInterface $driver The driver. + * @return int + */ + public function toStatement($value, DriverInterface $driver): int + { + return PDO::PARAM_INT; + } + + /** + * Marshals request data into PHP floats. + * + * @param mixed $value The value to convert. + * @return int|null Converted value. + */ + public function marshal($value): ?int + { + if ($value === null || $value === '') { + return null; + } + if (is_numeric($value)) { + return (int)$value; + } + + return null; + } +} diff --git a/vendor/cakephp/database/Type/JsonType.php b/vendor/cakephp/database/Type/JsonType.php new file mode 100644 index 0000000..5648d94 --- /dev/null +++ b/vendor/cakephp/database/Type/JsonType.php @@ -0,0 +1,124 @@ +_encodingOptions); + } + + /** + * {@inheritDoc} + * + * @param mixed $value The value to convert. + * @param \Cake\Database\DriverInterface $driver The driver instance to convert with. + * @return array|string|null + */ + public function toPHP($value, DriverInterface $driver) + { + if (!is_string($value)) { + return null; + } + + return json_decode($value, true); + } + + /** + * @inheritDoc + */ + public function manyToPHP(array $values, array $fields, DriverInterface $driver): array + { + foreach ($fields as $field) { + if (!isset($values[$field])) { + continue; + } + + $values[$field] = json_decode($values[$field], true); + } + + return $values; + } + + /** + * Get the correct PDO binding type for string data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\DriverInterface $driver The driver. + * @return int + */ + public function toStatement($value, DriverInterface $driver): int + { + return PDO::PARAM_STR; + } + + /** + * Marshals request data into a JSON compatible structure. + * + * @param mixed $value The value to convert. + * @return mixed Converted value. + */ + public function marshal($value) + { + return $value; + } + + /** + * Set json_encode options. + * + * @param int $options Encoding flags. Use JSON_* flags. Set `0` to reset. + * @return $this + * @see https://www.php.net/manual/en/function.json-encode.php + */ + public function setEncodingOptions(int $options) + { + $this->_encodingOptions = $options; + + return $this; + } +} diff --git a/vendor/cakephp/database/Type/OptionalConvertInterface.php b/vendor/cakephp/database/Type/OptionalConvertInterface.php new file mode 100644 index 0000000..512683b --- /dev/null +++ b/vendor/cakephp/database/Type/OptionalConvertInterface.php @@ -0,0 +1,32 @@ +__toString(); + } + + if (is_scalar($value)) { + return (string)$value; + } + + throw new InvalidArgumentException(sprintf( + 'Cannot convert value of type `%s` to string', + getTypeName($value) + )); + } + + /** + * Convert string values to PHP strings. + * + * @param mixed $value The value to convert. + * @param \Cake\Database\DriverInterface $driver The driver instance to convert with. + * @return string|null + */ + public function toPHP($value, DriverInterface $driver): ?string + { + if ($value === null) { + return null; + } + + return (string)$value; + } + + /** + * Get the correct PDO binding type for string data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\DriverInterface $driver The driver. + * @return int + */ + public function toStatement($value, DriverInterface $driver): int + { + return PDO::PARAM_STR; + } + + /** + * Marshals request data into PHP strings. + * + * @param mixed $value The value to convert. + * @return string|null Converted value. + */ + public function marshal($value): ?string + { + if ($value === null || is_array($value)) { + return null; + } + + return (string)$value; + } + + /** + * {@inheritDoc} + * + * @return bool False as database results are returned already as strings + */ + public function requiresToPhpCast(): bool + { + return false; + } +} diff --git a/vendor/cakephp/database/Type/TimeType.php b/vendor/cakephp/database/Type/TimeType.php new file mode 100644 index 0000000..4c021e2 --- /dev/null +++ b/vendor/cakephp/database/Type/TimeType.php @@ -0,0 +1,52 @@ + $class */ + $class = $this->_className; + + /** @psalm-suppress PossiblyInvalidArgument */ + return $class::parseTime($value, $this->_localeMarshalFormat); + } +} diff --git a/vendor/cakephp/database/Type/UuidType.php b/vendor/cakephp/database/Type/UuidType.php new file mode 100644 index 0000000..ce7f754 --- /dev/null +++ b/vendor/cakephp/database/Type/UuidType.php @@ -0,0 +1,67 @@ +toDatabase($value, $this->_driver); + $type = $type->toStatement($value, $this->_driver); + } + + return [$value, $type]; + } + + /** + * Matches columns to corresponding types + * + * Both $columns and $types should either be numeric based or string key based at + * the same time. + * + * @param array $columns list or associative array of columns and parameters to be bound with types + * @param array $types list or associative array of types + * @return array + */ + public function matchTypes(array $columns, array $types): array + { + if (!is_int(key($types))) { + $positions = array_intersect_key(array_flip($columns), $types); + $types = array_intersect_key($types, $positions); + $types = array_combine($positions, $types); + } + + return $types; + } +} diff --git a/vendor/cakephp/database/TypeFactory.php b/vendor/cakephp/database/TypeFactory.php new file mode 100644 index 0000000..2f50066 --- /dev/null +++ b/vendor/cakephp/database/TypeFactory.php @@ -0,0 +1,164 @@ + + * @psalm-var array> + */ + protected static $_types = [ + 'tinyinteger' => Type\IntegerType::class, + 'smallinteger' => Type\IntegerType::class, + 'integer' => Type\IntegerType::class, + 'biginteger' => Type\IntegerType::class, + 'binary' => Type\BinaryType::class, + 'binaryuuid' => Type\BinaryUuidType::class, + 'boolean' => Type\BoolType::class, + 'date' => Type\DateType::class, + 'datetime' => Type\DateTimeType::class, + 'datetimefractional' => Type\DateTimeFractionalType::class, + 'decimal' => Type\DecimalType::class, + 'float' => Type\FloatType::class, + 'json' => Type\JsonType::class, + 'string' => Type\StringType::class, + 'char' => Type\StringType::class, + 'text' => Type\StringType::class, + 'time' => Type\TimeType::class, + 'timestamp' => Type\DateTimeType::class, + 'timestampfractional' => Type\DateTimeFractionalType::class, + 'timestamptimezone' => Type\DateTimeTimezoneType::class, + 'uuid' => Type\UuidType::class, + ]; + + /** + * Contains a map of type object instances to be reused if needed. + * + * @var array<\Cake\Database\TypeInterface> + */ + protected static $_builtTypes = []; + + /** + * Returns a Type object capable of converting a type identified by name. + * + * @param string $name type identifier + * @throws \InvalidArgumentException If type identifier is unknown + * @return \Cake\Database\TypeInterface + */ + public static function build(string $name): TypeInterface + { + if (isset(static::$_builtTypes[$name])) { + return static::$_builtTypes[$name]; + } + if (!isset(static::$_types[$name])) { + throw new InvalidArgumentException(sprintf('Unknown type "%s"', $name)); + } + + return static::$_builtTypes[$name] = new static::$_types[$name]($name); + } + + /** + * Returns an arrays with all the mapped type objects, indexed by name. + * + * @return array<\Cake\Database\TypeInterface> + */ + public static function buildAll(): array + { + $result = []; + foreach (static::$_types as $name => $type) { + $result[$name] = static::$_builtTypes[$name] ?? static::build($name); + } + + return $result; + } + + /** + * Set TypeInterface instance capable of converting a type identified by $name + * + * @param string $name The type identifier you want to set. + * @param \Cake\Database\TypeInterface $instance The type instance you want to set. + * @return void + */ + public static function set(string $name, TypeInterface $instance): void + { + static::$_builtTypes[$name] = $instance; + static::$_types[$name] = get_class($instance); + } + + /** + * Registers a new type identifier and maps it to a fully namespaced classname. + * + * @param string $type Name of type to map. + * @param string $className The classname to register. + * @return void + * @psalm-param class-string<\Cake\Database\TypeInterface> $className + */ + public static function map(string $type, string $className): void + { + static::$_types[$type] = $className; + unset(static::$_builtTypes[$type]); + } + + /** + * Set type to classname mapping. + * + * @param array $map List of types to be mapped. + * @return void + * @psalm-param array> $map + */ + public static function setMap(array $map): void + { + static::$_types = $map; + static::$_builtTypes = []; + } + + /** + * Get mapped class name for given type or map array. + * + * @param string|null $type Type name to get mapped class for or null to get map array. + * @return array|string|null Configured class name for given $type or map array. + */ + public static function getMap(?string $type = null) + { + if ($type === null) { + return static::$_types; + } + + return static::$_types[$type] ?? null; + } + + /** + * Clears out all created instances and mapped types classes, useful for testing + * + * @return void + */ + public static function clear(): void + { + static::$_types = []; + static::$_builtTypes = []; + } +} diff --git a/vendor/cakephp/database/TypeInterface.php b/vendor/cakephp/database/TypeInterface.php new file mode 100644 index 0000000..fca0fdb --- /dev/null +++ b/vendor/cakephp/database/TypeInterface.php @@ -0,0 +1,91 @@ + + */ + protected $_defaults = []; + + /** + * Associative array with the fields and the related types that override defaults this query might contain + * + * Used to avoid repetition when calling multiple functions inside this class that + * may require a custom type for a specific field. + * + * @var array + */ + protected $_types = []; + + /** + * Creates an instance with the given defaults + * + * @param array $defaults The defaults to use. + */ + public function __construct(array $defaults = []) + { + $this->setDefaults($defaults); + } + + /** + * Configures a map of fields and associated type. + * + * These values will be used as the default mapping of types for every function + * in this instance that supports a `$types` param. + * + * This method is useful when you want to avoid repeating type definitions + * as setting types overwrites the last set of types. + * + * ### Example + * + * ``` + * $query->setDefaults(['created' => 'datetime', 'is_visible' => 'boolean']); + * ``` + * + * This method will replace all the existing default mappings with the ones provided. + * To add into the mappings use `addDefaults()`. + * + * @param array $defaults Associative array where keys are field names and values + * are the correspondent type. + * @return $this + */ + public function setDefaults(array $defaults) + { + $this->_defaults = $defaults; + + return $this; + } + + /** + * Returns the currently configured types. + * + * @return array + */ + public function getDefaults(): array + { + return $this->_defaults; + } + + /** + * Add additional default types into the type map. + * + * If a key already exists it will not be overwritten. + * + * @param array $types The additional types to add. + * @return void + */ + public function addDefaults(array $types): void + { + $this->_defaults += $types; + } + + /** + * Sets a map of fields and their associated types for single-use. + * + * ### Example + * + * ``` + * $query->setTypes(['created' => 'time']); + * ``` + * + * This method will replace all the existing type maps with the ones provided. + * + * @param array $types Associative array where keys are field names and values + * are the correspondent type. + * @return $this + */ + public function setTypes(array $types) + { + $this->_types = $types; + + return $this; + } + + /** + * Gets a map of fields and their associated types for single-use. + * + * @return array + */ + public function getTypes(): array + { + return $this->_types; + } + + /** + * Returns the type of the given column. If there is no single use type is configured, + * the column type will be looked for inside the default mapping. If neither exist, + * null will be returned. + * + * @param string|int $column The type for a given column + * @return string|null + */ + public function type($column): ?string + { + return $this->_types[$column] ?? $this->_defaults[$column] ?? null; + } + + /** + * Returns an array of all types mapped types + * + * @return array + */ + public function toArray(): array + { + return $this->_types + $this->_defaults; + } +} diff --git a/vendor/cakephp/database/TypeMapTrait.php b/vendor/cakephp/database/TypeMapTrait.php new file mode 100644 index 0000000..d330709 --- /dev/null +++ b/vendor/cakephp/database/TypeMapTrait.php @@ -0,0 +1,89 @@ +_typeMap = is_array($typeMap) ? new TypeMap($typeMap) : $typeMap; + + return $this; + } + + /** + * Returns the existing type map. + * + * @return \Cake\Database\TypeMap + */ + public function getTypeMap(): TypeMap + { + if ($this->_typeMap === null) { + $this->_typeMap = new TypeMap(); + } + + return $this->_typeMap; + } + + /** + * Overwrite the default type mappings for fields + * in the implementing object. + * + * This method is useful if you need to set type mappings that are shared across + * multiple functions/expressions in a query. + * + * To add a default without overwriting existing ones + * use `getTypeMap()->addDefaults()` + * + * @param array $types The array of types to set. + * @return $this + * @see \Cake\Database\TypeMap::setDefaults() + */ + public function setDefaultTypes(array $types) + { + $this->getTypeMap()->setDefaults($types); + + return $this; + } + + /** + * Gets default types of current type map. + * + * @return array + */ + public function getDefaultTypes(): array + { + return $this->getTypeMap()->getDefaults(); + } +} diff --git a/vendor/cakephp/database/TypedResultInterface.php b/vendor/cakephp/database/TypedResultInterface.php new file mode 100644 index 0000000..434ff08 --- /dev/null +++ b/vendor/cakephp/database/TypedResultInterface.php @@ -0,0 +1,38 @@ +_returnType; + } + + /** + * Sets the type of the value this object will generate. + * + * @param string $type The name of the type that is to be returned + * @return $this + */ + public function setReturnType(string $type) + { + $this->_returnType = $type; + + return $this; + } +} diff --git a/vendor/cakephp/database/ValueBinder.php b/vendor/cakephp/database/ValueBinder.php new file mode 100644 index 0000000..1a63a57 --- /dev/null +++ b/vendor/cakephp/database/ValueBinder.php @@ -0,0 +1,151 @@ +_bindings[$param] = compact('value', 'type') + [ + 'placeholder' => is_int($param) ? $param : substr($param, 1), + ]; + } + + /** + * Creates a unique placeholder name if the token provided does not start with ":" + * otherwise, it will return the same string and internally increment the number + * of placeholders generated by this object. + * + * @param string $token string from which the placeholder will be derived from, + * if it starts with a colon, then the same string is returned + * @return string to be used as a placeholder in a query expression + */ + public function placeholder(string $token): string + { + $number = $this->_bindingsCount++; + if ($token[0] !== ':' && $token !== '?') { + $token = sprintf(':%s%s', $token, $number); + } + + return $token; + } + + /** + * Creates unique named placeholders for each of the passed values + * and binds them with the specified type. + * + * @param iterable $values The list of values to be bound + * @param string|int|null $type The type with which all values will be bound + * @return array with the placeholders to insert in the query + */ + public function generateManyNamed(iterable $values, $type = null): array + { + $placeholders = []; + foreach ($values as $k => $value) { + $param = $this->placeholder('c'); + $this->_bindings[$param] = [ + 'value' => $value, + 'type' => $type, + 'placeholder' => substr($param, 1), + ]; + $placeholders[$k] = $param; + } + + return $placeholders; + } + + /** + * Returns all values bound to this expression object at this nesting level. + * Subexpression bound values will not be returned with this function. + * + * @return array + */ + public function bindings(): array + { + return $this->_bindings; + } + + /** + * Clears any bindings that were previously registered + * + * @return void + */ + public function reset(): void + { + $this->_bindings = []; + $this->_bindingsCount = 0; + } + + /** + * Resets the bindings count without clearing previously bound values + * + * @return void + */ + public function resetCount(): void + { + $this->_bindingsCount = 0; + } + + /** + * Binds all the stored values in this object to the passed statement. + * + * @param \Cake\Database\StatementInterface $statement The statement to add parameters to. + * @return void + */ + public function attachTo(StatementInterface $statement): void + { + $bindings = $this->bindings(); + if (empty($bindings)) { + return; + } + + foreach ($bindings as $b) { + $statement->bindValue($b['placeholder'], $b['value'], $b['type']); + } + } +} diff --git a/vendor/cakephp/database/composer.json b/vendor/cakephp/database/composer.json new file mode 100644 index 0000000..9325117 --- /dev/null +++ b/vendor/cakephp/database/composer.json @@ -0,0 +1,39 @@ +{ + "name": "cakephp/database", + "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API", + "type": "library", + "keywords": [ + "cakephp", + "database", + "abstraction", + "database abstraction", + "pdo" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/database/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/database" + }, + "require": { + "php": ">=7.2.0", + "cakephp/core": "^4.0", + "cakephp/datasource": "^4.0" + }, + "suggest": { + "cakephp/i18n": "If you are using locale-aware datetime formats or Chronos types." + }, + "autoload": { + "psr-4": { + "Cake\\Database\\": "." + } + } +} diff --git a/vendor/cakephp/datasource/ConnectionInterface.php b/vendor/cakephp/datasource/ConnectionInterface.php new file mode 100644 index 0000000..fcf96b6 --- /dev/null +++ b/vendor/cakephp/datasource/ConnectionInterface.php @@ -0,0 +1,146 @@ +transactional(function ($connection) { + * $connection->newQuery()->delete('users')->execute(); + * }); + * ``` + * + * @param callable $callback The callback to execute within a transaction. + * @return mixed The return value of the callback. + * @throws \Exception Will re-throw any exception raised in $callback after + * rolling back the transaction. + */ + public function transactional(callable $callback); + + /** + * Run an operation with constraints disabled. + * + * Constraints should be re-enabled after the callback succeeds/fails. + * + * ### Example: + * + * ``` + * $connection->disableConstraints(function ($connection) { + * $connection->newQuery()->delete('users')->execute(); + * }); + * ``` + * + * @param callable $callback The callback to execute within a transaction. + * @return mixed The return value of the callback. + * @throws \Exception Will re-throw any exception raised in $callback after + * rolling back the transaction. + */ + public function disableConstraints(callable $callback); + + /** + * Enable/disable query logging + * + * @param bool $enable Enable/disable query logging + * @return $this + */ + public function enableQueryLogging(bool $enable = true); + + /** + * Disable query logging + * + * @return $this + */ + public function disableQueryLogging(); + + /** + * Check if query logging is enabled. + * + * @return bool + */ + public function isQueryLoggingEnabled(): bool; +} diff --git a/vendor/cakephp/datasource/ConnectionManager.php b/vendor/cakephp/datasource/ConnectionManager.php new file mode 100644 index 0000000..54adf54 --- /dev/null +++ b/vendor/cakephp/datasource/ConnectionManager.php @@ -0,0 +1,206 @@ + + */ + protected static $_aliasMap = []; + + /** + * An array mapping url schemes to fully qualified driver class names + * + * @var array + * @psalm-var array + */ + protected static $_dsnClassMap = [ + 'mysql' => Mysql::class, + 'postgres' => Postgres::class, + 'sqlite' => Sqlite::class, + 'sqlserver' => Sqlserver::class, + ]; + + /** + * The ConnectionRegistry used by the manager. + * + * @var \Cake\Datasource\ConnectionRegistry + */ + protected static $_registry; + + /** + * Configure a new connection object. + * + * The connection will not be constructed until it is first used. + * + * @param array|string $key The name of the connection config, or an array of multiple configs. + * @param array|null $config An array of name => config data for adapter. + * @return void + * @throws \Cake\Core\Exception\CakeException When trying to modify an existing config. + * @see \Cake\Core\StaticConfigTrait::config() + */ + public static function setConfig($key, $config = null): void + { + if (is_array($config)) { + $config['name'] = $key; + } + + static::_setConfig($key, $config); + } + + /** + * Parses a DSN into a valid connection configuration + * + * This method allows setting a DSN using formatting similar to that used by PEAR::DB. + * The following is an example of its usage: + * + * ``` + * $dsn = 'mysql://user:pass@localhost/database'; + * $config = ConnectionManager::parseDsn($dsn); + * + * $dsn = 'Cake\Database\Driver\Mysql://localhost:3306/database?className=Cake\Database\Connection'; + * $config = ConnectionManager::parseDsn($dsn); + * + * $dsn = 'Cake\Database\Connection://localhost:3306/database?driver=Cake\Database\Driver\Mysql'; + * $config = ConnectionManager::parseDsn($dsn); + * ``` + * + * For all classes, the value of `scheme` is set as the value of both the `className` and `driver` + * unless they have been otherwise specified. + * + * Note that query-string arguments are also parsed and set as values in the returned configuration. + * + * @param string $config The DSN string to convert to a configuration array + * @return array The configuration array to be stored after parsing the DSN + */ + public static function parseDsn(string $config): array + { + $config = static::_parseDsn($config); + + if (isset($config['path']) && empty($config['database'])) { + $config['database'] = substr($config['path'], 1); + } + + if (empty($config['driver'])) { + $config['driver'] = $config['className']; + $config['className'] = Connection::class; + } + + unset($config['path']); + + return $config; + } + + /** + * Set one or more connection aliases. + * + * Connection aliases allow you to rename active connections without overwriting + * the aliased connection. This is most useful in the test-suite for replacing + * connections with their test variant. + * + * Defined aliases will take precedence over normal connection names. For example, + * if you alias 'default' to 'test', fetching 'default' will always return the 'test' + * connection as long as the alias is defined. + * + * You can remove aliases with ConnectionManager::dropAlias(). + * + * ### Usage + * + * ``` + * // Make 'things' resolve to 'test_things' connection + * ConnectionManager::alias('test_things', 'things'); + * ``` + * + * @param string $source The existing connection to alias. + * @param string $alias The alias name that resolves to `$source`. + * @return void + */ + public static function alias(string $source, string $alias): void + { + static::$_aliasMap[$alias] = $source; + } + + /** + * Drop an alias. + * + * Removes an alias from ConnectionManager. Fetching the aliased + * connection may fail if there is no other connection with that name. + * + * @param string $alias The connection alias to drop + * @return void + */ + public static function dropAlias(string $alias): void + { + unset(static::$_aliasMap[$alias]); + } + + /** + * Get a connection. + * + * If the connection has not been constructed an instance will be added + * to the registry. This method will use any aliases that have been + * defined. If you want the original unaliased connections pass `false` + * as second parameter. + * + * @param string $name The connection name. + * @param bool $useAliases Set to false to not use aliased connections. + * @return \Cake\Datasource\ConnectionInterface A connection object. + * @throws \Cake\Datasource\Exception\MissingDatasourceConfigException When config + * data is missing. + */ + public static function get(string $name, bool $useAliases = true) + { + if ($useAliases && isset(static::$_aliasMap[$name])) { + $name = static::$_aliasMap[$name]; + } + if (empty(static::$_config[$name])) { + throw new MissingDatasourceConfigException(['name' => $name]); + } + /** @psalm-suppress RedundantPropertyInitializationCheck */ + if (!isset(static::$_registry)) { + static::$_registry = new ConnectionRegistry(); + } + + return static::$_registry->{$name} + ?? static::$_registry->load($name, static::$_config[$name]); + } +} diff --git a/vendor/cakephp/datasource/ConnectionRegistry.php b/vendor/cakephp/datasource/ConnectionRegistry.php new file mode 100644 index 0000000..34a5aca --- /dev/null +++ b/vendor/cakephp/datasource/ConnectionRegistry.php @@ -0,0 +1,104 @@ + + */ +class ConnectionRegistry extends ObjectRegistry +{ + /** + * Resolve a datasource classname. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * @param string $class Partial classname to resolve. + * @return string|null Either the correct class name or null. + * @psalm-return class-string|null + */ + protected function _resolveClassName(string $class): ?string + { + return App::className($class, 'Datasource'); + } + + /** + * Throws an exception when a datasource is missing + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * @param string $class The classname that is missing. + * @param string|null $plugin The plugin the datasource is missing in. + * @return void + * @throws \Cake\Datasource\Exception\MissingDatasourceException + */ + protected function _throwMissingClassError(string $class, ?string $plugin): void + { + throw new MissingDatasourceException([ + 'class' => $class, + 'plugin' => $plugin, + ]); + } + + /** + * Create the connection object with the correct settings. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * If a callable is passed as first argument, The returned value of this + * function will be the result of the callable. + * + * @param \Cake\Datasource\ConnectionInterface|callable|string $class The classname or object to make. + * @param string $alias The alias of the object. + * @param array $config An array of settings to use for the datasource. + * @return \Cake\Datasource\ConnectionInterface A connection with the correct settings. + */ + protected function _create($class, string $alias, array $config) + { + if (is_callable($class)) { + return $class($alias); + } + + if (is_object($class)) { + return $class; + } + + unset($config['className']); + + /** @var \Cake\Datasource\ConnectionInterface */ + return new $class($config); + } + + /** + * Remove a single adapter from the registry. + * + * @param string $name The adapter name. + * @return $this + */ + public function unload(string $name) + { + unset($this->_loaded[$name]); + + return $this; + } +} diff --git a/vendor/cakephp/datasource/EntityInterface.php b/vendor/cakephp/datasource/EntityInterface.php new file mode 100644 index 0000000..2f7b273 --- /dev/null +++ b/vendor/cakephp/datasource/EntityInterface.php @@ -0,0 +1,287 @@ + $fields An array of fields to hide from array exports. + * @param bool $merge Merge the new fields with the existing. By default false. + * @return $this + */ + public function setHidden(array $fields, bool $merge = false); + + /** + * Gets the hidden fields. + * + * @return array + */ + public function getHidden(): array; + + /** + * Sets the virtual fields on this entity. + * + * @param array $fields An array of fields to treat as virtual. + * @param bool $merge Merge the new fields with the existing. By default false. + * @return $this + */ + public function setVirtual(array $fields, bool $merge = false); + + /** + * Gets the virtual fields on this entity. + * + * @return array + */ + public function getVirtual(): array; + + /** + * Sets the dirty status of a single field. + * + * @param string $field the field to set or check status for + * @param bool $isDirty true means the field was changed, false means + * it was not changed. Default true. + * @return $this + */ + public function setDirty(string $field, bool $isDirty = true); + + /** + * Checks if the entity is dirty or if a single field of it is dirty. + * + * @param string|null $field The field to check the status for. Null for the whole entity. + * @return bool Whether the field was changed or not + */ + public function isDirty(?string $field = null): bool; + + /** + * Gets the dirty fields. + * + * @return array + */ + public function getDirty(): array; + + /** + * Returns whether this entity has errors. + * + * @param bool $includeNested true will check nested entities for hasErrors() + * @return bool + */ + public function hasErrors(bool $includeNested = true): bool; + + /** + * Returns all validation errors. + * + * @return array + */ + public function getErrors(): array; + + /** + * Returns validation errors of a field + * + * @param string $field Field name to get the errors from + * @return array + */ + public function getError(string $field): array; + + /** + * Sets error messages to the entity + * + * @param array $errors The array of errors to set. + * @param bool $overwrite Whether to overwrite pre-existing errors for $fields + * @return $this + */ + public function setErrors(array $errors, bool $overwrite = false); + + /** + * Sets errors for a single field + * + * @param string $field The field to get errors for, or the array of errors to set. + * @param array|string $errors The errors to be set for $field + * @param bool $overwrite Whether to overwrite pre-existing errors for $field + * @return $this + */ + public function setError(string $field, $errors, bool $overwrite = false); + + /** + * Stores whether a field value can be changed or set in this entity. + * + * @param array|string $field single or list of fields to change its accessibility + * @param bool $set true marks the field as accessible, false will + * mark it as protected. + * @return $this + */ + public function setAccess($field, bool $set); + + /** + * Checks if a field is accessible + * + * @param string $field Field name to check + * @return bool + */ + public function isAccessible(string $field): bool; + + /** + * Sets the source alias + * + * @param string $alias the alias of the repository + * @return $this + */ + public function setSource(string $alias); + + /** + * Returns the alias of the repository from which this entity came from. + * + * @return string + */ + public function getSource(): string; + + /** + * Returns an array with the requested original fields + * stored in this entity, indexed by field name. + * + * @param array $fields List of fields to be returned + * @return array + */ + public function extractOriginal(array $fields): array; + + /** + * Returns an array with only the original fields + * stored in this entity, indexed by field name. + * + * @param array $fields List of fields to be returned + * @return array + */ + public function extractOriginalChanged(array $fields): array; + + /** + * Sets one or multiple fields to the specified value + * + * @param array|string $field the name of field to set or a list of + * fields with their respective values + * @param mixed $value The value to set to the field or an array if the + * first argument is also an array, in which case will be treated as $options + * @param array $options Options to be used for setting the field. Allowed option + * keys are `setter` and `guard` + * @return $this + */ + public function set($field, $value = null, array $options = []); + + /** + * Returns the value of a field by name + * + * @param string $field the name of the field to retrieve + * @return mixed + */ + public function &get(string $field); + + /** + * Returns the original value of a field. + * + * @param string $field The name of the field. + * @return mixed + */ + public function getOriginal(string $field); + + /** + * Gets all original values of the entity. + * + * @return array + */ + public function getOriginalValues(): array; + + /** + * Returns whether this entity contains a field named $field + * regardless of if it is empty. + * + * @param array|string $field The field to check. + * @return bool + */ + public function has($field): bool; + + /** + * Removes a field or list of fields from this entity + * + * @param array|string $field The field to unset. + * @return $this + */ + public function unset($field); + + /** + * Get the list of visible fields. + * + * @return array A list of fields that are 'visible' in all representations. + */ + public function getVisible(): array; + + /** + * Returns an array with all the visible fields set in this entity. + * + * *Note* hidden fields are not visible, and will not be output + * by toArray(). + * + * @return array + */ + public function toArray(): array; + + /** + * Returns an array with the requested fields + * stored in this entity, indexed by field name + * + * @param array $fields list of fields to be returned + * @param bool $onlyDirty Return the requested field only if it is dirty + * @return array + */ + public function extract(array $fields, bool $onlyDirty = false): array; + + /** + * Sets the entire entity as clean, which means that it will appear as + * no fields being modified or added at all. This is an useful call + * for an initial object hydration + * + * @return void + */ + public function clean(): void; + + /** + * Set the status of this entity. + * + * Using `true` means that the entity has not been persisted in the database, + * `false` indicates that the entity has been persisted. + * + * @param bool $new Indicate whether this entity has been persisted. + * @return $this + */ + public function setNew(bool $new); + + /** + * Returns whether this entity has already been persisted. + * + * @return bool Whether the entity has been persisted. + */ + public function isNew(): bool; +} diff --git a/vendor/cakephp/datasource/EntityTrait.php b/vendor/cakephp/datasource/EntityTrait.php new file mode 100644 index 0000000..616b3db --- /dev/null +++ b/vendor/cakephp/datasource/EntityTrait.php @@ -0,0 +1,1260 @@ + + */ + protected $_fields = []; + + /** + * Holds all fields that have been changed and their original values for this entity. + * + * @var array + */ + protected $_original = []; + + /** + * List of field names that should **not** be included in JSON or Array + * representations of this Entity. + * + * @var array + */ + protected $_hidden = []; + + /** + * List of computed or virtual fields that **should** be included in JSON or array + * representations of this Entity. If a field is present in both _hidden and _virtual + * the field will **not** be in the array/JSON versions of the entity. + * + * @var array + */ + protected $_virtual = []; + + /** + * Holds a list of the fields that were modified or added after this object + * was originally created. + * + * @var array + */ + protected $_dirty = []; + + /** + * Holds a cached list of getters/setters per class + * + * @var array>> + */ + protected static $_accessors = []; + + /** + * Indicates whether this entity is yet to be persisted. + * Entities default to assuming they are new. You can use Table::persisted() + * to set the new flag on an entity based on records in the database. + * + * @var bool + */ + protected $_new = true; + + /** + * List of errors per field as stored in this object. + * + * @var array + */ + protected $_errors = []; + + /** + * List of invalid fields and their data for errors upon validation/patching. + * + * @var array + */ + protected $_invalid = []; + + /** + * Map of fields in this entity that can be safely assigned, each + * field name points to a boolean indicating its status. An empty array + * means no fields are accessible + * + * The special field '\*' can also be mapped, meaning that any other field + * not defined in the map will take its value. For example, `'*' => true` + * means that any field not defined in the map will be accessible by default + * + * @var array + */ + protected $_accessible = ['*' => true]; + + /** + * The alias of the repository this entity came from + * + * @var string + */ + protected $_registryAlias = ''; + + /** + * Magic getter to access fields that have been set in this entity + * + * @param string $field Name of the field to access + * @return mixed + */ + public function &__get(string $field) + { + return $this->get($field); + } + + /** + * Magic setter to add or edit a field in this entity + * + * @param string $field The name of the field to set + * @param mixed $value The value to set to the field + * @return void + */ + public function __set(string $field, $value): void + { + $this->set($field, $value); + } + + /** + * Returns whether this entity contains a field named $field + * regardless of if it is empty. + * + * @param string $field The field to check. + * @return bool + * @see \Cake\ORM\Entity::has() + */ + public function __isset(string $field): bool + { + return $this->has($field); + } + + /** + * Removes a field from this entity + * + * @param string $field The field to unset + * @return void + */ + public function __unset(string $field): void + { + $this->unset($field); + } + + /** + * Sets a single field inside this entity. + * + * ### Example: + * + * ``` + * $entity->set('name', 'Andrew'); + * ``` + * + * It is also possible to mass-assign multiple fields to this entity + * with one call by passing a hashed array as fields in the form of + * field => value pairs + * + * ### Example: + * + * ``` + * $entity->set(['name' => 'andrew', 'id' => 1]); + * echo $entity->name // prints andrew + * echo $entity->id // prints 1 + * ``` + * + * Some times it is handy to bypass setter functions in this entity when assigning + * fields. You can achieve this by disabling the `setter` option using the + * `$options` parameter: + * + * ``` + * $entity->set('name', 'Andrew', ['setter' => false]); + * $entity->set(['name' => 'Andrew', 'id' => 1], ['setter' => false]); + * ``` + * + * Mass assignment should be treated carefully when accepting user input, by default + * entities will guard all fields when fields are assigned in bulk. You can disable + * the guarding for a single set call with the `guard` option: + * + * ``` + * $entity->set(['name' => 'Andrew', 'id' => 1], ['guard' => false]); + * ``` + * + * You do not need to use the guard option when assigning fields individually: + * + * ``` + * // No need to use the guard option. + * $entity->set('name', 'Andrew'); + * ``` + * + * @param array|string $field the name of field to set or a list of + * fields with their respective values + * @param mixed $value The value to set to the field or an array if the + * first argument is also an array, in which case will be treated as $options + * @param array $options Options to be used for setting the field. Allowed option + * keys are `setter` and `guard` + * @return $this + * @throws \InvalidArgumentException + */ + public function set($field, $value = null, array $options = []) + { + if (is_string($field) && $field !== '') { + $guard = false; + $field = [$field => $value]; + } else { + $guard = true; + $options = (array)$value; + } + + if (!is_array($field)) { + throw new InvalidArgumentException('Cannot set an empty field'); + } + $options += ['setter' => true, 'guard' => $guard]; + + foreach ($field as $name => $value) { + $name = (string)$name; + if ($options['guard'] === true && !$this->isAccessible($name)) { + continue; + } + + $this->setDirty($name, true); + + if ( + !array_key_exists($name, $this->_original) && + array_key_exists($name, $this->_fields) && + $this->_fields[$name] !== $value + ) { + $this->_original[$name] = $this->_fields[$name]; + } + + if (!$options['setter']) { + $this->_fields[$name] = $value; + continue; + } + + $setter = static::_accessor($name, 'set'); + if ($setter) { + $value = $this->{$setter}($value); + } + $this->_fields[$name] = $value; + } + + return $this; + } + + /** + * Returns the value of a field by name + * + * @param string $field the name of the field to retrieve + * @return mixed + * @throws \InvalidArgumentException if an empty field name is passed + */ + public function &get(string $field) + { + if ($field === '') { + throw new InvalidArgumentException('Cannot get an empty field'); + } + + $value = null; + $method = static::_accessor($field, 'get'); + + if (isset($this->_fields[$field])) { + $value = &$this->_fields[$field]; + } + + if ($method) { + $result = $this->{$method}($value); + + return $result; + } + + return $value; + } + + /** + * Returns the value of an original field by name + * + * @param string $field the name of the field for which original value is retrieved. + * @return mixed + * @throws \InvalidArgumentException if an empty field name is passed. + */ + public function getOriginal(string $field) + { + if ($field === '') { + throw new InvalidArgumentException('Cannot get an empty field'); + } + if (array_key_exists($field, $this->_original)) { + return $this->_original[$field]; + } + + return $this->get($field); + } + + /** + * Gets all original values of the entity. + * + * @return array + */ + public function getOriginalValues(): array + { + $originals = $this->_original; + $originalKeys = array_keys($originals); + foreach ($this->_fields as $key => $value) { + if (!in_array($key, $originalKeys, true)) { + $originals[$key] = $value; + } + } + + return $originals; + } + + /** + * Returns whether this entity contains a field named $field + * that contains a non-null value. + * + * ### Example: + * + * ``` + * $entity = new Entity(['id' => 1, 'name' => null]); + * $entity->has('id'); // true + * $entity->has('name'); // false + * $entity->has('last_name'); // false + * ``` + * + * You can check multiple fields by passing an array: + * + * ``` + * $entity->has(['name', 'last_name']); + * ``` + * + * All fields must not be null to get a truthy result. + * + * When checking multiple fields. All fields must not be null + * in order for true to be returned. + * + * @param array|string $field The field or fields to check. + * @return bool + */ + public function has($field): bool + { + foreach ((array)$field as $prop) { + if ($this->get($prop) === null) { + return false; + } + } + + return true; + } + + /** + * Checks that a field is empty + * + * This is not working like the PHP `empty()` function. The method will + * return true for: + * + * - `''` (empty string) + * - `null` + * - `[]` + * + * and false in all other cases. + * + * @param string $field The field to check. + * @return bool + */ + public function isEmpty(string $field): bool + { + $value = $this->get($field); + if ( + $value === null || + ( + is_array($value) && + empty($value) || + ( + is_string($value) && + $value === '' + ) + ) + ) { + return true; + } + + return false; + } + + /** + * Checks tha a field has a value. + * + * This method will return true for + * + * - Non-empty strings + * - Non-empty arrays + * - Any object + * - Integer, even `0` + * - Float, even 0.0 + * + * and false in all other cases. + * + * @param string $field The field to check. + * @return bool + */ + public function hasValue(string $field): bool + { + return !$this->isEmpty($field); + } + + /** + * Removes a field or list of fields from this entity + * + * ### Examples: + * + * ``` + * $entity->unset('name'); + * $entity->unset(['name', 'last_name']); + * ``` + * + * @param array|string $field The field to unset. + * @return $this + */ + public function unset($field) + { + $field = (array)$field; + foreach ($field as $p) { + unset($this->_fields[$p], $this->_original[$p], $this->_dirty[$p]); + } + + return $this; + } + + /** + * Removes a field or list of fields from this entity + * + * @deprecated 4.0.0 Use {@link unset()} instead. Will be removed in 5.0. + * @param array|string $field The field to unset. + * @return $this + */ + public function unsetProperty($field) + { + deprecationWarning('EntityTrait::unsetProperty() is deprecated. Use unset() instead.'); + + return $this->unset($field); + } + + /** + * Sets hidden fields. + * + * @param array $fields An array of fields to hide from array exports. + * @param bool $merge Merge the new fields with the existing. By default false. + * @return $this + */ + public function setHidden(array $fields, bool $merge = false) + { + if ($merge === false) { + $this->_hidden = $fields; + + return $this; + } + + $fields = array_merge($this->_hidden, $fields); + $this->_hidden = array_unique($fields); + + return $this; + } + + /** + * Gets the hidden fields. + * + * @return array + */ + public function getHidden(): array + { + return $this->_hidden; + } + + /** + * Sets the virtual fields on this entity. + * + * @param array $fields An array of fields to treat as virtual. + * @param bool $merge Merge the new fields with the existing. By default false. + * @return $this + */ + public function setVirtual(array $fields, bool $merge = false) + { + if ($merge === false) { + $this->_virtual = $fields; + + return $this; + } + + $fields = array_merge($this->_virtual, $fields); + $this->_virtual = array_unique($fields); + + return $this; + } + + /** + * Gets the virtual fields on this entity. + * + * @return array + */ + public function getVirtual(): array + { + return $this->_virtual; + } + + /** + * Gets the list of visible fields. + * + * The list of visible fields is all standard fields + * plus virtual fields minus hidden fields. + * + * @return array A list of fields that are 'visible' in all + * representations. + */ + public function getVisible(): array + { + $fields = array_keys($this->_fields); + $fields = array_merge($fields, $this->_virtual); + + return array_diff($fields, $this->_hidden); + } + + /** + * Returns an array with all the fields that have been set + * to this entity + * + * This method will recursively transform entities assigned to fields + * into arrays as well. + * + * @return array + */ + public function toArray(): array + { + $result = []; + foreach ($this->getVisible() as $field) { + $value = $this->get($field); + if (is_array($value)) { + $result[$field] = []; + foreach ($value as $k => $entity) { + if ($entity instanceof EntityInterface) { + $result[$field][$k] = $entity->toArray(); + } else { + $result[$field][$k] = $entity; + } + } + } elseif ($value instanceof EntityInterface) { + $result[$field] = $value->toArray(); + } else { + $result[$field] = $value; + } + } + + return $result; + } + + /** + * Returns the fields that will be serialized as JSON + * + * @return array + */ + public function jsonSerialize(): array + { + return $this->extract($this->getVisible()); + } + + /** + * Implements isset($entity); + * + * @param string $offset The offset to check. + * @return bool Success + */ + public function offsetExists($offset): bool + { + return $this->has($offset); + } + + /** + * Implements $entity[$offset]; + * + * @param string $offset The offset to get. + * @return mixed + */ + #[\ReturnTypeWillChange] + public function &offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Implements $entity[$offset] = $value; + * + * @param string $offset The offset to set. + * @param mixed $value The value to set. + * @return void + */ + public function offsetSet($offset, $value): void + { + $this->set($offset, $value); + } + + /** + * Implements unset($result[$offset]); + * + * @param string $offset The offset to remove. + * @return void + */ + public function offsetUnset($offset): void + { + $this->unset($offset); + } + + /** + * Fetch accessor method name + * Accessor methods (available or not) are cached in $_accessors + * + * @param string $property the field name to derive getter name from + * @param string $type the accessor type ('get' or 'set') + * @return string method name or empty string (no method available) + */ + protected static function _accessor(string $property, string $type): string + { + $class = static::class; + + if (isset(static::$_accessors[$class][$type][$property])) { + return static::$_accessors[$class][$type][$property]; + } + + if (!empty(static::$_accessors[$class])) { + return static::$_accessors[$class][$type][$property] = ''; + } + + if (static::class === Entity::class) { + return ''; + } + + foreach (get_class_methods($class) as $method) { + $prefix = substr($method, 1, 3); + if ($method[0] !== '_' || ($prefix !== 'get' && $prefix !== 'set')) { + continue; + } + $field = lcfirst(substr($method, 4)); + $snakeField = Inflector::underscore($field); + $titleField = ucfirst($field); + static::$_accessors[$class][$prefix][$snakeField] = $method; + static::$_accessors[$class][$prefix][$field] = $method; + static::$_accessors[$class][$prefix][$titleField] = $method; + } + + if (!isset(static::$_accessors[$class][$type][$property])) { + static::$_accessors[$class][$type][$property] = ''; + } + + return static::$_accessors[$class][$type][$property]; + } + + /** + * Returns an array with the requested fields + * stored in this entity, indexed by field name + * + * @param array $fields list of fields to be returned + * @param bool $onlyDirty Return the requested field only if it is dirty + * @return array + */ + public function extract(array $fields, bool $onlyDirty = false): array + { + $result = []; + foreach ($fields as $field) { + if (!$onlyDirty || $this->isDirty($field)) { + $result[$field] = $this->get($field); + } + } + + return $result; + } + + /** + * Returns an array with the requested original fields + * stored in this entity, indexed by field name. + * + * Fields that are unchanged from their original value will be included in the + * return of this method. + * + * @param array $fields List of fields to be returned + * @return array + */ + public function extractOriginal(array $fields): array + { + $result = []; + foreach ($fields as $field) { + $result[$field] = $this->getOriginal($field); + } + + return $result; + } + + /** + * Returns an array with only the original fields + * stored in this entity, indexed by field name. + * + * This method will only return fields that have been modified since + * the entity was built. Unchanged fields will be omitted. + * + * @param array $fields List of fields to be returned + * @return array + */ + public function extractOriginalChanged(array $fields): array + { + $result = []; + foreach ($fields as $field) { + $original = $this->getOriginal($field); + if ($original !== $this->get($field)) { + $result[$field] = $original; + } + } + + return $result; + } + + /** + * Sets the dirty status of a single field. + * + * @param string $field the field to set or check status for + * @param bool $isDirty true means the field was changed, false means + * it was not changed. Defaults to true. + * @return $this + */ + public function setDirty(string $field, bool $isDirty = true) + { + if ($isDirty === false) { + unset($this->_dirty[$field]); + + return $this; + } + + $this->_dirty[$field] = true; + unset($this->_errors[$field], $this->_invalid[$field]); + + return $this; + } + + /** + * Checks if the entity is dirty or if a single field of it is dirty. + * + * @param string|null $field The field to check the status for. Null for the whole entity. + * @return bool Whether the field was changed or not + */ + public function isDirty(?string $field = null): bool + { + if ($field === null) { + return !empty($this->_dirty); + } + + return isset($this->_dirty[$field]); + } + + /** + * Gets the dirty fields. + * + * @return array + */ + public function getDirty(): array + { + return array_keys($this->_dirty); + } + + /** + * Sets the entire entity as clean, which means that it will appear as + * no fields being modified or added at all. This is an useful call + * for an initial object hydration + * + * @return void + */ + public function clean(): void + { + $this->_dirty = []; + $this->_errors = []; + $this->_invalid = []; + $this->_original = []; + } + + /** + * Set the status of this entity. + * + * Using `true` means that the entity has not been persisted in the database, + * `false` that it already is. + * + * @param bool $new Indicate whether this entity has been persisted. + * @return $this + */ + public function setNew(bool $new) + { + if ($new) { + foreach ($this->_fields as $k => $p) { + $this->_dirty[$k] = true; + } + } + + $this->_new = $new; + + return $this; + } + + /** + * Returns whether this entity has already been persisted. + * + * @return bool Whether the entity has been persisted. + */ + public function isNew(): bool + { + if (func_num_args()) { + deprecationWarning('Using isNew() as setter is deprecated. Use setNew() instead.'); + + $this->setNew(func_get_arg(0)); + } + + return $this->_new; + } + + /** + * Returns whether this entity has errors. + * + * @param bool $includeNested true will check nested entities for hasErrors() + * @return bool + */ + public function hasErrors(bool $includeNested = true): bool + { + if (Hash::filter($this->_errors)) { + return true; + } + + if ($includeNested === false) { + return false; + } + + foreach ($this->_fields as $field) { + if ($this->_readHasErrors($field)) { + return true; + } + } + + return false; + } + + /** + * Returns all validation errors. + * + * @return array + */ + public function getErrors(): array + { + $diff = array_diff_key($this->_fields, $this->_errors); + + return $this->_errors + (new Collection($diff)) + ->filter(function ($value) { + return is_array($value) || $value instanceof EntityInterface; + }) + ->map(function ($value) { + return $this->_readError($value); + }) + ->filter() + ->toArray(); + } + + /** + * Returns validation errors of a field + * + * @param string $field Field name to get the errors from + * @return array + */ + public function getError(string $field): array + { + $errors = $this->_errors[$field] ?? []; + if ($errors) { + return $errors; + } + + return $this->_nestedErrors($field); + } + + /** + * Sets error messages to the entity + * + * ## Example + * + * ``` + * // Sets the error messages for multiple fields at once + * $entity->setErrors(['salary' => ['message'], 'name' => ['another message']]); + * ``` + * + * @param array $errors The array of errors to set. + * @param bool $overwrite Whether to overwrite pre-existing errors for $fields + * @return $this + */ + public function setErrors(array $errors, bool $overwrite = false) + { + if ($overwrite) { + foreach ($errors as $f => $error) { + $this->_errors[$f] = (array)$error; + } + + return $this; + } + + foreach ($errors as $f => $error) { + $this->_errors += [$f => []]; + + // String messages are appended to the list, + // while more complex error structures need their + // keys preserved for nested validator. + if (is_string($error)) { + $this->_errors[$f][] = $error; + } else { + foreach ($error as $k => $v) { + $this->_errors[$f][$k] = $v; + } + } + } + + return $this; + } + + /** + * Sets errors for a single field + * + * ### Example + * + * ``` + * // Sets the error messages for a single field + * $entity->setError('salary', ['must be numeric', 'must be a positive number']); + * ``` + * + * @param string $field The field to get errors for, or the array of errors to set. + * @param array|string $errors The errors to be set for $field + * @param bool $overwrite Whether to overwrite pre-existing errors for $field + * @return $this + */ + public function setError(string $field, $errors, bool $overwrite = false) + { + if (is_string($errors)) { + $errors = [$errors]; + } + + return $this->setErrors([$field => $errors], $overwrite); + } + + /** + * Auxiliary method for getting errors in nested entities + * + * @param string $field the field in this entity to check for errors + * @return array errors in nested entity if any + */ + protected function _nestedErrors(string $field): array + { + // Only one path element, check for nested entity with error. + if (strpos($field, '.') === false) { + return $this->_readError($this->get($field)); + } + // Try reading the errors data with field as a simple path + $error = Hash::get($this->_errors, $field); + if ($error !== null) { + return $error; + } + $path = explode('.', $field); + + // Traverse down the related entities/arrays for + // the relevant entity. + $entity = $this; + $len = count($path); + while ($len) { + $part = array_shift($path); + $len = count($path); + $val = null; + if ($entity instanceof EntityInterface) { + $val = $entity->get($part); + } elseif (is_array($entity)) { + $val = $entity[$part] ?? false; + } + + if ( + is_array($val) || + $val instanceof Traversable || + $val instanceof EntityInterface + ) { + $entity = $val; + } else { + $path[] = $part; + break; + } + } + if (count($path) <= 1) { + return $this->_readError($entity, array_pop($path)); + } + + return []; + } + + /** + * Reads if there are errors for one or many objects. + * + * @param \Cake\Datasource\EntityInterface|array $object The object to read errors from. + * @return bool + */ + protected function _readHasErrors($object): bool + { + if ($object instanceof EntityInterface && $object->hasErrors()) { + return true; + } + + if (is_array($object)) { + foreach ($object as $value) { + if ($this->_readHasErrors($value)) { + return true; + } + } + } + + return false; + } + + /** + * Read the error(s) from one or many objects. + * + * @param \Cake\Datasource\EntityInterface|iterable $object The object to read errors from. + * @param string|null $path The field name for errors. + * @return array + */ + protected function _readError($object, $path = null): array + { + if ($path !== null && $object instanceof EntityInterface) { + return $object->getError($path); + } + if ($object instanceof EntityInterface) { + return $object->getErrors(); + } + if (is_iterable($object)) { + $array = array_map(function ($val) { + if ($val instanceof EntityInterface) { + return $val->getErrors(); + } + + return null; + }, (array)$object); + + return array_filter($array); + } + + return []; + } + + /** + * Get a list of invalid fields and their data for errors upon validation/patching + * + * @return array + */ + public function getInvalid(): array + { + return $this->_invalid; + } + + /** + * Get a single value of an invalid field. Returns null if not set. + * + * @param string $field The name of the field. + * @return mixed|null + */ + public function getInvalidField(string $field) + { + return $this->_invalid[$field] ?? null; + } + + /** + * Set fields as invalid and not patchable into the entity. + * + * This is useful for batch operations when one needs to get the original value for an error message after patching. + * This value could not be patched into the entity and is simply copied into the _invalid property for debugging + * purposes or to be able to log it away. + * + * @param array $fields The values to set. + * @param bool $overwrite Whether to overwrite pre-existing values for $field. + * @return $this + */ + public function setInvalid(array $fields, bool $overwrite = false) + { + foreach ($fields as $field => $value) { + if ($overwrite === true) { + $this->_invalid[$field] = $value; + continue; + } + $this->_invalid += [$field => $value]; + } + + return $this; + } + + /** + * Sets a field as invalid and not patchable into the entity. + * + * @param string $field The value to set. + * @param mixed $value The invalid value to be set for $field. + * @return $this + */ + public function setInvalidField(string $field, $value) + { + $this->_invalid[$field] = $value; + + return $this; + } + + /** + * Stores whether a field value can be changed or set in this entity. + * The special field `*` can also be marked as accessible or protected, meaning + * that any other field specified before will take its value. For example + * `$entity->setAccess('*', true)` means that any field not specified already + * will be accessible by default. + * + * You can also call this method with an array of fields, in which case they + * will each take the accessibility value specified in the second argument. + * + * ### Example: + * + * ``` + * $entity->setAccess('id', true); // Mark id as not protected + * $entity->setAccess('author_id', false); // Mark author_id as protected + * $entity->setAccess(['id', 'user_id'], true); // Mark both fields as accessible + * $entity->setAccess('*', false); // Mark all fields as protected + * ``` + * + * @param array|string $field Single or list of fields to change its accessibility + * @param bool $set True marks the field as accessible, false will + * mark it as protected. + * @return $this + */ + public function setAccess($field, bool $set) + { + if ($field === '*') { + $this->_accessible = array_map(function ($p) use ($set) { + return $set; + }, $this->_accessible); + $this->_accessible['*'] = $set; + + return $this; + } + + foreach ((array)$field as $prop) { + $this->_accessible[$prop] = $set; + } + + return $this; + } + + /** + * Returns the raw accessible configuration for this entity. + * The `*` wildcard refers to all fields. + * + * @return array + */ + public function getAccessible(): array + { + return $this->_accessible; + } + + /** + * Checks if a field is accessible + * + * ### Example: + * + * ``` + * $entity->isAccessible('id'); // Returns whether it can be set or not + * ``` + * + * @param string $field Field name to check + * @return bool + */ + public function isAccessible(string $field): bool + { + $value = $this->_accessible[$field] ?? null; + + return ($value === null && !empty($this->_accessible['*'])) || $value; + } + + /** + * Returns the alias of the repository from which this entity came from. + * + * @return string + */ + public function getSource(): string + { + return $this->_registryAlias; + } + + /** + * Sets the source alias + * + * @param string $alias the alias of the repository + * @return $this + */ + public function setSource(string $alias) + { + $this->_registryAlias = $alias; + + return $this; + } + + /** + * Returns a string representation of this object in a human readable format. + * + * @return string + */ + public function __toString(): string + { + return (string)json_encode($this, JSON_PRETTY_PRINT); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo(): array + { + $fields = $this->_fields; + foreach ($this->_virtual as $field) { + $fields[$field] = $this->$field; + } + + return $fields + [ + '[new]' => $this->isNew(), + '[accessible]' => $this->_accessible, + '[dirty]' => $this->_dirty, + '[original]' => $this->_original, + '[virtual]' => $this->_virtual, + '[hasErrors]' => $this->hasErrors(), + '[errors]' => $this->_errors, + '[invalid]' => $this->_invalid, + '[repository]' => $this->_registryAlias, + ]; + } +} diff --git a/vendor/cakephp/datasource/Exception/InvalidPrimaryKeyException.php b/vendor/cakephp/datasource/Exception/InvalidPrimaryKeyException.php new file mode 100644 index 0000000..ac7a72b --- /dev/null +++ b/vendor/cakephp/datasource/Exception/InvalidPrimaryKeyException.php @@ -0,0 +1,26 @@ + + */ + protected static $_modelFactories = []; + + /** + * Register a callable to generate repositories of a given type. + * + * @param string $type The name of the repository type the factory function is for. + * @param \Cake\Datasource\Locator\LocatorInterface|callable $factory The factory function used to create instances. + * @return void + */ + public static function add(string $type, $factory): void + { + if (!$factory instanceof LocatorInterface && !is_callable($factory)) { + throw new InvalidArgumentException(sprintf( + '`$factory` must be an instance of Cake\Datasource\Locator\LocatorInterface or a callable.' + . ' Got type `%s` instead.', + getTypeName($factory) + )); + } + + static::$_modelFactories[$type] = $factory; + } + + /** + * Drop a model factory. + * + * @param string $type The name of the repository type to drop the factory for. + * @return void + */ + public static function drop(string $type): void + { + unset(static::$_modelFactories[$type]); + } + + /** + * Get the factory for the specified repository type. + * + * @param string $type The repository type to get the factory for. + * @throws \InvalidArgumentException If the specified repository type has no factory. + * @return \Cake\Datasource\Locator\LocatorInterface|callable The factory for the repository type. + */ + public static function get(string $type) + { + if (!isset(static::$_modelFactories['Table'])) { + static::$_modelFactories['Table'] = new TableLocator(); + } + + if (!isset(static::$_modelFactories[$type])) { + throw new InvalidArgumentException(sprintf( + 'Unknown repository type "%s". Make sure you register a type before trying to use it.', + $type + )); + } + + return static::$_modelFactories[$type]; + } +} diff --git a/vendor/cakephp/datasource/FixtureInterface.php b/vendor/cakephp/datasource/FixtureInterface.php new file mode 100644 index 0000000..64c28e4 --- /dev/null +++ b/vendor/cakephp/datasource/FixtureInterface.php @@ -0,0 +1,73 @@ + + */ + protected $instances = []; + + /** + * Contains a list of options that were passed to get() method. + * + * @var array + */ + protected $options = []; + + /** + * {@inheritDoc} + * + * @param string $alias The alias name you want to get. + * @param array $options The options you want to build the table with. + * @return \Cake\Datasource\RepositoryInterface + * @throws \RuntimeException When trying to get alias for which instance + * has already been created with different options. + */ + public function get(string $alias, array $options = []) + { + $storeOptions = $options; + unset($storeOptions['allowFallbackClass']); + + if (isset($this->instances[$alias])) { + if (!empty($storeOptions) && $this->options[$alias] !== $storeOptions) { + throw new RuntimeException(sprintf( + 'You cannot configure "%s", it already exists in the registry.', + $alias + )); + } + + return $this->instances[$alias]; + } + + $this->options[$alias] = $storeOptions; + + return $this->instances[$alias] = $this->createInstance($alias, $options); + } + + /** + * Create an instance of a given classname. + * + * @param string $alias Repository alias. + * @param array $options The options you want to build the instance with. + * @return \Cake\Datasource\RepositoryInterface + */ + abstract protected function createInstance(string $alias, array $options); + + /** + * @inheritDoc + */ + public function set(string $alias, RepositoryInterface $repository) + { + return $this->instances[$alias] = $repository; + } + + /** + * @inheritDoc + */ + public function exists(string $alias): bool + { + return isset($this->instances[$alias]); + } + + /** + * @inheritDoc + */ + public function remove(string $alias): void + { + unset( + $this->instances[$alias], + $this->options[$alias] + ); + } + + /** + * @inheritDoc + */ + public function clear(): void + { + $this->instances = []; + $this->options = []; + } +} diff --git a/vendor/cakephp/datasource/Locator/LocatorInterface.php b/vendor/cakephp/datasource/Locator/LocatorInterface.php new file mode 100644 index 0000000..256ae0a --- /dev/null +++ b/vendor/cakephp/datasource/Locator/LocatorInterface.php @@ -0,0 +1,68 @@ + $options The options you want to build the table with. + * @return \Cake\Datasource\RepositoryInterface + * @throws \RuntimeException When trying to get alias for which instance + * has already been created with different options. + */ + public function get(string $alias, array $options = []); + + /** + * Set a repository instance. + * + * @param string $alias The alias to set. + * @param \Cake\Datasource\RepositoryInterface $repository The repository to set. + * @return \Cake\Datasource\RepositoryInterface + */ + public function set(string $alias, RepositoryInterface $repository); + + /** + * Check to see if an instance exists in the registry. + * + * @param string $alias The alias to check for. + * @return bool + */ + public function exists(string $alias): bool; + + /** + * Removes an repository instance from the registry. + * + * @param string $alias The alias to remove. + * @return void + */ + public function remove(string $alias): void; + + /** + * Clears the registry of configuration and instances. + * + * @return void + */ + public function clear(): void; +} diff --git a/vendor/cakephp/datasource/ModelAwareTrait.php b/vendor/cakephp/datasource/ModelAwareTrait.php new file mode 100644 index 0000000..b890cbf --- /dev/null +++ b/vendor/cakephp/datasource/ModelAwareTrait.php @@ -0,0 +1,180 @@ + + */ + protected $_modelFactories = []; + + /** + * The model type to use. + * + * @var string + */ + protected $_modelType = 'Table'; + + /** + * Set the modelClass property based on conventions. + * + * If the property is already set it will not be overwritten + * + * @param string $name Class name. + * @return void + */ + protected function _setModelClass(string $name): void + { + if ($this->modelClass === null) { + $this->modelClass = $name; + } + } + + /** + * Loads and constructs repository objects required by this object + * + * Typically used to load ORM Table objects as required. Can + * also be used to load other types of repository objects your application uses. + * + * If a repository provider does not return an object a MissingModelException will + * be thrown. + * + * @param string|null $modelClass Name of model class to load. Defaults to $this->modelClass. + * The name can be an alias like `'Post'` or FQCN like `App\Model\Table\PostsTable::class`. + * @param string|null $modelType The type of repository to load. Defaults to the getModelType() value. + * @return \Cake\Datasource\RepositoryInterface The model instance created. + * @throws \Cake\Datasource\Exception\MissingModelException If the model class cannot be found. + * @throws \UnexpectedValueException If $modelClass argument is not provided + * and ModelAwareTrait::$modelClass property value is empty. + * @deprecated 4.3.0 Use `LocatorAwareTrait::fetchTable()` instead. + */ + public function loadModel(?string $modelClass = null, ?string $modelType = null): RepositoryInterface + { + $modelClass = $modelClass ?? $this->modelClass; + if (empty($modelClass)) { + throw new UnexpectedValueException('Default modelClass is empty'); + } + $modelType = $modelType ?? $this->getModelType(); + + $options = []; + if (strpos($modelClass, '\\') === false) { + [, $alias] = pluginSplit($modelClass, true); + } else { + $options['className'] = $modelClass; + /** @psalm-suppress PossiblyFalseOperand */ + $alias = substr( + $modelClass, + strrpos($modelClass, '\\') + 1, + -strlen($modelType) + ); + $modelClass = $alias; + } + + if (isset($this->{$alias})) { + return $this->{$alias}; + } + + $factory = $this->_modelFactories[$modelType] ?? FactoryLocator::get($modelType); + if ($factory instanceof LocatorInterface) { + $this->{$alias} = $factory->get($modelClass, $options); + } else { + $this->{$alias} = $factory($modelClass, $options); + } + + if (!$this->{$alias}) { + throw new MissingModelException([$modelClass, $modelType]); + } + + return $this->{$alias}; + } + + /** + * Override a existing callable to generate repositories of a given type. + * + * @param string $type The name of the repository type the factory function is for. + * @param \Cake\Datasource\Locator\LocatorInterface|callable $factory The factory function used to create instances. + * @return void + */ + public function modelFactory(string $type, $factory): void + { + if (!$factory instanceof LocatorInterface && !is_callable($factory)) { + throw new InvalidArgumentException(sprintf( + '`$factory` must be an instance of Cake\Datasource\Locator\LocatorInterface or a callable.' + . ' Got type `%s` instead.', + getTypeName($factory) + )); + } + + $this->_modelFactories[$type] = $factory; + } + + /** + * Get the model type to be used by this class + * + * @return string + */ + public function getModelType(): string + { + return $this->_modelType; + } + + /** + * Set the model type to be used by this class + * + * @param string $modelType The model type + * @return $this + */ + public function setModelType(string $modelType) + { + $this->_modelType = $modelType; + + return $this; + } +} diff --git a/vendor/cakephp/datasource/Paginator.php b/vendor/cakephp/datasource/Paginator.php new file mode 100644 index 0000000..d842cd9 --- /dev/null +++ b/vendor/cakephp/datasource/Paginator.php @@ -0,0 +1,679 @@ + + */ + protected $_defaultConfig = [ + 'page' => 1, + 'limit' => 20, + 'maxLimit' => 100, + 'allowedParameters' => ['limit', 'sort', 'page', 'direction'], + ]; + + /** + * Paging params after pagination operation is done. + * + * @var array + */ + protected $_pagingParams = []; + + /** + * Handles automatic pagination of model records. + * + * ### Configuring pagination + * + * When calling `paginate()` you can use the $settings parameter to pass in + * pagination settings. These settings are used to build the queries made + * and control other pagination settings. + * + * If your settings contain a key with the current table's alias. The data + * inside that key will be used. Otherwise the top level configuration will + * be used. + * + * ``` + * $settings = [ + * 'limit' => 20, + * 'maxLimit' => 100 + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * The above settings will be used to paginate any repository. You can configure + * repository specific settings by keying the settings with the repository alias. + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'limit' => 20, + * 'maxLimit' => 100 + * ], + * 'Comments' => [ ... ] + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * This would allow you to have different pagination settings for + * `Articles` and `Comments` repositories. + * + * ### Controlling sort fields + * + * By default CakePHP will automatically allow sorting on any column on the + * repository object being paginated. Often times you will want to allow + * sorting on either associated columns or calculated fields. In these cases + * you will need to define an allowed list of all the columns you wish to allow + * sorting on. You can define the allowed sort fields in the `$settings` parameter: + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'finder' => 'custom', + * 'sortableFields' => ['title', 'author_id', 'comment_count'], + * ] + * ]; + * ``` + * + * Passing an empty array as sortableFields disallows sorting altogether. + * + * ### Paginating with custom finders + * + * You can paginate with any find type defined on your table using the + * `finder` option. + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'finder' => 'popular' + * ] + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * Would paginate using the `find('popular')` method. + * + * You can also pass an already created instance of a query to this method: + * + * ``` + * $query = $this->Articles->find('popular')->matching('Tags', function ($q) { + * return $q->where(['name' => 'CakePHP']) + * }); + * $results = $paginator->paginate($query); + * ``` + * + * ### Scoping Request parameters + * + * By using request parameter scopes you can paginate multiple queries in + * the same controller action: + * + * ``` + * $articles = $paginator->paginate($articlesQuery, ['scope' => 'articles']); + * $tags = $paginator->paginate($tagsQuery, ['scope' => 'tags']); + * ``` + * + * Each of the above queries will use different query string parameter sets + * for pagination data. An example URL paginating both results would be: + * + * ``` + * /dashboard?articles[page]=1&tags[page]=2 + * ``` + * + * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The repository or query + * to paginate. + * @param array $params Request params + * @param array $settings The settings/configuration used for pagination. + * @return \Cake\Datasource\ResultSetInterface Query results + * @throws \Cake\Datasource\Exception\PageOutOfBoundsException + */ + public function paginate(object $object, array $params = [], array $settings = []): ResultSetInterface + { + $query = null; + if ($object instanceof QueryInterface) { + $query = $object; + $object = $query->getRepository(); + if ($object === null) { + throw new CakeException('No repository set for query.'); + } + } + + $data = $this->extractData($object, $params, $settings); + $query = $this->getQuery($object, $query, $data); + + $cleanQuery = clone $query; + $results = $query->all(); + $data['numResults'] = count($results); + $data['count'] = $this->getCount($cleanQuery, $data); + + $pagingParams = $this->buildParams($data); + $alias = $object->getAlias(); + $this->_pagingParams = [$alias => $pagingParams]; + if ($pagingParams['requestedPage'] > $pagingParams['page']) { + throw new PageOutOfBoundsException([ + 'requestedPage' => $pagingParams['requestedPage'], + 'pagingParams' => $this->_pagingParams, + ]); + } + + return $results; + } + + /** + * Get query for fetching paginated results. + * + * @param \Cake\Datasource\RepositoryInterface $object Repository instance. + * @param \Cake\Datasource\QueryInterface|null $query Query Instance. + * @param array $data Pagination data. + * @return \Cake\Datasource\QueryInterface + */ + protected function getQuery(RepositoryInterface $object, ?QueryInterface $query, array $data): QueryInterface + { + if ($query === null) { + $query = $object->find($data['finder'], $data['options']); + } else { + $query->applyOptions($data['options']); + } + + return $query; + } + + /** + * Get total count of records. + * + * @param \Cake\Datasource\QueryInterface $query Query instance. + * @param array $data Pagination data. + * @return int|null + */ + protected function getCount(QueryInterface $query, array $data): ?int + { + return $query->count(); + } + + /** + * Extract pagination data needed + * + * @param \Cake\Datasource\RepositoryInterface $object The repository object. + * @param array $params Request params + * @param array $settings The settings/configuration used for pagination. + * @return array Array with keys 'defaults', 'options' and 'finder' + */ + protected function extractData(RepositoryInterface $object, array $params, array $settings): array + { + $alias = $object->getAlias(); + $defaults = $this->getDefaults($alias, $settings); + $options = $this->mergeOptions($params, $defaults); + $options = $this->validateSort($object, $options); + $options = $this->checkLimit($options); + + $options += ['page' => 1, 'scope' => null]; + $options['page'] = (int)$options['page'] < 1 ? 1 : (int)$options['page']; + [$finder, $options] = $this->_extractFinder($options); + + return compact('defaults', 'options', 'finder'); + } + + /** + * Build pagination params. + * + * @param array $data Paginator data containing keys 'options', + * 'count', 'defaults', 'finder', 'numResults'. + * @return array Paging params. + */ + protected function buildParams(array $data): array + { + $limit = $data['options']['limit']; + + $paging = [ + 'count' => $data['count'], + 'current' => $data['numResults'], + 'perPage' => $limit, + 'page' => $data['options']['page'], + 'requestedPage' => $data['options']['page'], + ]; + + $paging = $this->addPageCountParams($paging, $data); + $paging = $this->addStartEndParams($paging, $data); + $paging = $this->addPrevNextParams($paging, $data); + $paging = $this->addSortingParams($paging, $data); + + $paging += [ + 'limit' => $data['defaults']['limit'] != $limit ? $limit : null, + 'scope' => $data['options']['scope'], + 'finder' => $data['finder'], + ]; + + return $paging; + } + + /** + * Add "page" and "pageCount" params. + * + * @param array $params Paging params. + * @param array $data Paginator data. + * @return array Updated params. + */ + protected function addPageCountParams(array $params, array $data): array + { + $page = $params['page']; + $pageCount = 0; + + if ($params['count'] !== null) { + $pageCount = max((int)ceil($params['count'] / $params['perPage']), 1); + $page = min($page, $pageCount); + } elseif ($params['current'] === 0 && $params['requestedPage'] > 1) { + $page = 1; + } + + $params['page'] = $page; + $params['pageCount'] = $pageCount; + + return $params; + } + + /** + * Add "start" and "end" params. + * + * @param array $params Paging params. + * @param array $data Paginator data. + * @return array Updated params. + */ + protected function addStartEndParams(array $params, array $data): array + { + $start = $end = 0; + + if ($params['current'] > 0) { + $start = (($params['page'] - 1) * $params['perPage']) + 1; + $end = $start + $params['current'] - 1; + } + + $params['start'] = $start; + $params['end'] = $end; + + return $params; + } + + /** + * Add "prevPage" and "nextPage" params. + * + * @param array $params Paginator params. + * @param array $data Paging data. + * @return array Updated params. + */ + protected function addPrevNextParams(array $params, array $data): array + { + $params['prevPage'] = $params['page'] > 1; + if ($params['count'] === null) { + $params['nextPage'] = true; + } else { + $params['nextPage'] = $params['count'] > $params['page'] * $params['perPage']; + } + + return $params; + } + + /** + * Add sorting / ordering params. + * + * @param array $params Paginator params. + * @param array $data Paging data. + * @return array Updated params. + */ + protected function addSortingParams(array $params, array $data): array + { + $defaults = $data['defaults']; + $order = (array)$data['options']['order']; + $sortDefault = $directionDefault = false; + + if (!empty($defaults['order']) && count($defaults['order']) === 1) { + $sortDefault = key($defaults['order']); + $directionDefault = current($defaults['order']); + } + + $params += [ + 'sort' => $data['options']['sort'], + 'direction' => isset($data['options']['sort']) && count($order) ? current($order) : null, + 'sortDefault' => $sortDefault, + 'directionDefault' => $directionDefault, + 'completeSort' => $order, + ]; + + return $params; + } + + /** + * Extracts the finder name and options out of the provided pagination options. + * + * @param array $options the pagination options. + * @return array An array containing in the first position the finder name + * and in the second the options to be passed to it. + */ + protected function _extractFinder(array $options): array + { + $type = !empty($options['finder']) ? $options['finder'] : 'all'; + unset($options['finder'], $options['maxLimit']); + + if (is_array($type)) { + $options = (array)current($type) + $options; + $type = key($type); + } + + return [$type, $options]; + } + + /** + * Get paging params after pagination operation. + * + * @return array + */ + public function getPagingParams(): array + { + return $this->_pagingParams; + } + + /** + * Shim method for reading the deprecated whitelist or allowedParameters options + * + * @return array + */ + protected function getAllowedParameters(): array + { + $allowed = $this->getConfig('allowedParameters'); + if (!$allowed) { + $allowed = []; + } + $whitelist = $this->getConfig('whitelist'); + if ($whitelist) { + deprecationWarning('The `whitelist` option is deprecated. Use the `allowedParameters` option instead.'); + + return array_merge($allowed, $whitelist); + } + + return $allowed; + } + + /** + * Shim method for reading the deprecated sortWhitelist or sortableFields options. + * + * @param array $config The configuration data to coalesce and emit warnings on. + * @return array|null + */ + protected function getSortableFields(array $config): ?array + { + $allowed = $config['sortableFields'] ?? null; + if ($allowed !== null) { + return $allowed; + } + $deprecated = $config['sortWhitelist'] ?? null; + if ($deprecated !== null) { + deprecationWarning('The `sortWhitelist` option is deprecated. Use `sortableFields` instead.'); + } + + return $deprecated; + } + + /** + * Merges the various options that Paginator uses. + * Pulls settings together from the following places: + * + * - General pagination settings + * - Model specific settings. + * - Request parameters + * + * The result of this method is the aggregate of all the option sets + * combined together. You can change config value `allowedParameters` to modify + * which options/values can be set using request parameters. + * + * @param array $params Request params. + * @param array $settings The settings to merge with the request data. + * @return array Array of merged options. + */ + public function mergeOptions(array $params, array $settings): array + { + if (!empty($settings['scope'])) { + $scope = $settings['scope']; + $params = !empty($params[$scope]) ? (array)$params[$scope] : []; + } + + $allowed = $this->getAllowedParameters(); + $params = array_intersect_key($params, array_flip($allowed)); + + return array_merge($settings, $params); + } + + /** + * Get the settings for a $model. If there are no settings for a specific + * repository, the general settings will be used. + * + * @param string $alias Model name to get settings for. + * @param array $settings The settings which is used for combining. + * @return array An array of pagination settings for a model, + * or the general settings. + */ + public function getDefaults(string $alias, array $settings): array + { + if (isset($settings[$alias])) { + $settings = $settings[$alias]; + } + + $defaults = $this->getConfig(); + $defaults['whitelist'] = $defaults['allowedParameters'] = $this->getAllowedParameters(); + + $maxLimit = $settings['maxLimit'] ?? $defaults['maxLimit']; + $limit = $settings['limit'] ?? $defaults['limit']; + + if ($limit > $maxLimit) { + $limit = $maxLimit; + } + + $settings['maxLimit'] = $maxLimit; + $settings['limit'] = $limit; + + return $settings + $defaults; + } + + /** + * Validate that the desired sorting can be performed on the $object. + * + * Only fields or virtualFields can be sorted on. The direction param will + * also be sanitized. Lastly sort + direction keys will be converted into + * the model friendly order key. + * + * You can use the allowedParameters option to control which columns/fields are + * available for sorting via URL parameters. This helps prevent users from ordering large + * result sets on un-indexed values. + * + * If you need to sort on associated columns or synthetic properties you + * will need to use the `sortableFields` option. + * + * Any columns listed in the allowed sort fields will be implicitly trusted. + * You can use this to sort on synthetic columns, or columns added in custom + * find operations that may not exist in the schema. + * + * The default order options provided to paginate() will be merged with the user's + * requested sorting field/direction. + * + * @param \Cake\Datasource\RepositoryInterface $object Repository object. + * @param array $options The pagination options being used for this request. + * @return array An array of options with sort + direction removed and + * replaced with order if possible. + */ + public function validateSort(RepositoryInterface $object, array $options): array + { + if (isset($options['sort'])) { + $direction = null; + if (isset($options['direction'])) { + $direction = strtolower($options['direction']); + } + if (!in_array($direction, ['asc', 'desc'], true)) { + $direction = 'asc'; + } + + $order = isset($options['order']) && is_array($options['order']) ? $options['order'] : []; + if ($order && $options['sort'] && strpos($options['sort'], '.') === false) { + $order = $this->_removeAliases($order, $object->getAlias()); + } + + $options['order'] = [$options['sort'] => $direction] + $order; + } else { + $options['sort'] = null; + } + unset($options['direction']); + + if (empty($options['order'])) { + $options['order'] = []; + } + if (!is_array($options['order'])) { + return $options; + } + + $sortAllowed = false; + $allowed = $this->getSortableFields($options); + if ($allowed !== null) { + $options['sortableFields'] = $options['sortWhitelist'] = $allowed; + + $field = key($options['order']); + $sortAllowed = in_array($field, $allowed, true); + if (!$sortAllowed) { + $options['order'] = []; + $options['sort'] = null; + + return $options; + } + } + + if ( + $options['sort'] === null + && count($options['order']) === 1 + && !is_numeric(key($options['order'])) + ) { + $options['sort'] = key($options['order']); + } + + $options['order'] = $this->_prefix($object, $options['order'], $sortAllowed); + + return $options; + } + + /** + * Remove alias if needed. + * + * @param array $fields Current fields + * @param string $model Current model alias + * @return array $fields Unaliased fields where applicable + */ + protected function _removeAliases(array $fields, string $model): array + { + $result = []; + foreach ($fields as $field => $sort) { + if (strpos($field, '.') === false) { + $result[$field] = $sort; + continue; + } + + [$alias, $currentField] = explode('.', $field); + + if ($alias === $model) { + $result[$currentField] = $sort; + continue; + } + + $result[$field] = $sort; + } + + return $result; + } + + /** + * Prefixes the field with the table alias if possible. + * + * @param \Cake\Datasource\RepositoryInterface $object Repository object. + * @param array $order Order array. + * @param bool $allowed Whether the field was allowed. + * @return array Final order array. + */ + protected function _prefix(RepositoryInterface $object, array $order, bool $allowed = false): array + { + $tableAlias = $object->getAlias(); + $tableOrder = []; + foreach ($order as $key => $value) { + if (is_numeric($key)) { + $tableOrder[] = $value; + continue; + } + $field = $key; + $alias = $tableAlias; + + if (strpos($key, '.') !== false) { + [$alias, $field] = explode('.', $key); + } + $correctAlias = ($tableAlias === $alias); + + if ($correctAlias && $allowed) { + // Disambiguate fields in schema. As id is quite common. + if ($object->hasField($field)) { + $field = $alias . '.' . $field; + } + $tableOrder[$field] = $value; + } elseif ($correctAlias && $object->hasField($field)) { + $tableOrder[$tableAlias . '.' . $field] = $value; + } elseif (!$correctAlias && $allowed) { + $tableOrder[$alias . '.' . $field] = $value; + } + } + + return $tableOrder; + } + + /** + * Check the limit parameter and ensure it's within the maxLimit bounds. + * + * @param array $options An array of options with a limit key to be checked. + * @return array An array of options for pagination. + */ + public function checkLimit(array $options): array + { + $options['limit'] = (int)$options['limit']; + if ($options['limit'] < 1) { + $options['limit'] = 1; + } + $options['limit'] = max(min($options['limit'], $options['maxLimit']), 1); + + return $options; + } +} diff --git a/vendor/cakephp/datasource/PaginatorInterface.php b/vendor/cakephp/datasource/PaginatorInterface.php new file mode 100644 index 0000000..c2bff1a --- /dev/null +++ b/vendor/cakephp/datasource/PaginatorInterface.php @@ -0,0 +1,41 @@ +_key = $key; + + if (!is_string($config) && !($config instanceof CacheInterface)) { + throw new RuntimeException('Cache configs must be strings or \Psr\SimpleCache\CacheInterface instances.'); + } + $this->_config = $config; + } + + /** + * Load the cached results from the cache or run the query. + * + * @param object $query The query the cache read is for. + * @return mixed|null Either the cached results or null. + */ + public function fetch(object $query) + { + $key = $this->_resolveKey($query); + $storage = $this->_resolveCacher(); + $result = $storage->get($key); + if (empty($result)) { + return null; + } + + return $result; + } + + /** + * Store the result set into the cache. + * + * @param object $query The query the cache read is for. + * @param \Traversable $results The result set to store. + * @return bool True if the data was successfully cached, false on failure + */ + public function store(object $query, Traversable $results): bool + { + $key = $this->_resolveKey($query); + $storage = $this->_resolveCacher(); + + return $storage->set($key, $results); + } + + /** + * Get/generate the cache key. + * + * @param object $query The query to generate a key for. + * @return string + * @throws \RuntimeException + */ + protected function _resolveKey(object $query): string + { + if (is_string($this->_key)) { + return $this->_key; + } + $func = $this->_key; + $key = $func($query); + if (!is_string($key)) { + $msg = sprintf('Cache key functions must return a string. Got %s.', var_export($key, true)); + throw new RuntimeException($msg); + } + + return $key; + } + + /** + * Get the cache engine. + * + * @return \Psr\SimpleCache\CacheInterface + */ + protected function _resolveCacher() + { + if (is_string($this->_config)) { + return Cache::pool($this->_config); + } + + return $this->_config; + } +} diff --git a/vendor/cakephp/datasource/QueryInterface.php b/vendor/cakephp/datasource/QueryInterface.php new file mode 100644 index 0000000..e391052 --- /dev/null +++ b/vendor/cakephp/datasource/QueryInterface.php @@ -0,0 +1,405 @@ + value array representing a single aliased field + * that can be passed directly to the select() method. + * The key will contain the alias and the value the actual field name. + * + * If the field is already aliased, then it will not be changed. + * If no $alias is passed, the default table for this query will be used. + * + * @param string $field The field to alias + * @param string|null $alias the alias used to prefix the field + * @return array + */ + public function aliasField(string $field, ?string $alias = null): array; + + /** + * Runs `aliasField()` for each field in the provided list and returns + * the result under a single array. + * + * @param array $fields The fields to alias + * @param string|null $defaultAlias The default alias + * @return array + */ + public function aliasFields(array $fields, ?string $defaultAlias = null): array; + + /** + * Fetch the results for this query. + * + * Will return either the results set through setResult(), or execute this query + * and return the ResultSetDecorator object ready for streaming of results. + * + * ResultSetDecorator is a traversable object that implements the methods found + * on Cake\Collection\Collection. + * + * @return \Cake\Datasource\ResultSetInterface + */ + public function all(): ResultSetInterface; + + /** + * Populates or adds parts to current query clauses using an array. + * This is handy for passing all query clauses at once. The option array accepts: + * + * - fields: Maps to the select method + * - conditions: Maps to the where method + * - limit: Maps to the limit method + * - order: Maps to the order method + * - offset: Maps to the offset method + * - group: Maps to the group method + * - having: Maps to the having method + * - contain: Maps to the contain options for eager loading + * - join: Maps to the join method + * - page: Maps to the page method + * + * ### Example: + * + * ``` + * $query->applyOptions([ + * 'fields' => ['id', 'name'], + * 'conditions' => [ + * 'created >=' => '2013-01-01' + * ], + * 'limit' => 10 + * ]); + * ``` + * + * Is equivalent to: + * + * ``` + * $query + * ->select(['id', 'name']) + * ->where(['created >=' => '2013-01-01']) + * ->limit(10) + * ``` + * + * @param array $options list of query clauses to apply new parts to. + * @return $this + */ + public function applyOptions(array $options); + + /** + * Apply custom finds to against an existing query object. + * + * Allows custom find methods to be combined and applied to each other. + * + * ``` + * $repository->find('all')->find('recent'); + * ``` + * + * The above is an example of stacking multiple finder methods onto + * a single query. + * + * @param string $finder The finder method to use. + * @param array $options The options for the finder. + * @return static Returns a modified query. + */ + public function find(string $finder, array $options = []); + + /** + * Returns the first result out of executing this query, if the query has not been + * executed before, it will set the limit clause to 1 for performance reasons. + * + * ### Example: + * + * ``` + * $singleUser = $query->select(['id', 'username'])->first(); + * ``` + * + * @return \Cake\Datasource\EntityInterface|array|null the first result from the ResultSet + */ + public function first(); + + /** + * Returns the total amount of results for the query. + * + * @return int + */ + public function count(): int; + + /** + * Sets the number of records that should be retrieved from database, + * accepts an integer or an expression object that evaluates to an integer. + * In some databases, this operation might not be supported or will require + * the query to be transformed in order to limit the result set size. + * + * ### Examples + * + * ``` + * $query->limit(10) // generates LIMIT 10 + * $query->limit($query->newExpr()->add(['1 + 1'])); // LIMIT (1 + 1) + * ``` + * + * @param \Cake\Database\ExpressionInterface|int|null $limit number of records to be returned + * @return $this + */ + public function limit($limit); + + /** + * Sets the number of records that should be skipped from the original result set + * This is commonly used for paginating large results. Accepts an integer or an + * expression object that evaluates to an integer. + * + * In some databases, this operation might not be supported or will require + * the query to be transformed in order to limit the result set size. + * + * ### Examples + * + * ``` + * $query->offset(10) // generates OFFSET 10 + * $query->offset($query->newExpr()->add(['1 + 1'])); // OFFSET (1 + 1) + * ``` + * + * @param \Cake\Database\ExpressionInterface|int|null $offset number of records to be skipped + * @return $this + */ + public function offset($offset); + + /** + * Adds a single or multiple fields to be used in the ORDER clause for this query. + * Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used as the field itself and the value will + * represent the order in which such field should be ordered. When called multiple + * times with the same fields as key, the last order definition will prevail over + * the others. + * + * By default this function will append any passed argument to the list of fields + * to be selected, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * $query->order(['title' => 'DESC', 'author_id' => 'ASC']); + * ``` + * + * Produces: + * + * `ORDER BY title DESC, author_id ASC` + * + * ``` + * $query + * ->order(['title' => $query->newExpr('DESC NULLS FIRST')]) + * ->order('author_id'); + * ``` + * + * Will generate: + * + * `ORDER BY title DESC NULLS FIRST, author_id` + * + * ``` + * $expression = $query->newExpr()->add(['id % 2 = 0']); + * $query->order($expression)->order(['title' => 'ASC']); + * ``` + * + * Will become: + * + * `ORDER BY (id %2 = 0), title ASC` + * + * If you need to set complex expressions as order conditions, you + * should use `orderAsc()` or `orderDesc()`. + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $fields fields to be added to the list + * @param bool $overwrite whether to reset order with field list or not + * @return $this + */ + public function order($fields, $overwrite = false); + + /** + * Set the page of results you want. + * + * This method provides an easier to use interface to set the limit + offset + * in the record set you want as results. If empty the limit will default to + * the existing limit clause, and if that too is empty, then `25` will be used. + * + * Pages must start at 1. + * + * @param int $num The page number you want. + * @param int|null $limit The number of rows you want in the page. If null + * the current limit clause will be used. + * @return $this + * @throws \InvalidArgumentException If page number < 1. + */ + public function page(int $num, ?int $limit = null); + + /** + * Returns an array representation of the results after executing the query. + * + * @return array + */ + public function toArray(): array; + + /** + * Set the default Table object that will be used by this query + * and form the `FROM` clause. + * + * @param \Cake\Datasource\RepositoryInterface $repository The default repository object to use + * @return $this + */ + public function repository(RepositoryInterface $repository); + + /** + * Returns the default repository object that will be used by this query, + * that is, the repository that will appear in the from clause. + * + * @return \Cake\Datasource\RepositoryInterface|null $repository The default repository object to use + */ + public function getRepository(): ?RepositoryInterface; + + /** + * Adds a condition or set of conditions to be used in the WHERE clause for this + * query. Conditions can be expressed as an array of fields as keys with + * comparison operators in it, the values for the array will be used for comparing + * the field to such literal. Finally, conditions can be expressed as a single + * string or an array of strings. + * + * When using arrays, each entry will be joined to the rest of the conditions using + * an AND operator. Consecutive calls to this function will also join the new + * conditions specified using the AND operator. Additionally, values can be + * expressed using expression objects which can include other query objects. + * + * Any conditions created with this methods can be used with any SELECT, UPDATE + * and DELETE type of queries. + * + * ### Conditions using operators: + * + * ``` + * $query->where([ + * 'posted >=' => new DateTime('3 days ago'), + * 'title LIKE' => 'Hello W%', + * 'author_id' => 1, + * ], ['posted' => 'datetime']); + * ``` + * + * The previous example produces: + * + * `WHERE posted >= 2012-01-27 AND title LIKE 'Hello W%' AND author_id = 1` + * + * Second parameter is used to specify what type is expected for each passed + * key. Valid types can be used from the mapped with Database\Type class. + * + * ### Nesting conditions with conjunctions: + * + * ``` + * $query->where([ + * 'author_id !=' => 1, + * 'OR' => ['published' => true, 'posted <' => new DateTime('now')], + * 'NOT' => ['title' => 'Hello'] + * ], ['published' => boolean, 'posted' => 'datetime'] + * ``` + * + * The previous example produces: + * + * `WHERE author_id = 1 AND (published = 1 OR posted < '2012-02-01') AND NOT (title = 'Hello')` + * + * You can nest conditions using conjunctions as much as you like. Sometimes, you + * may want to define 2 different options for the same key, in that case, you can + * wrap each condition inside a new array: + * + * `$query->where(['OR' => [['published' => false], ['published' => true]])` + * + * Keep in mind that every time you call where() with the third param set to false + * (default), it will join the passed conditions to the previous stored list using + * the AND operator. Also, using the same array key twice in consecutive calls to + * this method will not override the previous value. + * + * ### Using expressions objects: + * + * ``` + * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR'); + * $query->where(['published' => true], ['published' => 'boolean'])->where($exp); + * ``` + * + * The previous example produces: + * + * `WHERE (id != 100 OR author_id != 1) AND published = 1` + * + * Other Query objects that be used as conditions for any field. + * + * ### Adding conditions in multiple steps: + * + * You can use callable functions to construct complex expressions, functions + * receive as first argument a new QueryExpression object and this query instance + * as second argument. Functions must return an expression object, that will be + * added the list of conditions for the query using the AND operator. + * + * ``` + * $query + * ->where(['title !=' => 'Hello World']) + * ->where(function ($exp, $query) { + * $or = $exp->or(['id' => 1]); + * $and = $exp->and(['id >' => 2, 'id <' => 10]); + * return $or->add($and); + * }); + * ``` + * + * * The previous example produces: + * + * `WHERE title != 'Hello World' AND (id = 1 OR (id > 2 AND id < 10))` + * + * ### Conditions as strings: + * + * ``` + * $query->where(['articles.author_id = authors.id', 'modified IS NULL']); + * ``` + * + * The previous example produces: + * + * `WHERE articles.author_id = authors.id AND modified IS NULL` + * + * Please note that when using the array notation or the expression objects, all + * values will be correctly quoted and transformed to the correspondent database + * data type automatically for you, thus securing your application from SQL injections. + * If you use string conditions make sure that your values are correctly quoted. + * The safest thing you can do is to never use string conditions. + * + * @param \Closure|array|string|null $conditions The conditions to filter on. + * @param array $types Associative array of type names used to bind values to query + * @param bool $overwrite whether to reset conditions with passed list or not + * @return $this + */ + public function where($conditions = null, array $types = [], bool $overwrite = false); +} diff --git a/vendor/cakephp/datasource/QueryTrait.php b/vendor/cakephp/datasource/QueryTrait.php new file mode 100644 index 0000000..708e058 --- /dev/null +++ b/vendor/cakephp/datasource/QueryTrait.php @@ -0,0 +1,618 @@ + + */ + protected $_formatters = []; + + /** + * A query cacher instance if this query has caching enabled. + * + * @var \Cake\Datasource\QueryCacher|null + */ + protected $_cache; + + /** + * Holds any custom options passed using applyOptions that could not be processed + * by any method in this class. + * + * @var array + */ + protected $_options = []; + + /** + * Whether the query is standalone or the product of an eager load operation. + * + * @var bool + */ + protected $_eagerLoaded = false; + + /** + * Set the default Table object that will be used by this query + * and form the `FROM` clause. + * + * @param \Cake\Datasource\RepositoryInterface|\Cake\ORM\Table $repository The default table object to use + * @return $this + */ + public function repository(RepositoryInterface $repository) + { + $this->_repository = $repository; + + return $this; + } + + /** + * Returns the default table object that will be used by this query, + * that is, the table that will appear in the from clause. + * + * @return \Cake\Datasource\RepositoryInterface + */ + public function getRepository(): RepositoryInterface + { + return $this->_repository; + } + + /** + * Set the result set for a query. + * + * Setting the resultset of a query will make execute() a no-op. Instead + * of executing the SQL query and fetching results, the ResultSet provided to this + * method will be returned. + * + * This method is most useful when combined with results stored in a persistent cache. + * + * @param iterable $results The results this query should return. + * @return $this + */ + public function setResult(iterable $results) + { + $this->_results = $results; + + return $this; + } + + /** + * Executes this query and returns a results iterator. This function is required + * for implementing the IteratorAggregate interface and allows the query to be + * iterated without having to call execute() manually, thus making it look like + * a result set instead of the query itself. + * + * @return \Cake\Datasource\ResultSetInterface + * @psalm-suppress ImplementedReturnTypeMismatch + */ + public function getIterator() + { + return $this->all(); + } + + /** + * Enable result caching for this query. + * + * If a query has caching enabled, it will do the following when executed: + * + * - Check the cache for $key. If there are results no SQL will be executed. + * Instead the cached results will be returned. + * - When the cached data is stale/missing the result set will be cached as the query + * is executed. + * + * ### Usage + * + * ``` + * // Simple string key + config + * $query->cache('my_key', 'db_results'); + * + * // Function to generate key. + * $query->cache(function ($q) { + * $key = serialize($q->clause('select')); + * $key .= serialize($q->clause('where')); + * return md5($key); + * }); + * + * // Using a pre-built cache engine. + * $query->cache('my_key', $engine); + * + * // Disable caching + * $query->cache(false); + * ``` + * + * @param \Closure|string|false $key Either the cache key or a function to generate the cache key. + * When using a function, this query instance will be supplied as an argument. + * @param \Psr\SimpleCache\CacheInterface|string $config Either the name of the cache config to use, or + * a cache engine instance. + * @return $this + */ + public function cache($key, $config = 'default') + { + if ($key === false) { + $this->_cache = null; + + return $this; + } + $this->_cache = new QueryCacher($key, $config); + + return $this; + } + + /** + * Returns the current configured query `_eagerLoaded` value + * + * @return bool + */ + public function isEagerLoaded(): bool + { + return $this->_eagerLoaded; + } + + /** + * Sets the query instance to be an eager loaded query. If no argument is + * passed, the current configured query `_eagerLoaded` value is returned. + * + * @param bool $value Whether to eager load. + * @return $this + */ + public function eagerLoaded(bool $value) + { + $this->_eagerLoaded = $value; + + return $this; + } + + /** + * Returns a key => value array representing a single aliased field + * that can be passed directly to the select() method. + * The key will contain the alias and the value the actual field name. + * + * If the field is already aliased, then it will not be changed. + * If no $alias is passed, the default table for this query will be used. + * + * @param string $field The field to alias + * @param string|null $alias the alias used to prefix the field + * @return array + */ + public function aliasField(string $field, ?string $alias = null): array + { + if (strpos($field, '.') === false) { + $alias = $alias ?: $this->getRepository()->getAlias(); + $aliasedField = $alias . '.' . $field; + } else { + $aliasedField = $field; + [$alias, $field] = explode('.', $field); + } + + $key = sprintf('%s__%s', $alias, $field); + + return [$key => $aliasedField]; + } + + /** + * Runs `aliasField()` for each field in the provided list and returns + * the result under a single array. + * + * @param array $fields The fields to alias + * @param string|null $defaultAlias The default alias + * @return array + */ + public function aliasFields(array $fields, ?string $defaultAlias = null): array + { + $aliased = []; + foreach ($fields as $alias => $field) { + if (is_numeric($alias) && is_string($field)) { + $aliased += $this->aliasField($field, $defaultAlias); + continue; + } + $aliased[$alias] = $field; + } + + return $aliased; + } + + /** + * Fetch the results for this query. + * + * Will return either the results set through setResult(), or execute this query + * and return the ResultSetDecorator object ready for streaming of results. + * + * ResultSetDecorator is a traversable object that implements the methods found + * on Cake\Collection\Collection. + * + * @return \Cake\Datasource\ResultSetInterface + */ + public function all(): ResultSetInterface + { + if ($this->_results !== null) { + return $this->_results; + } + + $results = null; + if ($this->_cache) { + $results = $this->_cache->fetch($this); + } + if ($results === null) { + $results = $this->_decorateResults($this->_execute()); + if ($this->_cache) { + $this->_cache->store($this, $results); + } + } + $this->_results = $results; + + return $this->_results; + } + + /** + * Returns an array representation of the results after executing the query. + * + * @return array + */ + public function toArray(): array + { + return $this->all()->toArray(); + } + + /** + * Register a new MapReduce routine to be executed on top of the database results + * Both the mapper and caller callable should be invokable objects. + * + * The MapReduce routing will only be run when the query is executed and the first + * result is attempted to be fetched. + * + * If the third argument is set to true, it will erase previous map reducers + * and replace it with the arguments passed. + * + * @param callable|null $mapper The mapper callable. + * @param callable|null $reducer The reducing function. + * @param bool $overwrite Set to true to overwrite existing map + reduce functions. + * @return $this + * @see \Cake\Collection\Iterator\MapReduce for details on how to use emit data to the map reducer. + */ + public function mapReduce(?callable $mapper = null, ?callable $reducer = null, bool $overwrite = false) + { + if ($overwrite) { + $this->_mapReduce = []; + } + if ($mapper === null) { + if (!$overwrite) { + throw new InvalidArgumentException('$mapper can be null only when $overwrite is true.'); + } + + return $this; + } + $this->_mapReduce[] = compact('mapper', 'reducer'); + + return $this; + } + + /** + * Returns the list of previously registered map reduce routines. + * + * @return array + */ + public function getMapReducers(): array + { + return $this->_mapReduce; + } + + /** + * Registers a new formatter callback function that is to be executed when trying + * to fetch the results from the database. + * + * If the second argument is set to true, it will erase previous formatters + * and replace them with the passed first argument. + * + * Callbacks are required to return an iterator object, which will be used as + * the return value for this query's result. Formatter functions are applied + * after all the `MapReduce` routines for this query have been executed. + * + * Formatting callbacks will receive two arguments, the first one being an object + * implementing `\Cake\Collection\CollectionInterface`, that can be traversed and + * modified at will. The second one being the query instance on which the formatter + * callback is being applied. + * + * Usually the query instance received by the formatter callback is the same query + * instance on which the callback was attached to, except for in a joined + * association, in that case the callback will be invoked on the association source + * side query, and it will receive that query instance instead of the one on which + * the callback was originally attached to - see the examples below! + * + * ### Examples: + * + * Return all results from the table indexed by id: + * + * ``` + * $query->select(['id', 'name'])->formatResults(function ($results) { + * return $results->indexBy('id'); + * }); + * ``` + * + * Add a new column to the ResultSet: + * + * ``` + * $query->select(['name', 'birth_date'])->formatResults(function ($results) { + * return $results->map(function ($row) { + * $row['age'] = $row['birth_date']->diff(new DateTime)->y; + * + * return $row; + * }); + * }); + * ``` + * + * Add a new column to the results with respect to the query's hydration configuration: + * + * ``` + * $query->formatResults(function ($results, $query) { + * return $results->map(function ($row) use ($query) { + * $data = [ + * 'bar' => 'baz', + * ]; + * + * if ($query->isHydrationEnabled()) { + * $row['foo'] = new Foo($data) + * } else { + * $row['foo'] = $data; + * } + * + * return $row; + * }); + * }); + * ``` + * + * Retaining access to the association target query instance of joined associations, + * by inheriting the contain callback's query argument: + * + * ``` + * // Assuming a `Articles belongsTo Authors` association that uses the join strategy + * + * $articlesQuery->contain('Authors', function ($authorsQuery) { + * return $authorsQuery->formatResults(function ($results, $query) use ($authorsQuery) { + * // Here `$authorsQuery` will always be the instance + * // where the callback was attached to. + * + * // The instance passed to the callback in the second + * // argument (`$query`), will be the one where the + * // callback is actually being applied to, in this + * // example that would be `$articlesQuery`. + * + * // ... + * + * return $results; + * }); + * }); + * ``` + * + * @param callable|null $formatter The formatting callable. + * @param int|bool $mode Whether to overwrite, append or prepend the formatter. + * @return $this + * @throws \InvalidArgumentException + */ + public function formatResults(?callable $formatter = null, $mode = self::APPEND) + { + if ($mode === self::OVERWRITE) { + $this->_formatters = []; + } + if ($formatter === null) { + if ($mode !== self::OVERWRITE) { + throw new InvalidArgumentException('$formatter can be null only when $mode is overwrite.'); + } + + return $this; + } + + if ($mode === self::PREPEND) { + array_unshift($this->_formatters, $formatter); + + return $this; + } + + $this->_formatters[] = $formatter; + + return $this; + } + + /** + * Returns the list of previously registered format routines. + * + * @return array + */ + public function getResultFormatters(): array + { + return $this->_formatters; + } + + /** + * Returns the first result out of executing this query, if the query has not been + * executed before, it will set the limit clause to 1 for performance reasons. + * + * ### Example: + * + * ``` + * $singleUser = $query->select(['id', 'username'])->first(); + * ``` + * + * @return \Cake\Datasource\EntityInterface|array|null The first result from the ResultSet. + */ + public function first() + { + if ($this->_dirty) { + $this->limit(1); + } + + return $this->all()->first(); + } + + /** + * Get the first result from the executing query or raise an exception. + * + * @throws \Cake\Datasource\Exception\RecordNotFoundException When there is no first record. + * @return \Cake\Datasource\EntityInterface|array The first result from the ResultSet. + */ + public function firstOrFail() + { + $entity = $this->first(); + if (!$entity) { + $table = $this->getRepository(); + throw new RecordNotFoundException(sprintf( + 'Record not found in table "%s"', + $table->getTable() + )); + } + + return $entity; + } + + /** + * Returns an array with the custom options that were applied to this query + * and that were not already processed by another method in this class. + * + * ### Example: + * + * ``` + * $query->applyOptions(['doABarrelRoll' => true, 'fields' => ['id', 'name']); + * $query->getOptions(); // Returns ['doABarrelRoll' => true] + * ``` + * + * @see \Cake\Datasource\QueryInterface::applyOptions() to read about the options that will + * be processed by this class and not returned by this function + * @return array + * @see applyOptions() + */ + public function getOptions(): array + { + return $this->_options; + } + + /** + * Enables calling methods from the result set as if they were from this class + * + * @param string $method the method to call + * @param array $arguments list of arguments for the method to call + * @return mixed + * @throws \BadMethodCallException if no such method exists in result set + */ + public function __call(string $method, array $arguments) + { + $resultSetClass = $this->_decoratorClass(); + if (in_array($method, get_class_methods($resultSetClass), true)) { + deprecationWarning(sprintf( + 'Calling result set method `%s()` directly on query instance is deprecated. ' . + 'You must call `all()` to retrieve the results first.', + $method + ), 2); + $results = $this->all(); + + return $results->$method(...$arguments); + } + throw new BadMethodCallException( + sprintf('Unknown method "%s"', $method) + ); + } + + /** + * Populates or adds parts to current query clauses using an array. + * This is handy for passing all query clauses at once. + * + * @param array $options the options to be applied + * @return $this + */ + abstract public function applyOptions(array $options); + + /** + * Executes this query and returns a traversable object containing the results + * + * @return \Cake\Datasource\ResultSetInterface + */ + abstract protected function _execute(): ResultSetInterface; + + /** + * Decorates the results iterator with MapReduce routines and formatters + * + * @param \Traversable $result Original results + * @return \Cake\Datasource\ResultSetInterface + */ + protected function _decorateResults(Traversable $result): ResultSetInterface + { + $decorator = $this->_decoratorClass(); + foreach ($this->_mapReduce as $functions) { + $result = new MapReduce($result, $functions['mapper'], $functions['reducer']); + } + + if (!empty($this->_mapReduce)) { + $result = new $decorator($result); + } + + foreach ($this->_formatters as $formatter) { + $result = $formatter($result, $this); + } + + if (!empty($this->_formatters) && !($result instanceof $decorator)) { + $result = new $decorator($result); + } + + return $result; + } + + /** + * Returns the name of the class to be used for decorating results + * + * @return string + * @psalm-return class-string<\Cake\Datasource\ResultSetInterface> + */ + protected function _decoratorClass(): string + { + return ResultSetDecorator::class; + } +} diff --git a/vendor/cakephp/datasource/README.md b/vendor/cakephp/datasource/README.md new file mode 100644 index 0000000..38626ea --- /dev/null +++ b/vendor/cakephp/datasource/README.md @@ -0,0 +1,82 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/datasource.svg?style=flat-square)](https://packagist.org/packages/cakephp/datasource) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Datasource Library + +This library contains interfaces for implementing Repositories and Entities using any data source, +a class for managing connections to datasources and traits to help you quickly implement the +interfaces provided by this package. + +## Repositories + +A repository is a class capable of interfacing with a data source using operations such as +`find`, `save` and `delete` by using intermediate query objects for expressing commands to +the data store and returning Entities as the single result unit of such system. + +In the case of a Relational database, a Repository would be a `Table`, which can be return single +or multiple `Entity` objects by using a `Query`. + +This library exposes the following interfaces for creating a system that implements the +repository pattern and is compatible with the CakePHP framework: + +* `RepositoryInterface` - Describes the methods for a base repository class. +* `EntityInterface` - Describes the methods for a single result object. +* `ResultSetInterface` - Represents the idea of a collection of Entities as a result of a query. + +Additionally, this package provides a few traits and classes you can use in your own implementations: + +* `EntityTrait` - Contains the default implementation for the `EntityInterface`. +* `QueryTrait` - Exposes the methods for creating a query object capable of returning decoratable collections. +* `ResultSetDecorator` - Decorates any traversable object, so it complies with `ResultSetInterface`. + + +## Connections + +This library contains a couple of utility classes meant to create and manage +connection objects. Connections are typically used in repositories for +interfacing with the actual data source system. + +The `ConnectionManager` class acts as a registry to access database connections +your application has. It provides a place that other objects can get references +to existing connections. Creating connections with the `ConnectionManager` is +easy: + +```php +use Cake\Datasource\ConnectionManager; + +ConnectionManager::config('connection-one', [ + 'className' => 'MyApp\Connections\CustomConnection', + 'param1' => 'value', + 'param2' => 'another value' +]); + +ConnectionManager::config('connection-two', [ + 'className' => 'MyApp\Connections\CustomConnection', + 'param1' => 'different value', + 'param2' => 'another value' +]); +``` + +When requested, the `ConnectionManager` will instantiate +`MyApp\Connections\CustomConnection` by passing `param1` and `param2` inside an +array as the first argument of the constructor. + +Once configured connections can be fetched using `ConnectionManager::get()`. +This method will construct and load a connection if it has not been built +before, or return the existing known connection: + +```php +use Cake\Datasource\ConnectionManager; +$conn = ConnectionManager::get('master'); +``` + +It is also possible to store connection objects by passing the instance directly to the manager: + +```php +use Cake\Datasource\ConnectionManager; +$conn = ConnectionManager::config('other', $connectionInstance); +``` + +## Documentation + +Please make sure you check the [official API documentation](https://api.cakephp.org/4.x/namespace-Cake.Datasource.html) diff --git a/vendor/cakephp/datasource/RepositoryInterface.php b/vendor/cakephp/datasource/RepositoryInterface.php new file mode 100644 index 0000000..df4ed7d --- /dev/null +++ b/vendor/cakephp/datasource/RepositoryInterface.php @@ -0,0 +1,252 @@ + $options An array that will be passed to Query::applyOptions() + * @return \Cake\Datasource\QueryInterface + */ + public function find(string $type = 'all', array $options = []); + + /** + * Returns a single record after finding it by its primary key, if no record is + * found this method throws an exception. + * + * ### Example: + * + * ``` + * $id = 10; + * $article = $articles->get($id); + * + * $article = $articles->get($id, ['contain' => ['Comments]]); + * ``` + * + * @param mixed $primaryKey primary key value to find + * @param array $options options accepted by `Table::find()` + * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id + * could not be found + * @return \Cake\Datasource\EntityInterface + * @see \Cake\Datasource\RepositoryInterface::find() + */ + public function get($primaryKey, array $options = []): EntityInterface; + + /** + * Creates a new Query instance for this repository + * + * @return \Cake\Datasource\QueryInterface + */ + public function query(); + + /** + * Update all matching records. + * + * Sets the $fields to the provided values based on $conditions. + * This method will *not* trigger beforeSave/afterSave events. If you need those + * first load a collection of records and update them. + * + * @param \Cake\Database\Expression\QueryExpression|\Closure|array|string $fields A hash of field => new value. + * @param mixed $conditions Conditions to be used, accepts anything Query::where() + * can take. + * @return int Count Returns the affected rows. + */ + public function updateAll($fields, $conditions): int; + + /** + * Deletes all records matching the provided conditions. + * + * This method will *not* trigger beforeDelete/afterDelete events. If you + * need those first load a collection of records and delete them. + * + * This method will *not* execute on associations' `cascade` attribute. You should + * use database foreign keys + ON CASCADE rules if you need cascading deletes combined + * with this method. + * + * @param mixed $conditions Conditions to be used, accepts anything Query::where() + * can take. + * @return int Returns the number of affected rows. + * @see \Cake\Datasource\RepositoryInterface::delete() + */ + public function deleteAll($conditions): int; + + /** + * Returns true if there is any record in this repository matching the specified + * conditions. + * + * @param array $conditions list of conditions to pass to the query + * @return bool + */ + public function exists($conditions): bool; + + /** + * Persists an entity based on the fields that are marked as dirty and + * returns the same entity after a successful save or false in case + * of any error. + * + * @param \Cake\Datasource\EntityInterface $entity the entity to be saved + * @param \ArrayAccess|array $options The options to use when saving. + * @return \Cake\Datasource\EntityInterface|false + */ + public function save(EntityInterface $entity, $options = []); + + /** + * Delete a single entity. + * + * Deletes an entity and possibly related associations from the database + * based on the 'dependent' option used when defining the association. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to remove. + * @param \ArrayAccess|array $options The options for the delete. + * @return bool success + */ + public function delete(EntityInterface $entity, $options = []): bool; + + /** + * This creates a new entity object. + * + * Careful: This does not trigger any field validation. + * This entity can be persisted without validation error as empty record. + * Always patch in required fields before saving. + * + * @return \Cake\Datasource\EntityInterface + */ + public function newEmptyEntity(): EntityInterface; + + /** + * Create a new entity + associated entities from an array. + * + * This is most useful when hydrating request data back into entities. + * For example, in your controller code: + * + * ``` + * $article = $this->Articles->newEntity($this->request->getData()); + * ``` + * + * The hydrated entity will correctly do an insert/update based + * on the primary key data existing in the database when the entity + * is saved. Until the entity is saved, it will be a detached record. + * + * @param array $data The data to build an entity with. + * @param array $options A list of options for the object hydration. + * @return \Cake\Datasource\EntityInterface + */ + public function newEntity(array $data, array $options = []): EntityInterface; + + /** + * Create a list of entities + associated entities from an array. + * + * This is most useful when hydrating request data back into entities. + * For example, in your controller code: + * + * ``` + * $articles = $this->Articles->newEntities($this->request->getData()); + * ``` + * + * The hydrated entities can then be iterated and saved. + * + * @param array $data The data to build an entity with. + * @param array $options A list of options for the objects hydration. + * @return array<\Cake\Datasource\EntityInterface> An array of hydrated records. + */ + public function newEntities(array $data, array $options = []): array; + + /** + * Merges the passed `$data` into `$entity` respecting the accessible + * fields configured on the entity. Returns the same entity after being + * altered. + * + * This is most useful when editing an existing entity using request data: + * + * ``` + * $article = $this->Articles->patchEntity($article, $this->request->getData()); + * ``` + * + * @param \Cake\Datasource\EntityInterface $entity the entity that will get the + * data merged in + * @param array $data key value list of fields to be merged into the entity + * @param array $options A list of options for the object hydration. + * @return \Cake\Datasource\EntityInterface + */ + public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface; + + /** + * Merges each of the elements passed in `$data` into the entities + * found in `$entities` respecting the accessible fields configured on the entities. + * Merging is done by matching the primary key in each of the elements in `$data` + * and `$entities`. + * + * This is most useful when editing a list of existing entities using request data: + * + * ``` + * $article = $this->Articles->patchEntities($articles, $this->request->getData()); + * ``` + * + * @param \Traversable|array<\Cake\Datasource\EntityInterface> $entities the entities that will get the + * data merged in + * @param array $data list of arrays to be merged into the entities + * @param array $options A list of options for the objects hydration. + * @return array<\Cake\Datasource\EntityInterface> + */ + public function patchEntities(iterable $entities, array $data, array $options = []): array; +} diff --git a/vendor/cakephp/datasource/ResultSetDecorator.php b/vendor/cakephp/datasource/ResultSetDecorator.php new file mode 100644 index 0000000..e59e63e --- /dev/null +++ b/vendor/cakephp/datasource/ResultSetDecorator.php @@ -0,0 +1,46 @@ +getInnerIterator(); + if ($iterator instanceof Countable) { + return $iterator->count(); + } + + return count($this->toArray()); + } +} diff --git a/vendor/cakephp/datasource/ResultSetInterface.php b/vendor/cakephp/datasource/ResultSetInterface.php new file mode 100644 index 0000000..a62ee77 --- /dev/null +++ b/vendor/cakephp/datasource/ResultSetInterface.php @@ -0,0 +1,28 @@ + $options The options for the rule. See above. + */ + public function __construct(callable $rule, ?string $name, array $options = []) + { + $this->rule = $rule; + $this->name = $name; + $this->options = $options; + } + + /** + * Set options for the rule invocation. + * + * Old options will be merged with the new ones. + * + * @param array $options The options to set. + * @return $this + */ + public function setOptions(array $options) + { + $this->options = $options + $this->options; + + return $this; + } + + /** + * Set the rule name. + * + * Only truthy names will be set. + * + * @param string|null $name The name to set. + * @return $this + */ + public function setName(?string $name) + { + if ($name) { + $this->name = $name; + } + + return $this; + } + + /** + * Invoke the rule. + * + * @param \Cake\Datasource\EntityInterface $entity The entity the rule + * should apply to. + * @param array $scope The rule's scope/options. + * @return bool Whether the rule passed. + */ + public function __invoke(EntityInterface $entity, array $scope): bool + { + $rule = $this->rule; + $pass = $rule($entity, $this->options + $scope); + if ($pass === true || empty($this->options['errorField'])) { + return $pass === true; + } + + $message = $this->options['message'] ?? 'invalid'; + if (is_string($pass)) { + $message = $pass; + } + if ($this->name) { + $message = [$this->name => $message]; + } else { + $message = [$message]; + } + $errorField = $this->options['errorField']; + $entity->setError($errorField, $message); + + if ($entity instanceof InvalidPropertyInterface && isset($entity->{$errorField})) { + $invalidValue = $entity->{$errorField}; + $entity->setInvalidField($errorField, $invalidValue); + } + + return $pass === true; + } +} diff --git a/vendor/cakephp/datasource/RulesAwareTrait.php b/vendor/cakephp/datasource/RulesAwareTrait.php new file mode 100644 index 0000000..5c01614 --- /dev/null +++ b/vendor/cakephp/datasource/RulesAwareTrait.php @@ -0,0 +1,120 @@ +rulesChecker(); + $options = $options ?: new ArrayObject(); + $options = is_array($options) ? new ArrayObject($options) : $options; + $hasEvents = ($this instanceof EventDispatcherInterface); + + if ($hasEvents) { + $event = $this->dispatchEvent( + 'Model.beforeRules', + compact('entity', 'options', 'operation') + ); + if ($event->isStopped()) { + return $event->getResult(); + } + } + + $result = $rules->check($entity, $operation, $options->getArrayCopy()); + + if ($hasEvents) { + $event = $this->dispatchEvent( + 'Model.afterRules', + compact('entity', 'options', 'result', 'operation') + ); + + if ($event->isStopped()) { + return $event->getResult(); + } + } + + return $result; + } + + /** + * Returns the RulesChecker for this instance. + * + * A RulesChecker object is used to test an entity for validity + * on rules that may involve complex logic or data that + * needs to be fetched from relevant datasources. + * + * @see \Cake\Datasource\RulesChecker + * @return \Cake\Datasource\RulesChecker + */ + public function rulesChecker(): RulesChecker + { + if ($this->_rulesChecker !== null) { + return $this->_rulesChecker; + } + /** @psalm-var class-string<\Cake\Datasource\RulesChecker> $class */ + $class = defined('static::RULES_CLASS') ? static::RULES_CLASS : RulesChecker::class; + /** @psalm-suppress ArgumentTypeCoercion */ + $this->_rulesChecker = $this->buildRules(new $class(['repository' => $this])); + $this->dispatchEvent('Model.buildRules', ['rules' => $this->_rulesChecker]); + + return $this->_rulesChecker; + } + + /** + * Returns a RulesChecker object after modifying the one that was supplied. + * + * Subclasses should override this method in order to initialize the rules to be applied to + * entities saved by this instance. + * + * @param \Cake\Datasource\RulesChecker $rules The rules object to be modified. + * @return \Cake\Datasource\RulesChecker + */ + public function buildRules(RulesChecker $rules): RulesChecker + { + return $rules; + } +} diff --git a/vendor/cakephp/datasource/RulesChecker.php b/vendor/cakephp/datasource/RulesChecker.php new file mode 100644 index 0000000..1bf5c9c --- /dev/null +++ b/vendor/cakephp/datasource/RulesChecker.php @@ -0,0 +1,330 @@ + + */ + protected $_rules = []; + + /** + * The list of rules to check during create operations + * + * @var array<\Cake\Datasource\RuleInvoker> + */ + protected $_createRules = []; + + /** + * The list of rules to check during update operations + * + * @var array<\Cake\Datasource\RuleInvoker> + */ + protected $_updateRules = []; + + /** + * The list of rules to check during delete operations + * + * @var array<\Cake\Datasource\RuleInvoker> + */ + protected $_deleteRules = []; + + /** + * List of options to pass to every callable rule + * + * @var array + */ + protected $_options = []; + + /** + * Whether to use I18n functions for translating default error messages + * + * @var bool + */ + protected $_useI18n = false; + + /** + * Constructor. Takes the options to be passed to all rules. + * + * @param array $options The options to pass to every rule + */ + public function __construct(array $options = []) + { + $this->_options = $options; + $this->_useI18n = function_exists('__d'); + } + + /** + * Adds a rule that will be applied to the entity both on create and update + * operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param array|string|null $name The alias for a rule, or an array of options. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function add(callable $rule, $name = null, array $options = []) + { + $this->_rules[] = $this->_addError($rule, $name, $options); + + return $this; + } + + /** + * Adds a rule that will be applied to the entity on create operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param array|string|null $name The alias for a rule or an array of options. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function addCreate(callable $rule, $name = null, array $options = []) + { + $this->_createRules[] = $this->_addError($rule, $name, $options); + + return $this; + } + + /** + * Adds a rule that will be applied to the entity on update operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param array|string|null $name The alias for a rule, or an array of options. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function addUpdate(callable $rule, $name = null, array $options = []) + { + $this->_updateRules[] = $this->_addError($rule, $name, $options); + + return $this; + } + + /** + * Adds a rule that will be applied to the entity on delete operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param array|string|null $name The alias for a rule, or an array of options. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function addDelete(callable $rule, $name = null, array $options = []) + { + $this->_deleteRules[] = $this->_addError($rule, $name, $options); + + return $this; + } + + /** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules to be applied are depended on the $mode parameter which + * can only be RulesChecker::CREATE, RulesChecker::UPDATE or RulesChecker::DELETE + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param string $mode Either 'create, 'update' or 'delete'. + * @param array $options Extra options to pass to checker functions. + * @return bool + * @throws \InvalidArgumentException if an invalid mode is passed. + */ + public function check(EntityInterface $entity, string $mode, array $options = []): bool + { + if ($mode === self::CREATE) { + return $this->checkCreate($entity, $options); + } + + if ($mode === self::UPDATE) { + return $this->checkUpdate($entity, $options); + } + + if ($mode === self::DELETE) { + return $this->checkDelete($entity, $options); + } + + throw new InvalidArgumentException('Wrong checking mode: ' . $mode); + } + + /** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules selected will be only those specified to be run on 'create' + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param array $options Extra options to pass to checker functions. + * @return bool + */ + public function checkCreate(EntityInterface $entity, array $options = []): bool + { + return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_createRules)); + } + + /** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules selected will be only those specified to be run on 'update' + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param array $options Extra options to pass to checker functions. + * @return bool + */ + public function checkUpdate(EntityInterface $entity, array $options = []): bool + { + return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_updateRules)); + } + + /** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules selected will be only those specified to be run on 'delete' + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param array $options Extra options to pass to checker functions. + * @return bool + */ + public function checkDelete(EntityInterface $entity, array $options = []): bool + { + return $this->_checkRules($entity, $options, $this->_deleteRules); + } + + /** + * Used by top level functions checkDelete, checkCreate and checkUpdate, this function + * iterates an array containing the rules to be checked and checks them all. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param array $options Extra options to pass to checker functions. + * @param array<\Cake\Datasource\RuleInvoker> $rules The list of rules that must be checked. + * @return bool + */ + protected function _checkRules(EntityInterface $entity, array $options = [], array $rules = []): bool + { + $success = true; + $options += $this->_options; + foreach ($rules as $rule) { + $success = $rule($entity, $options) && $success; + } + + return $success; + } + + /** + * Utility method for decorating any callable so that if it returns false, the correct + * property in the entity is marked as invalid. + * + * @param callable|\Cake\Datasource\RuleInvoker $rule The rule to decorate + * @param array|string|null $name The alias for a rule or an array of options + * @param array $options The options containing the error message and field. + * @return \Cake\Datasource\RuleInvoker + */ + protected function _addError(callable $rule, $name = null, array $options = []): RuleInvoker + { + if (is_array($name)) { + $options = $name; + $name = null; + } + + if (!($rule instanceof RuleInvoker)) { + $rule = new RuleInvoker($rule, $name, $options); + } else { + $rule->setOptions($options)->setName($name); + } + + return $rule; + } +} diff --git a/vendor/cakephp/datasource/SchemaInterface.php b/vendor/cakephp/datasource/SchemaInterface.php new file mode 100644 index 0000000..eef98d1 --- /dev/null +++ b/vendor/cakephp/datasource/SchemaInterface.php @@ -0,0 +1,166 @@ +|string $attrs The attributes for the column or the type name. + * @return $this + */ + public function addColumn(string $name, $attrs); + + /** + * Get column data in the table. + * + * @param string $name The column name. + * @return array|null Column data or null. + */ + public function getColumn(string $name): ?array; + + /** + * Returns true if a column exists in the schema. + * + * @param string $name Column name. + * @return bool + */ + public function hasColumn(string $name): bool; + + /** + * Remove a column from the table schema. + * + * If the column is not defined in the table, no error will be raised. + * + * @param string $name The name of the column + * @return $this + */ + public function removeColumn(string $name); + + /** + * Get the column names in the table. + * + * @return array + */ + public function columns(): array; + + /** + * Returns column type or null if a column does not exist. + * + * @param string $name The column to get the type of. + * @return string|null + */ + public function getColumnType(string $name): ?string; + + /** + * Sets the type of a column. + * + * @param string $name The column to set the type of. + * @param string $type The type to set the column to. + * @return $this + */ + public function setColumnType(string $name, string $type); + + /** + * Returns the base type name for the provided column. + * This represent the database type a more complex class is + * based upon. + * + * @param string $column The column name to get the base type from + * @return string|null The base type name + */ + public function baseColumnType(string $column): ?string; + + /** + * Check whether a field is nullable + * + * Missing columns are nullable. + * + * @param string $name The column to get the type of. + * @return bool Whether the field is nullable. + */ + public function isNullable(string $name): bool; + + /** + * Returns an array where the keys are the column names in the schema + * and the values the database type they have. + * + * @return array + */ + public function typeMap(): array; + + /** + * Get a hash of columns and their default values. + * + * @return array + */ + public function defaultValues(): array; + + /** + * Sets the options for a table. + * + * Table options allow you to set platform specific table level options. + * For example the engine type in MySQL. + * + * @param array $options The options to set, or null to read options. + * @return $this + */ + public function setOptions(array $options); + + /** + * Gets the options for a table. + * + * Table options allow you to set platform specific table level options. + * For example the engine type in MySQL. + * + * @return array An array of options. + */ + public function getOptions(): array; +} diff --git a/vendor/cakephp/datasource/SimplePaginator.php b/vendor/cakephp/datasource/SimplePaginator.php new file mode 100644 index 0000000..705be9a --- /dev/null +++ b/vendor/cakephp/datasource/SimplePaginator.php @@ -0,0 +1,40 @@ +=7.2.0", + "cakephp/core": "^4.0", + "psr/log": "^1.1", + "psr/simple-cache": "^1.0" + }, + "suggest": { + "cakephp/utility": "If you decide to use EntityTrait.", + "cakephp/collection": "If you decide to use ResultSetInterface.", + "cakephp/cache": "If you decide to use Query caching." + }, + "autoload": { + "psr-4": { + "Cake\\Datasource\\": "." + } + } +} diff --git a/vendor/cakephp/utility/CookieCryptTrait.php b/vendor/cakephp/utility/CookieCryptTrait.php new file mode 100644 index 0000000..a57bfc0 --- /dev/null +++ b/vendor/cakephp/utility/CookieCryptTrait.php @@ -0,0 +1,192 @@ + + */ + protected $_validCiphers = ['aes']; + + /** + * Returns the encryption key to be used. + * + * @return string + */ + abstract protected function _getCookieEncryptionKey(): string; + + /** + * Encrypts $value using public $type method in Security class + * + * @param array|string $value Value to encrypt + * @param string|false $encrypt Encryption mode to use. False + * disabled encryption. + * @param string|null $key Used as the security salt if specified. + * @return string Encoded values + */ + protected function _encrypt($value, $encrypt, ?string $key = null): string + { + if (is_array($value)) { + $value = $this->_implode($value); + } + if ($encrypt === false) { + return $value; + } + $this->_checkCipher($encrypt); + $prefix = 'Q2FrZQ==.'; + $cipher = ''; + if ($key === null) { + $key = $this->_getCookieEncryptionKey(); + } + if ($encrypt === 'aes') { + $cipher = Security::encrypt($value, $key); + } + + return $prefix . base64_encode($cipher); + } + + /** + * Helper method for validating encryption cipher names. + * + * @param string $encrypt The cipher name. + * @return void + * @throws \RuntimeException When an invalid cipher is provided. + */ + protected function _checkCipher(string $encrypt): void + { + if (!in_array($encrypt, $this->_validCiphers, true)) { + $msg = sprintf( + 'Invalid encryption cipher. Must be one of %s or false.', + implode(', ', $this->_validCiphers) + ); + throw new RuntimeException($msg); + } + } + + /** + * Decrypts $value using public $type method in Security class + * + * @param array|string $values Values to decrypt + * @param string|false $mode Encryption mode + * @param string|null $key Used as the security salt if specified. + * @return array|string Decrypted values + */ + protected function _decrypt($values, $mode, ?string $key = null) + { + if (is_string($values)) { + return $this->_decode($values, $mode, $key); + } + + $decrypted = []; + foreach ($values as $name => $value) { + $decrypted[$name] = $this->_decode($value, $mode, $key); + } + + return $decrypted; + } + + /** + * Decodes and decrypts a single value. + * + * @param string $value The value to decode & decrypt. + * @param string|false $encrypt The encryption cipher to use. + * @param string|null $key Used as the security salt if specified. + * @return array|string Decoded values. + */ + protected function _decode(string $value, $encrypt, ?string $key) + { + if (!$encrypt) { + return $this->_explode($value); + } + $this->_checkCipher($encrypt); + $prefix = 'Q2FrZQ==.'; + $prefixLength = strlen($prefix); + + if (strncmp($value, $prefix, $prefixLength) !== 0) { + return ''; + } + + $value = base64_decode(substr($value, $prefixLength), true); + + if ($value === false || $value === '') { + return ''; + } + + if ($key === null) { + $key = $this->_getCookieEncryptionKey(); + } + if ($encrypt === 'aes') { + $value = Security::decrypt($value, $key); + } + + if ($value === null) { + return ''; + } + + return $this->_explode($value); + } + + /** + * Implode method to keep keys are multidimensional arrays + * + * @param array $array Map of key and values + * @return string A JSON encoded string. + */ + protected function _implode(array $array): string + { + return json_encode($array); + } + + /** + * Explode method to return array from string set in CookieComponent::_implode() + * Maintains reading backwards compatibility with 1.x CookieComponent::_implode(). + * + * @param string $string A string containing JSON encoded data, or a bare string. + * @return array|string Map of key and values + */ + protected function _explode(string $string) + { + $first = substr($string, 0, 1); + if ($first === '{' || $first === '[') { + $ret = json_decode($string, true); + + return $ret ?? $string; + } + $array = []; + foreach (explode(',', $string) as $pair) { + $key = explode('|', $pair); + if (!isset($key[1])) { + return $key[0]; + } + $array[$key[0]] = $key[1]; + } + + return $array; + } +} diff --git a/vendor/cakephp/utility/Crypto/OpenSsl.php b/vendor/cakephp/utility/Crypto/OpenSsl.php new file mode 100644 index 0000000..99590d3 --- /dev/null +++ b/vendor/cakephp/utility/Crypto/OpenSsl.php @@ -0,0 +1,79 @@ +|string|int|null $path The path being searched for. Either a dot + * separated string, or an array of path segments. + * @param mixed $default The return value when the path does not exist + * @throws \InvalidArgumentException + * @return mixed The value fetched from the array, or null. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::get + */ + public static function get($data, $path, $default = null) + { + if (!(is_array($data) || $data instanceof ArrayAccess)) { + throw new InvalidArgumentException( + 'Invalid data type, must be an array or \ArrayAccess instance.' + ); + } + + if (empty($data) || $path === null) { + return $default; + } + + if (is_string($path) || is_numeric($path)) { + $parts = explode('.', (string)$path); + } else { + if (!is_array($path)) { + throw new InvalidArgumentException(sprintf( + 'Invalid Parameter %s, should be dot separated path or array.', + $path + )); + } + + $parts = $path; + } + + switch (count($parts)) { + case 1: + return $data[$parts[0]] ?? $default; + case 2: + return $data[$parts[0]][$parts[1]] ?? $default; + case 3: + return $data[$parts[0]][$parts[1]][$parts[2]] ?? $default; + default: + foreach ($parts as $key) { + if ((is_array($data) || $data instanceof ArrayAccess) && isset($data[$key])) { + $data = $data[$key]; + } else { + return $default; + } + } + } + + return $data; + } + + /** + * Gets the values from an array matching the $path expression. + * The path expression is a dot separated expression, that can contain a set + * of patterns and expressions: + * + * - `{n}` Matches any numeric key, or integer. + * - `{s}` Matches any string key. + * - `{*}` Matches any value. + * - `Foo` Matches any key with the exact same value. + * + * There are a number of attribute operators: + * + * - `=`, `!=` Equality. + * - `>`, `<`, `>=`, `<=` Value comparison. + * - `=/.../` Regular expression pattern match. + * + * Given a set of User array data, from a `$usersTable->find('all')` call: + * + * - `1.User.name` Get the name of the user at index 1. + * - `{n}.User.name` Get the name of every user in the set of users. + * - `{n}.User[id].name` Get the name of every user with an id key. + * - `{n}.User[id>=2].name` Get the name of every user with an id key greater than or equal to 2. + * - `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`. + * - `{n}.User[id=1].name` Get the Users name with id matching `1`. + * + * @param \ArrayAccess|array $data The data to extract from. + * @param string $path The path to extract. + * @return \ArrayAccess|array An array of the extracted values. Returns an empty array + * if there are no matches. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::extract + */ + public static function extract($data, string $path) + { + if (!(is_array($data) || $data instanceof ArrayAccess)) { + throw new InvalidArgumentException( + 'Invalid data type, must be an array or \ArrayAccess instance.' + ); + } + + if (empty($path)) { + return $data; + } + + // Simple paths. + if (!preg_match('/[{\[]/', $path)) { + $data = static::get($data, $path); + if ($data !== null && !(is_array($data) || $data instanceof ArrayAccess)) { + return [$data]; + } + + return $data !== null ? (array)$data : []; + } + + if (strpos($path, '[') === false) { + $tokens = explode('.', $path); + } else { + $tokens = Text::tokenize($path, '.', '[', ']'); + } + + $_key = '__set_item__'; + + $context = [$_key => [$data]]; + + foreach ($tokens as $token) { + $next = []; + + [$token, $conditions] = self::_splitConditions($token); + + foreach ($context[$_key] as $item) { + if (is_object($item) && method_exists($item, 'toArray')) { + /** @var \Cake\Datasource\EntityInterface $item */ + $item = $item->toArray(); + } + foreach ((array)$item as $k => $v) { + if (static::_matchToken($k, $token)) { + $next[] = $v; + } + } + } + + // Filter for attributes. + if ($conditions) { + $filter = []; + foreach ($next as $item) { + if ( + ( + is_array($item) || + $item instanceof ArrayAccess + ) && + static::_matches($item, $conditions) + ) { + $filter[] = $item; + } + } + $next = $filter; + } + $context = [$_key => $next]; + } + + return $context[$_key]; + } + + /** + * Split token conditions + * + * @param string $token the token being splitted. + * @return array [token, conditions] with token splitted + */ + protected static function _splitConditions(string $token): array + { + $conditions = false; + $position = strpos($token, '['); + if ($position !== false) { + $conditions = substr($token, $position); + $token = substr($token, 0, $position); + } + + return [$token, $conditions]; + } + + /** + * Check a key against a token. + * + * @param mixed $key The key in the array being searched. + * @param string $token The token being matched. + * @return bool + */ + protected static function _matchToken($key, string $token): bool + { + switch ($token) { + case '{n}': + return is_numeric($key); + case '{s}': + return is_string($key); + case '{*}': + return true; + default: + return is_numeric($token) ? ($key == $token) : $key === $token; + } + } + + /** + * Checks whether $data matches the attribute patterns + * + * @param \ArrayAccess|array $data Array of data to match. + * @param string $selector The patterns to match. + * @return bool Fitness of expression. + */ + protected static function _matches($data, string $selector): bool + { + preg_match_all( + '/(\[ (?P[^=>[><]) \s* (?P(?:\/.*?\/ | [^\]]+)) )? \])/x', + $selector, + $conditions, + PREG_SET_ORDER + ); + + foreach ($conditions as $cond) { + $attr = $cond['attr']; + $op = $cond['op'] ?? null; + $val = $cond['val'] ?? null; + + // Presence test. + if (empty($op) && empty($val) && !isset($data[$attr])) { + return false; + } + + if (is_array($data)) { + $attrPresent = array_key_exists($attr, $data); + } else { + $attrPresent = $data->offsetExists($attr); + } + // Empty attribute = fail. + if (!$attrPresent) { + return false; + } + + $prop = $data[$attr] ?? ''; + $isBool = is_bool($prop); + if ($isBool && is_numeric($val)) { + $prop = $prop ? '1' : '0'; + } elseif ($isBool) { + $prop = $prop ? 'true' : 'false'; + } elseif (is_numeric($prop)) { + $prop = (string)$prop; + } + + // Pattern matches and other operators. + if ($op === '=' && $val && $val[0] === '/') { + if (!preg_match($val, $prop)) { + return false; + } + // phpcs:disable + } elseif ( + ($op === '=' && $prop != $val) || + ($op === '!=' && $prop == $val) || + ($op === '>' && $prop <= $val) || + ($op === '<' && $prop >= $val) || + ($op === '>=' && $prop < $val) || + ($op === '<=' && $prop > $val) + // phpcs:enable + ) { + return false; + } + } + + return true; + } + + /** + * Insert $values into an array with the given $path. You can use + * `{n}` and `{s}` elements to insert $data multiple times. + * + * @param array $data The data to insert into. + * @param string $path The path to insert at. + * @param mixed $values The values to insert. + * @return array The data with $values inserted. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::insert + */ + public static function insert(array $data, string $path, $values = null): array + { + $noTokens = strpos($path, '[') === false; + if ($noTokens && strpos($path, '.') === false) { + $data[$path] = $values; + + return $data; + } + + if ($noTokens) { + $tokens = explode('.', $path); + } else { + $tokens = Text::tokenize($path, '.', '[', ']'); + } + + if ($noTokens && strpos($path, '{') === false) { + return static::_simpleOp('insert', $data, $tokens, $values); + } + + $token = array_shift($tokens); + $nextPath = implode('.', $tokens); + + [$token, $conditions] = static::_splitConditions($token); + + foreach ($data as $k => $v) { + if (static::_matchToken($k, $token)) { + if (!$conditions || static::_matches($v, $conditions)) { + $data[$k] = $nextPath + ? static::insert($v, $nextPath, $values) + : array_merge($v, (array)$values); + } + } + } + + return $data; + } + + /** + * Perform a simple insert/remove operation. + * + * @param string $op The operation to do. + * @param array $data The data to operate on. + * @param array $path The path to work on. + * @param mixed $values The values to insert when doing inserts. + * @return array data. + */ + protected static function _simpleOp(string $op, array $data, array $path, $values = null): array + { + $_list = &$data; + + $count = count($path); + $last = $count - 1; + foreach ($path as $i => $key) { + if ($op === 'insert') { + if ($i === $last) { + $_list[$key] = $values; + + return $data; + } + $_list[$key] = $_list[$key] ?? []; + $_list = &$_list[$key]; + if (!is_array($_list)) { + $_list = []; + } + } elseif ($op === 'remove') { + if ($i === $last) { + if (is_array($_list)) { + unset($_list[$key]); + } + + return $data; + } + if (!isset($_list[$key])) { + return $data; + } + $_list = &$_list[$key]; + } + } + + return $data; + } + + /** + * Remove data matching $path from the $data array. + * You can use `{n}` and `{s}` to remove multiple elements + * from $data. + * + * @param array $data The data to operate on + * @param string $path A path expression to use to remove. + * @return array The modified array. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::remove + */ + public static function remove(array $data, string $path): array + { + $noTokens = strpos($path, '[') === false; + $noExpansion = strpos($path, '{') === false; + + if ($noExpansion && $noTokens && strpos($path, '.') === false) { + unset($data[$path]); + + return $data; + } + + $tokens = $noTokens ? explode('.', $path) : Text::tokenize($path, '.', '[', ']'); + + if ($noExpansion && $noTokens) { + return static::_simpleOp('remove', $data, $tokens); + } + + $token = array_shift($tokens); + $nextPath = implode('.', $tokens); + + [$token, $conditions] = self::_splitConditions($token); + + foreach ($data as $k => $v) { + $match = static::_matchToken($k, $token); + if ($match && is_array($v)) { + if ($conditions) { + if (static::_matches($v, $conditions)) { + if ($nextPath !== '') { + $data[$k] = static::remove($v, $nextPath); + } else { + unset($data[$k]); + } + } + } else { + $data[$k] = static::remove($v, $nextPath); + } + if (empty($data[$k])) { + unset($data[$k]); + } + } elseif ($match && $nextPath === '') { + unset($data[$k]); + } + } + + return $data; + } + + /** + * Creates an associative array using `$keyPath` as the path to build its keys, and optionally + * `$valuePath` as path to get the values. If `$valuePath` is not specified, all values will be initialized + * to null (useful for Hash::merge). You can optionally group the values by what is obtained when + * following the path specified in `$groupPath`. + * + * @param array $data Array from where to extract keys and values + * @param array|string|null $keyPath A dot-separated string. + * @param array|string|null $valuePath A dot-separated string. + * @param string|null $groupPath A dot-separated string. + * @return array Combined array + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::combine + * @throws \RuntimeException When keys and values count is unequal. + */ + public static function combine(array $data, $keyPath, $valuePath = null, ?string $groupPath = null): array + { + if (empty($data)) { + return []; + } + + if (is_array($keyPath)) { + $format = array_shift($keyPath); + /** @var array $keys */ + $keys = static::format($data, $keyPath, $format); + } elseif ($keyPath === null) { + $keys = $keyPath; + } else { + /** @var array $keys */ + $keys = static::extract($data, $keyPath); + } + if ($keyPath !== null && empty($keys)) { + return []; + } + + $vals = null; + if (!empty($valuePath) && is_array($valuePath)) { + $format = array_shift($valuePath); + /** @var array $vals */ + $vals = static::format($data, $valuePath, $format); + } elseif (!empty($valuePath)) { + /** @var array $vals */ + $vals = static::extract($data, $valuePath); + } + if (empty($vals)) { + $vals = array_fill(0, $keys === null ? count($data) : count($keys), null); + } + + if (is_array($keys) && count($keys) !== count($vals)) { + throw new RuntimeException( + 'Hash::combine() needs an equal number of keys + values.' + ); + } + + if ($groupPath !== null) { + $group = static::extract($data, $groupPath); + if (!empty($group)) { + $c = is_array($keys) ? count($keys) : count($vals); + $out = []; + for ($i = 0; $i < $c; $i++) { + $group[$i] = $group[$i] ?? 0; + $out[$group[$i]] = $out[$group[$i]] ?? []; + if ($keys === null) { + $out[$group[$i]][] = $vals[$i]; + } else { + $out[$group[$i]][$keys[$i]] = $vals[$i]; + } + } + + return $out; + } + } + if (empty($vals)) { + return []; + } + + return array_combine($keys ?? range(0, count($vals) - 1), $vals); + } + + /** + * Returns a formatted series of values extracted from `$data`, using + * `$format` as the format and `$paths` as the values to extract. + * + * Usage: + * + * ``` + * $result = Hash::format($users, ['{n}.User.id', '{n}.User.name'], '%s : %s'); + * ``` + * + * The `$format` string can use any format options that `vsprintf()` and `sprintf()` do. + * + * @param array $data Source array from which to extract the data + * @param array $paths An array containing one or more Hash::extract()-style key paths + * @param string $format Format string into which values will be inserted, see sprintf() + * @return array|null An array of strings extracted from `$path` and formatted with `$format` + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::format + * @see sprintf() + * @see \Cake\Utility\Hash::extract() + */ + public static function format(array $data, array $paths, string $format): ?array + { + $extracted = []; + $count = count($paths); + + if (!$count) { + return null; + } + + for ($i = 0; $i < $count; $i++) { + $extracted[] = static::extract($data, $paths[$i]); + } + $out = []; + /** @var array $data */ + $data = $extracted; + $count = count($data[0]); + + $countTwo = count($data); + for ($j = 0; $j < $count; $j++) { + $args = []; + for ($i = 0; $i < $countTwo; $i++) { + if (array_key_exists($j, $data[$i])) { + $args[] = $data[$i][$j]; + } + } + $out[] = vsprintf($format, $args); + } + + return $out; + } + + /** + * Determines if one array contains the exact keys and values of another. + * + * @param array $data The data to search through. + * @param array $needle The values to file in $data + * @return bool true If $data contains $needle, false otherwise + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::contains + */ + public static function contains(array $data, array $needle): bool + { + if (empty($data) || empty($needle)) { + return false; + } + $stack = []; + + while (!empty($needle)) { + $key = key($needle); + $val = $needle[$key]; + unset($needle[$key]); + + if (array_key_exists($key, $data) && is_array($val)) { + $next = $data[$key]; + unset($data[$key]); + + if (!empty($val)) { + $stack[] = [$val, $next]; + } + } elseif (!array_key_exists($key, $data) || $data[$key] != $val) { + return false; + } + + if (empty($needle) && !empty($stack)) { + [$needle, $data] = array_pop($stack); + } + } + + return true; + } + + /** + * Test whether a given path exists in $data. + * This method uses the same path syntax as Hash::extract() + * + * Checking for paths that could target more than one element will + * make sure that at least one matching element exists. + * + * @param array $data The data to check. + * @param string $path The path to check for. + * @return bool Existence of path. + * @see \Cake\Utility\Hash::extract() + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::check + */ + public static function check(array $data, string $path): bool + { + $results = static::extract($data, $path); + if (!is_array($results)) { + return false; + } + + return count($results) > 0; + } + + /** + * Recursively filters a data set. + * + * @param array $data Either an array to filter, or value when in callback + * @param callable|array $callback A function to filter the data with. Defaults to + * `static::_filter()` Which strips out all non-zero empty values. + * @return array Filtered array + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::filter + */ + public static function filter(array $data, $callback = [Hash::class, '_filter']): array + { + foreach ($data as $k => $v) { + if (is_array($v)) { + $data[$k] = static::filter($v, $callback); + } + } + + return array_filter($data, $callback); + } + + /** + * Callback function for filtering. + * + * @param mixed $var Array to filter. + * @return bool + */ + protected static function _filter($var): bool + { + return $var === 0 || $var === 0.0 || $var === '0' || !empty($var); + } + + /** + * Collapses a multi-dimensional array into a single dimension, using a delimited array path for + * each array element's key, i.e. [['Foo' => ['Bar' => 'Far']]] becomes + * ['0.Foo.Bar' => 'Far'].) + * + * @param array $data Array to flatten + * @param string $separator String used to separate array key elements in a path, defaults to '.' + * @return array + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::flatten + */ + public static function flatten(array $data, string $separator = '.'): array + { + $result = []; + $stack = []; + $path = ''; + + reset($data); + while (!empty($data)) { + $key = key($data); + $element = $data[$key]; + unset($data[$key]); + + if (is_array($element) && !empty($element)) { + if (!empty($data)) { + $stack[] = [$data, $path]; + } + $data = $element; + reset($data); + $path .= $key . $separator; + } else { + $result[$path . $key] = $element; + } + + if (empty($data) && !empty($stack)) { + [$data, $path] = array_pop($stack); + reset($data); + } + } + + return $result; + } + + /** + * Expands a flat array to a nested array. + * + * For example, unflattens an array that was collapsed with `Hash::flatten()` + * into a multi-dimensional array. So, `['0.Foo.Bar' => 'Far']` becomes + * `[['Foo' => ['Bar' => 'Far']]]`. + * + * @phpstan-param non-empty-string $separator + * @param array $data Flattened array + * @param string $separator The delimiter used + * @return array + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::expand + */ + public static function expand(array $data, string $separator = '.'): array + { + $result = []; + foreach ($data as $flat => $value) { + $keys = explode($separator, (string)$flat); + $keys = array_reverse($keys); + $child = [ + $keys[0] => $value, + ]; + array_shift($keys); + foreach ($keys as $k) { + $child = [ + $k => $child, + ]; + } + + $stack = [[$child, &$result]]; + static::_merge($stack, $result); + } + + return $result; + } + + /** + * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`. + * + * The difference between this method and the built-in ones, is that if an array key contains another array, then + * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for + * keys that contain scalar values (unlike `array_merge_recursive`). + * + * This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays. + * + * @param array $data Array to be merged + * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged + * @return array Merged array + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::merge + */ + public static function merge(array $data, $merge): array + { + $args = array_slice(func_get_args(), 1); + $return = $data; + $stack = []; + + foreach ($args as &$curArg) { + $stack[] = [(array)$curArg, &$return]; + } + unset($curArg); + static::_merge($stack, $return); + + return $return; + } + + /** + * Merge helper function to reduce duplicated code between merge() and expand(). + * + * @param array $stack The stack of operations to work with. + * @param array $return The return value to operate on. + * @return void + */ + protected static function _merge(array $stack, array &$return): void + { + while (!empty($stack)) { + foreach ($stack as $curKey => &$curMerge) { + foreach ($curMerge[0] as $key => &$val) { + if (!is_array($curMerge[1])) { + continue; + } + + if ( + !empty($curMerge[1][$key]) + && (array)$curMerge[1][$key] === $curMerge[1][$key] + && (array)$val === $val + ) { + // Recurse into the current merge data as it is an array. + $stack[] = [&$val, &$curMerge[1][$key]]; + } elseif ((int)$key === $key && isset($curMerge[1][$key])) { + $curMerge[1][] = $val; + } else { + $curMerge[1][$key] = $val; + } + } + unset($stack[$curKey]); + } + unset($curMerge); + } + } + + /** + * Checks to see if all the values in the array are numeric + * + * @param array $data The array to check. + * @return bool true if values are numeric, false otherwise + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::numeric + */ + public static function numeric(array $data): bool + { + if (empty($data)) { + return false; + } + + return $data === array_filter($data, 'is_numeric'); + } + + /** + * Counts the dimensions of an array. + * Only considers the dimension of the first element in the array. + * + * If you have an un-even or heterogeneous array, consider using Hash::maxDimensions() + * to get the dimensions of the array. + * + * @param array $data Array to count dimensions on + * @return int The number of dimensions in $data + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::dimensions + */ + public static function dimensions(array $data): int + { + if (empty($data)) { + return 0; + } + reset($data); + $depth = 1; + while ($elem = array_shift($data)) { + if (is_array($elem)) { + $depth++; + $data = $elem; + } else { + break; + } + } + + return $depth; + } + + /** + * Counts the dimensions of *all* array elements. Useful for finding the maximum + * number of dimensions in a mixed array. + * + * @param array $data Array to count dimensions on + * @return int The maximum number of dimensions in $data + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::maxDimensions + */ + public static function maxDimensions(array $data): int + { + $depth = []; + if (!empty($data)) { + foreach ($data as $value) { + if (is_array($value)) { + $depth[] = static::maxDimensions($value) + 1; + } else { + $depth[] = 1; + } + } + } + + return empty($depth) ? 0 : max($depth); + } + + /** + * Map a callback across all elements in a set. + * Can be provided a path to only modify slices of the set. + * + * @param array $data The data to map over, and extract data out of. + * @param string $path The path to extract for mapping over. + * @param callable $function The function to call on each extracted value. + * @return array An array of the modified values. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::map + */ + public static function map(array $data, string $path, callable $function): array + { + $values = (array)static::extract($data, $path); + + return array_map($function, $values); + } + + /** + * Reduce a set of extracted values using `$function`. + * + * @param array $data The data to reduce. + * @param string $path The path to extract from $data. + * @param callable $function The function to call on each extracted value. + * @return mixed The reduced value. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::reduce + */ + public static function reduce(array $data, string $path, callable $function) + { + $values = (array)static::extract($data, $path); + + return array_reduce($values, $function); + } + + /** + * Apply a callback to a set of extracted values using `$function`. + * The function will get the extracted values as the first argument. + * + * ### Example + * + * You can easily count the results of an extract using apply(). + * For example to count the comments on an Article: + * + * ``` + * $count = Hash::apply($data, 'Article.Comment.{n}', 'count'); + * ``` + * + * You could also use a function like `array_sum` to sum the results. + * + * ``` + * $total = Hash::apply($data, '{n}.Item.price', 'array_sum'); + * ``` + * + * @param array $data The data to reduce. + * @param string $path The path to extract from $data. + * @param callable $function The function to call on each extracted value. + * @return mixed The results of the applied method. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::apply + */ + public static function apply(array $data, string $path, callable $function) + { + $values = (array)static::extract($data, $path); + + return $function($values); + } + + /** + * Sorts an array by any value, determined by a Set-compatible path + * + * ### Sort directions + * + * - `asc` or \SORT_ASC Sort ascending. + * - `desc` or \SORT_DESC Sort descending. + * + * ### Sort types + * + * - `regular` For regular sorting (don't change types) + * - `numeric` Compare values numerically + * - `string` Compare values as strings + * - `locale` Compare items as strings, based on the current locale + * - `natural` Compare items as strings using "natural ordering" in a human friendly way + * Will sort foo10 below foo2 as an example. + * + * To do case insensitive sorting, pass the type as an array as follows: + * + * ``` + * Hash::sort($data, 'some.attribute', 'asc', ['type' => 'regular', 'ignoreCase' => true]); + * ``` + * + * When using the array form, `type` defaults to 'regular'. The `ignoreCase` option + * defaults to `false`. + * + * @param array $data An array of data to sort + * @param string $path A Set-compatible path to the array value + * @param string|int $dir See directions above. Defaults to 'asc'. + * @param array|string $type See direction types above. Defaults to 'regular'. + * @return array Sorted array of data + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::sort + */ + public static function sort(array $data, string $path, $dir = 'asc', $type = 'regular'): array + { + if (empty($data)) { + return []; + } + $originalKeys = array_keys($data); + $numeric = is_numeric(implode('', $originalKeys)); + if ($numeric) { + $data = array_values($data); + } + /** @var array $sortValues */ + $sortValues = static::extract($data, $path); + $dataCount = count($data); + + // Make sortValues match the data length, as some keys could be missing + // the sorted value path. + $missingData = count($sortValues) < $dataCount; + if ($missingData && $numeric) { + // Get the path without the leading '{n}.' + $itemPath = substr($path, 4); + foreach ($data as $key => $value) { + $sortValues[$key] = static::get($value, $itemPath); + } + } elseif ($missingData) { + $sortValues = array_pad($sortValues, $dataCount, null); + } + $result = static::_squash($sortValues); + /** @var array $keys */ + $keys = static::extract($result, '{n}.id'); + /** @var array $values */ + $values = static::extract($result, '{n}.value'); + + if (is_string($dir)) { + $dir = strtolower($dir); + } + if (!in_array($dir, [\SORT_ASC, \SORT_DESC], true)) { + $dir = $dir === 'asc' ? \SORT_ASC : \SORT_DESC; + } + + $ignoreCase = false; + + // $type can be overloaded for case insensitive sort + if (is_array($type)) { + $type += ['ignoreCase' => false, 'type' => 'regular']; + $ignoreCase = $type['ignoreCase']; + $type = $type['type']; + } + $type = strtolower($type); + + if ($type === 'numeric') { + $type = \SORT_NUMERIC; + } elseif ($type === 'string') { + $type = \SORT_STRING; + } elseif ($type === 'natural') { + $type = \SORT_NATURAL; + } elseif ($type === 'locale') { + $type = \SORT_LOCALE_STRING; + } else { + $type = \SORT_REGULAR; + } + if ($ignoreCase) { + $values = array_map('mb_strtolower', $values); + } + array_multisort($values, $dir, $type, $keys, $dir, $type); + $sorted = []; + $keys = array_unique($keys); + + foreach ($keys as $k) { + if ($numeric) { + $sorted[] = $data[$k]; + continue; + } + if (isset($originalKeys[$k])) { + $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]]; + } else { + $sorted[$k] = $data[$k]; + } + } + + return $sorted; + } + + /** + * Helper method for sort() + * Squashes an array to a single hash so it can be sorted. + * + * @param array $data The data to squash. + * @param mixed $key The key for the data. + * @return array + */ + protected static function _squash(array $data, $key = null): array + { + $stack = []; + foreach ($data as $k => $r) { + $id = $k; + if ($key !== null) { + $id = $key; + } + if (is_array($r) && !empty($r)) { + $stack = array_merge($stack, static::_squash($r, $id)); + } else { + $stack[] = ['id' => $id, 'value' => $r]; + } + } + + return $stack; + } + + /** + * Computes the difference between two complex arrays. + * This method differs from the built-in array_diff() in that it will preserve keys + * and work on multi-dimensional arrays. + * + * @param array $data First value + * @param array $compare Second value + * @return array Returns the key => value pairs that are not common in $data and $compare + * The expression for this function is ($data - $compare) + ($compare - ($data - $compare)) + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::diff + */ + public static function diff(array $data, array $compare): array + { + if (empty($data)) { + return $compare; + } + if (empty($compare)) { + return $data; + } + $intersection = array_intersect_key($data, $compare); + while (($key = key($intersection)) !== null) { + if ($data[$key] == $compare[$key]) { + unset($data[$key], $compare[$key]); + } + next($intersection); + } + + return $data + $compare; + } + + /** + * Merges the difference between $data and $compare onto $data. + * + * @param array $data The data to append onto. + * @param array $compare The data to compare and append onto. + * @return array The merged array. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::mergeDiff + */ + public static function mergeDiff(array $data, array $compare): array + { + if (empty($data) && !empty($compare)) { + return $compare; + } + if (empty($compare)) { + return $data; + } + foreach ($compare as $key => $value) { + if (!array_key_exists($key, $data)) { + $data[$key] = $value; + } elseif (is_array($value) && is_array($data[$key])) { + $data[$key] = static::mergeDiff($data[$key], $value); + } + } + + return $data; + } + + /** + * Normalizes an array, and converts it to a standard format. + * + * @param array $data List to normalize + * @param bool $assoc If true, $data will be converted to an associative array. + * @return array + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::normalize + */ + public static function normalize(array $data, bool $assoc = true): array + { + $keys = array_keys($data); + $count = count($keys); + $numeric = true; + + if (!$assoc) { + for ($i = 0; $i < $count; $i++) { + if (!is_int($keys[$i])) { + $numeric = false; + break; + } + } + } + if (!$numeric || $assoc) { + $newList = []; + for ($i = 0; $i < $count; $i++) { + if (is_int($keys[$i])) { + $newList[$data[$keys[$i]]] = null; + } else { + $newList[$keys[$i]] = $data[$keys[$i]]; + } + } + $data = $newList; + } + + return $data; + } + + /** + * Takes in a flat array and returns a nested array + * + * ### Options: + * + * - `children` The key name to use in the resultset for children. + * - `idPath` The path to a key that identifies each entry. Should be + * compatible with Hash::extract(). Defaults to `{n}.$alias.id` + * - `parentPath` The path to a key that identifies the parent of each entry. + * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id` + * - `root` The id of the desired top-most result. + * + * @param array $data The data to nest. + * @param array $options Options are: + * @return array of results, nested + * @see \Cake\Utility\Hash::extract() + * @throws \InvalidArgumentException When providing invalid data. + * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::nest + */ + public static function nest(array $data, array $options = []): array + { + if (!$data) { + return $data; + } + + $alias = key(current($data)); + $options += [ + 'idPath' => "{n}.$alias.id", + 'parentPath' => "{n}.$alias.parent_id", + 'children' => 'children', + 'root' => null, + ]; + + $return = $idMap = []; + /** @var array $ids */ + $ids = static::extract($data, $options['idPath']); + + $idKeys = explode('.', $options['idPath']); + array_shift($idKeys); + + $parentKeys = explode('.', $options['parentPath']); + array_shift($parentKeys); + + foreach ($data as $result) { + $result[$options['children']] = []; + + $id = static::get($result, $idKeys); + $parentId = static::get($result, $parentKeys); + + if (isset($idMap[$id][$options['children']])) { + $idMap[$id] = array_merge($result, $idMap[$id]); + } else { + $idMap[$id] = array_merge($result, [$options['children'] => []]); + } + if (!$parentId || !in_array($parentId, $ids)) { + $return[] = &$idMap[$id]; + } else { + $idMap[$parentId][$options['children']][] = &$idMap[$id]; + } + } + + if (!$return) { + throw new InvalidArgumentException('Invalid data array to nest.'); + } + + if ($options['root']) { + $root = $options['root']; + } else { + $root = static::get($return[0], $parentKeys); + } + + foreach ($return as $i => $result) { + $id = static::get($result, $idKeys); + $parentId = static::get($result, $parentKeys); + if ($id !== $root && $parentId != $root) { + unset($return[$i]); + } + } + + return array_values($return); + } +} diff --git a/vendor/cakephp/utility/Inflector.php b/vendor/cakephp/utility/Inflector.php new file mode 100644 index 0000000..33438c3 --- /dev/null +++ b/vendor/cakephp/utility/Inflector.php @@ -0,0 +1,524 @@ + + */ + protected static $_plural = [ + '/(s)tatus$/i' => '\1tatuses', + '/(quiz)$/i' => '\1zes', + '/^(ox)$/i' => '\1\2en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert)(ix|ex)$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(chef)$/i' => '\1s', + '/(?:([^f])fe|([lre])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(p)erson$/i' => '\1eople', + '/(? '\1en', + '/(c)hild$/i' => '\1hildren', + '/(buffal|tomat)o$/i' => '\1\2oes', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin)us$/i' => '\1i', + '/us$/i' => 'uses', + '/(alias)$/i' => '\1es', + '/(ax|cris|test)is$/i' => '\1es', + '/s$/' => 's', + '/^$/' => '', + '/$/' => 's', + ]; + + /** + * Singular inflector rules + * + * @var array + */ + protected static $_singular = [ + '/(s)tatuses$/i' => '\1\2tatus', + '/^(.*)(menu)s$/i' => '\1\2', + '/(quiz)zes$/i' => '\\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias|lens)(es)*$/i' => '\1', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', + '/([ftw]ax)es/i' => '\1', + '/(cris|ax|test)es$/i' => '\1is', + '/(shoe)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/ouses$/' => 'ouse', + '/([^a])uses$/' => '\1us', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1\2ovie', + '/(s)eries$/i' => '\1\2eries', + '/(s)pecies$/i' => '\1\2pecies', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/(drive)s$/i' => '\1', + '/([le])ves$/i' => '\1f', + '/([^rfoa])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(p)eople$/i' => '\1\2erson', + '/(m)en$/i' => '\1an', + '/(c)hildren$/i' => '\1\2hild', + '/(n)ews$/i' => '\1\2ews', + '/eaus$/' => 'eau', + '/^(.*us)$/' => '\\1', + '/s$/i' => '', + ]; + + /** + * Irregular rules + * + * @var array + */ + protected static $_irregular = [ + 'atlas' => 'atlases', + 'beef' => 'beefs', + 'brief' => 'briefs', + 'brother' => 'brothers', + 'cafe' => 'cafes', + 'child' => 'children', + 'cookie' => 'cookies', + 'corpus' => 'corpuses', + 'cow' => 'cows', + 'criterion' => 'criteria', + 'ganglion' => 'ganglions', + 'genie' => 'genies', + 'genus' => 'genera', + 'graffito' => 'graffiti', + 'hoof' => 'hoofs', + 'loaf' => 'loaves', + 'man' => 'men', + 'money' => 'monies', + 'mongoose' => 'mongooses', + 'move' => 'moves', + 'mythos' => 'mythoi', + 'niche' => 'niches', + 'numen' => 'numina', + 'occiput' => 'occiputs', + 'octopus' => 'octopuses', + 'opus' => 'opuses', + 'ox' => 'oxen', + 'penis' => 'penises', + 'person' => 'people', + 'sex' => 'sexes', + 'soliloquy' => 'soliloquies', + 'testis' => 'testes', + 'trilby' => 'trilbys', + 'turf' => 'turfs', + 'potato' => 'potatoes', + 'hero' => 'heroes', + 'tooth' => 'teeth', + 'goose' => 'geese', + 'foot' => 'feet', + 'foe' => 'foes', + 'sieve' => 'sieves', + 'cache' => 'caches', + ]; + + /** + * Words that should not be inflected + * + * @var array + */ + protected static $_uninflected = [ + '.*[nrlm]ese', '.*data', '.*deer', '.*fish', '.*measles', '.*ois', + '.*pox', '.*sheep', 'people', 'feedback', 'stadia', '.*?media', + 'chassis', 'clippers', 'debris', 'diabetes', 'equipment', 'gallows', + 'graffiti', 'headquarters', 'information', 'innings', 'news', 'nexus', + 'pokemon', 'proceedings', 'research', 'sea[- ]bass', 'series', 'species', 'weather', + ]; + + /** + * Method cache array. + * + * @var array + */ + protected static $_cache = []; + + /** + * The initial state of Inflector so reset() works. + * + * @var array + */ + protected static $_initialState = []; + + /** + * Cache inflected values, and return if already available + * + * @param string $type Inflection type + * @param string $key Original value + * @param string|false $value Inflected value + * @return string|false Inflected value on cache hit or false on cache miss. + */ + protected static function _cache(string $type, string $key, $value = false) + { + $key = '_' . $key; + $type = '_' . $type; + if ($value !== false) { + static::$_cache[$type][$key] = $value; + + return $value; + } + if (!isset(static::$_cache[$type][$key])) { + return false; + } + + return static::$_cache[$type][$key]; + } + + /** + * Clears Inflectors inflected value caches. And resets the inflection + * rules to the initial values. + * + * @return void + */ + public static function reset(): void + { + if (empty(static::$_initialState)) { + static::$_initialState = get_class_vars(self::class); + + return; + } + foreach (static::$_initialState as $key => $val) { + if ($key !== '_initialState') { + static::${$key} = $val; + } + } + } + + /** + * Adds custom inflection $rules, of either 'plural', 'singular', + * 'uninflected' or 'irregular' $type. + * + * ### Usage: + * + * ``` + * Inflector::rules('plural', ['/^(inflect)or$/i' => '\1ables']); + * Inflector::rules('irregular', ['red' => 'redlings']); + * Inflector::rules('uninflected', ['dontinflectme']); + * ``` + * + * @param string $type The type of inflection, either 'plural', 'singular', + * or 'uninflected'. + * @param array $rules Array of rules to be added. + * @param bool $reset If true, will unset default inflections for all + * new rules that are being defined in $rules. + * @return void + */ + public static function rules(string $type, array $rules, bool $reset = false): void + { + $var = '_' . $type; + + if ($reset) { + static::${$var} = $rules; + } elseif ($type === 'uninflected') { + static::$_uninflected = array_merge( + $rules, + static::$_uninflected + ); + } else { + static::${$var} = $rules + static::${$var}; + } + + static::$_cache = []; + } + + /** + * Return $word in plural form. + * + * @param string $word Word in singular + * @return string Word in plural + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-plural-singular-forms + */ + public static function pluralize(string $word): string + { + if (isset(static::$_cache['pluralize'][$word])) { + return static::$_cache['pluralize'][$word]; + } + + if (!isset(static::$_cache['irregular']['pluralize'])) { + $words = array_keys(static::$_irregular); + static::$_cache['irregular']['pluralize'] = '/(.*?(?:\\b|_))(' . implode('|', $words) . ')$/i'; + + $upperWords = array_map('ucfirst', $words); + static::$_cache['irregular']['upperPluralize'] = '/(.*?(?:\\b|[a-z]))(' . implode('|', $upperWords) . ')$/'; + } + + if ( + preg_match(static::$_cache['irregular']['pluralize'], $word, $regs) || + preg_match(static::$_cache['irregular']['upperPluralize'], $word, $regs) + ) { + static::$_cache['pluralize'][$word] = $regs[1] . substr($regs[2], 0, 1) . + substr(static::$_irregular[strtolower($regs[2])], 1); + + return static::$_cache['pluralize'][$word]; + } + + if (!isset(static::$_cache['uninflected'])) { + static::$_cache['uninflected'] = '/^(' . implode('|', static::$_uninflected) . ')$/i'; + } + + if (preg_match(static::$_cache['uninflected'], $word, $regs)) { + static::$_cache['pluralize'][$word] = $word; + + return $word; + } + + foreach (static::$_plural as $rule => $replacement) { + if (preg_match($rule, $word)) { + static::$_cache['pluralize'][$word] = preg_replace($rule, $replacement, $word); + + return static::$_cache['pluralize'][$word]; + } + } + + return $word; + } + + /** + * Return $word in singular form. + * + * @param string $word Word in plural + * @return string Word in singular + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-plural-singular-forms + */ + public static function singularize(string $word): string + { + if (isset(static::$_cache['singularize'][$word])) { + return static::$_cache['singularize'][$word]; + } + + if (!isset(static::$_cache['irregular']['singular'])) { + $wordList = array_values(static::$_irregular); + static::$_cache['irregular']['singular'] = '/(.*?(?:\\b|_))(' . implode('|', $wordList) . ')$/i'; + + $upperWordList = array_map('ucfirst', $wordList); + static::$_cache['irregular']['singularUpper'] = '/(.*?(?:\\b|[a-z]))(' . + implode('|', $upperWordList) . + ')$/'; + } + + if ( + preg_match(static::$_cache['irregular']['singular'], $word, $regs) || + preg_match(static::$_cache['irregular']['singularUpper'], $word, $regs) + ) { + $suffix = array_search(strtolower($regs[2]), static::$_irregular, true); + $suffix = $suffix ? substr($suffix, 1) : ''; + static::$_cache['singularize'][$word] = $regs[1] . substr($regs[2], 0, 1) . $suffix; + + return static::$_cache['singularize'][$word]; + } + + if (!isset(static::$_cache['uninflected'])) { + static::$_cache['uninflected'] = '/^(' . implode('|', static::$_uninflected) . ')$/i'; + } + + if (preg_match(static::$_cache['uninflected'], $word, $regs)) { + static::$_cache['pluralize'][$word] = $word; + + return $word; + } + + foreach (static::$_singular as $rule => $replacement) { + if (preg_match($rule, $word)) { + static::$_cache['singularize'][$word] = preg_replace($rule, $replacement, $word); + + return static::$_cache['singularize'][$word]; + } + } + static::$_cache['singularize'][$word] = $word; + + return $word; + } + + /** + * Returns the input lower_case_delimited_string as a CamelCasedString. + * + * @param string $string String to camelize + * @param string $delimiter the delimiter in the input string + * @return string CamelizedStringLikeThis. + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-camelcase-and-under-scored-forms + */ + public static function camelize(string $string, string $delimiter = '_'): string + { + $cacheKey = __FUNCTION__ . $delimiter; + + $result = static::_cache($cacheKey, $string); + + if ($result === false) { + $result = str_replace(' ', '', static::humanize($string, $delimiter)); + static::_cache($cacheKey, $string, $result); + } + + return $result; + } + + /** + * Returns the input CamelCasedString as an underscored_string. + * + * Also replaces dashes with underscores + * + * @param string $string CamelCasedString to be "underscorized" + * @return string underscore_version of the input string + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-camelcase-and-under-scored-forms + */ + public static function underscore(string $string): string + { + return static::delimit(str_replace('-', '_', $string), '_'); + } + + /** + * Returns the input CamelCasedString as an dashed-string. + * + * Also replaces underscores with dashes + * + * @param string $string The string to dasherize. + * @return string Dashed version of the input string + */ + public static function dasherize(string $string): string + { + return static::delimit(str_replace('_', '-', $string), '-'); + } + + /** + * Returns the input lower_case_delimited_string as 'A Human Readable String'. + * (Underscores are replaced by spaces and capitalized following words.) + * + * @param string $string String to be humanized + * @param string $delimiter the character to replace with a space + * @return string Human-readable string + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-human-readable-forms + */ + public static function humanize(string $string, string $delimiter = '_'): string + { + $cacheKey = __FUNCTION__ . $delimiter; + + $result = static::_cache($cacheKey, $string); + + if ($result === false) { + $result = explode(' ', str_replace($delimiter, ' ', $string)); + foreach ($result as &$word) { + $word = mb_strtoupper(mb_substr($word, 0, 1)) . mb_substr($word, 1); + } + $result = implode(' ', $result); + static::_cache($cacheKey, $string, $result); + } + + return $result; + } + + /** + * Expects a CamelCasedInputString, and produces a lower_case_delimited_string + * + * @param string $string String to delimit + * @param string $delimiter the character to use as a delimiter + * @return string delimited string + */ + public static function delimit(string $string, string $delimiter = '_'): string + { + $cacheKey = __FUNCTION__ . $delimiter; + + $result = static::_cache($cacheKey, $string); + + if ($result === false) { + $result = mb_strtolower(preg_replace('/(?<=\\w)([A-Z])/', $delimiter . '\\1', $string)); + static::_cache($cacheKey, $string, $result); + } + + return $result; + } + + /** + * Returns corresponding table name for given model $className. ("people" for the model class "Person"). + * + * @param string $className Name of class to get database table name for + * @return string Name of the database table for given class + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-table-and-class-name-forms + */ + public static function tableize(string $className): string + { + $result = static::_cache(__FUNCTION__, $className); + + if ($result === false) { + $result = static::pluralize(static::underscore($className)); + static::_cache(__FUNCTION__, $className, $result); + } + + return $result; + } + + /** + * Returns Cake model class name ("Person" for the database table "people".) for given database table. + * + * @param string $tableName Name of database table to get class name for + * @return string Class name + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-table-and-class-name-forms + */ + public static function classify(string $tableName): string + { + $result = static::_cache(__FUNCTION__, $tableName); + + if ($result === false) { + $result = static::camelize(static::singularize($tableName)); + static::_cache(__FUNCTION__, $tableName, $result); + } + + return $result; + } + + /** + * Returns camelBacked version of an underscored string. + * + * @param string $string String to convert. + * @return string in variable form + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-variable-names + */ + public static function variable(string $string): string + { + $result = static::_cache(__FUNCTION__, $string); + + if ($result === false) { + $camelized = static::camelize(static::underscore($string)); + $replace = strtolower(substr($camelized, 0, 1)); + $result = $replace . substr($camelized, 1); + static::_cache(__FUNCTION__, $string, $result); + } + + return $result; + } +} diff --git a/vendor/cakephp/utility/LICENSE.txt b/vendor/cakephp/utility/LICENSE.txt new file mode 100644 index 0000000..b938c9e --- /dev/null +++ b/vendor/cakephp/utility/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/cakephp/utility/MergeVariablesTrait.php b/vendor/cakephp/utility/MergeVariablesTrait.php new file mode 100644 index 0000000..39bafb8 --- /dev/null +++ b/vendor/cakephp/utility/MergeVariablesTrait.php @@ -0,0 +1,118 @@ + $properties An array of properties and the merge strategy for them. + * @param array $options The options to use when merging properties. + * @return void + */ + protected function _mergeVars(array $properties, array $options = []): void + { + $class = static::class; + $parents = []; + while (true) { + $parent = get_parent_class($class); + if (!$parent) { + break; + } + $parents[] = $parent; + $class = $parent; + } + foreach ($properties as $property) { + if (!property_exists($this, $property)) { + continue; + } + $thisValue = $this->{$property}; + if ($thisValue === null || $thisValue === false) { + continue; + } + $this->_mergeProperty($property, $parents, $options); + } + } + + /** + * Merge a single property with the values declared in all parent classes. + * + * @param string $property The name of the property being merged. + * @param array $parentClasses An array of classes you want to merge with. + * @param array $options Options for merging the property, see _mergeVars() + * @return void + */ + protected function _mergeProperty(string $property, array $parentClasses, array $options): void + { + $thisValue = $this->{$property}; + $isAssoc = false; + if ( + isset($options['associative']) && + in_array($property, (array)$options['associative'], true) + ) { + $isAssoc = true; + } + + if ($isAssoc) { + $thisValue = Hash::normalize($thisValue); + } + foreach ($parentClasses as $class) { + $parentProperties = get_class_vars($class); + if (empty($parentProperties[$property])) { + continue; + } + $parentProperty = $parentProperties[$property]; + if (!is_array($parentProperty)) { + continue; + } + $thisValue = $this->_mergePropertyData($thisValue, $parentProperty, $isAssoc); + } + $this->{$property} = $thisValue; + } + + /** + * Merge each of the keys in a property together. + * + * @param array $current The current merged value. + * @param array $parent The parent class' value. + * @param bool $isAssoc Whether the merging should be done in associative mode. + * @return array The updated value. + */ + protected function _mergePropertyData(array $current, array $parent, bool $isAssoc) + { + if (!$isAssoc) { + return array_merge($parent, $current); + } + $parent = Hash::normalize($parent); + foreach ($parent as $key => $value) { + if (!isset($current[$key])) { + $current[$key] = $value; + } + } + + return $current; + } +} diff --git a/vendor/cakephp/utility/README.md b/vendor/cakephp/utility/README.md new file mode 100644 index 0000000..45601b8 --- /dev/null +++ b/vendor/cakephp/utility/README.md @@ -0,0 +1,91 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/utility.svg?style=flat-square)](https://packagist.org/packages/cakephp/utility) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Utility Classes + +This library provides a range of utility classes that are used throughout the CakePHP framework + +## What's in the toolbox? + +### Hash + +A ``Hash`` (as in PHP arrays) class, capable of extracting data using an intuitive DSL: + +```php +$things = [ + ['name' => 'Mark', 'age' => 15], + ['name' => 'Susan', 'age' => 30], + ['name' => 'Lucy', 'age' => 25] +]; + +$bigPeople = Hash::extract($things, '{n}[age>21].name'); + +// $bigPeople will contain ['Susan', 'Lucy'] +``` + +Check the [official Hash class documentation](https://book.cakephp.org/4/en/core-libraries/hash.html) + +### Inflector + +The Inflector class takes a string and can manipulate it to handle word variations +such as pluralizations or camelizing. + +```php +echo Inflector::pluralize('Apple'); // echoes Apples + +echo Inflector::singularize('People'); // echoes Person +``` + +Check the [official Inflector class documentation](https://book.cakephp.org/4/en/core-libraries/inflector.html) + +### Text + +The Text class includes convenience methods for creating and manipulating strings. + +```php +Text::insert( + 'My name is :name and I am :age years old.', + ['name' => 'Bob', 'age' => '65'] +); +// Returns: "My name is Bob and I am 65 years old." + +$text = 'This is the song that never ends.'; +$result = Text::wrap($text, 22); + +// Returns +This is the song +that never ends. +``` + +Check the [official Text class documentation](https://book.cakephp.org/4/en/core-libraries/text.html) + +### Security + +The security library handles basic security measures such as providing methods for hashing and encrypting data. + +```php +$key = 'wt1U5MACWJFTXGenFoZoiLwQGrLgdbHA'; +$result = Security::encrypt($value, $key); + +Security::decrypt($result, $key); +``` + +Check the [official Security class documentation](https://book.cakephp.org/4/en/core-libraries/security.html) + +### Xml + +The Xml class allows you to easily transform arrays into SimpleXMLElement or DOMDocument objects +and back into arrays again + +```php +$data = [ + 'post' => [ + 'id' => 1, + 'title' => 'Best post', + 'body' => ' ... ' + ] +]; +$xml = Xml::build($data); +``` + +Check the [official Xml class documentation](https://book.cakephp.org/4/en/core-libraries/xml.html) diff --git a/vendor/cakephp/utility/Security.php b/vendor/cakephp/utility/Security.php new file mode 100644 index 0000000..5fcdc4f --- /dev/null +++ b/vendor/cakephp/utility/Security.php @@ -0,0 +1,310 @@ +encrypt($plain, $key); + $hmac = hash_hmac('sha256', $ciphertext, $key); + + return $hmac . $ciphertext; + } + + /** + * Check the encryption key for proper length. + * + * @param string $key Key to check. + * @param string $method The method the key is being checked for. + * @return void + * @throws \InvalidArgumentException When key length is not 256 bit/32 bytes + */ + protected static function _checkKey(string $key, string $method): void + { + if (mb_strlen($key, '8bit') < 32) { + throw new InvalidArgumentException( + sprintf('Invalid key for %s, key must be at least 256 bits (32 bytes) long.', $method) + ); + } + } + + /** + * Decrypt a value using AES-256. + * + * @param string $cipher The ciphertext to decrypt. + * @param string $key The 256 bit/32 byte key to use as a cipher key. + * @param string|null $hmacSalt The salt to use for the HMAC process. + * Leave null to use value of Security::getSalt(). + * @return string|null Decrypted data. Any trailing null bytes will be removed. + * @throws \InvalidArgumentException On invalid data or key. + */ + public static function decrypt(string $cipher, string $key, ?string $hmacSalt = null): ?string + { + self::_checkKey($key, 'decrypt()'); + if (empty($cipher)) { + throw new InvalidArgumentException('The data to decrypt cannot be empty.'); + } + if ($hmacSalt === null) { + $hmacSalt = static::getSalt(); + } + + // Generate the encryption and hmac key. + $key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit'); + + // Split out hmac for comparison + $macSize = 64; + $hmac = mb_substr($cipher, 0, $macSize, '8bit'); + $cipher = mb_substr($cipher, $macSize, null, '8bit'); + + $compareHmac = hash_hmac('sha256', $cipher, $key); + if (!static::constantEquals($hmac, $compareHmac)) { + return null; + } + + $crypto = static::engine(); + + return $crypto->decrypt($cipher, $key); + } + + /** + * A timing attack resistant comparison that prefers native PHP implementations. + * + * @param mixed $original The original value. + * @param mixed $compare The comparison value. + * @return bool + * @since 3.6.2 + */ + public static function constantEquals($original, $compare): bool + { + return is_string($original) && is_string($compare) && hash_equals($original, $compare); + } + + /** + * Gets the HMAC salt to be used for encryption/decryption + * routines. + * + * @return string The currently configured salt + */ + public static function getSalt(): string + { + if (static::$_salt === null) { + throw new RuntimeException( + 'Salt not set. Use Security::setSalt() to set one, ideally in `config/bootstrap.php`.' + ); + } + + return static::$_salt; + } + + /** + * Sets the HMAC salt to be used for encryption/decryption + * routines. + * + * @param string $salt The salt to use for encryption routines. + * @return void + */ + public static function setSalt(string $salt): void + { + static::$_salt = $salt; + } +} diff --git a/vendor/cakephp/utility/Text.php b/vendor/cakephp/utility/Text.php new file mode 100644 index 0000000..570a247 --- /dev/null +++ b/vendor/cakephp/utility/Text.php @@ -0,0 +1,1187 @@ + + */ + protected static $_defaultHtmlNoCount = [ + 'style', + 'script', + ]; + + /** + * Generate a random UUID version 4 + * + * Warning: This method should not be used as a random seed for any cryptographic operations. + * Instead, you should use the openssl or mcrypt extensions. + * + * It should also not be used to create identifiers that have security implications, such as + * 'unguessable' URL identifiers. Instead, you should use {@link \Cake\Utility\Security::randomBytes()}` for that. + * + * @see https://www.ietf.org/rfc/rfc4122.txt + * @return string RFC 4122 UUID + * @copyright Matt Farina MIT License https://github.com/lootils/uuid/blob/master/LICENSE + */ + public static function uuid(): string + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + // 32 bits for "time_low" + random_int(0, 65535), + random_int(0, 65535), + // 16 bits for "time_mid" + random_int(0, 65535), + // 12 bits before the 0100 of (version) 4 for "time_hi_and_version" + random_int(0, 4095) | 0x4000, + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + random_int(0, 0x3fff) | 0x8000, + // 48 bits for "node" + random_int(0, 65535), + random_int(0, 65535), + random_int(0, 65535) + ); + } + + /** + * Tokenizes a string using $separator, ignoring any instance of $separator that appears between + * $leftBound and $rightBound. + * + * @param string $data The data to tokenize. + * @param string $separator The token to split the data on. + * @param string $leftBound The left boundary to ignore separators in. + * @param string $rightBound The right boundary to ignore separators in. + * @return array Array of tokens in $data. + */ + public static function tokenize( + string $data, + string $separator = ',', + string $leftBound = '(', + string $rightBound = ')' + ): array { + if (empty($data)) { + return []; + } + + $depth = 0; + $offset = 0; + $buffer = ''; + $results = []; + $length = mb_strlen($data); + $open = false; + + while ($offset <= $length) { + $tmpOffset = -1; + $offsets = [ + mb_strpos($data, $separator, $offset), + mb_strpos($data, $leftBound, $offset), + mb_strpos($data, $rightBound, $offset), + ]; + for ($i = 0; $i < 3; $i++) { + if ($offsets[$i] !== false && ($offsets[$i] < $tmpOffset || $tmpOffset === -1)) { + $tmpOffset = $offsets[$i]; + } + } + if ($tmpOffset !== -1) { + $buffer .= mb_substr($data, $offset, $tmpOffset - $offset); + $char = mb_substr($data, $tmpOffset, 1); + if (!$depth && $char === $separator) { + $results[] = $buffer; + $buffer = ''; + } else { + $buffer .= $char; + } + if ($leftBound !== $rightBound) { + if ($char === $leftBound) { + $depth++; + } + if ($char === $rightBound) { + $depth--; + } + } else { + if ($char === $leftBound) { + if (!$open) { + $depth++; + $open = true; + } else { + $depth--; + $open = false; + } + } + } + $tmpOffset += 1; + $offset = $tmpOffset; + } else { + $results[] = $buffer . mb_substr($data, $offset); + $offset = $length + 1; + } + } + if (empty($results) && !empty($buffer)) { + $results[] = $buffer; + } + + if (!empty($results)) { + return array_map('trim', $results); + } + + return []; + } + + /** + * Replaces variable placeholders inside a $str with any given $data. Each key in the $data array + * corresponds to a variable placeholder name in $str. + * Example: + * ``` + * Text::insert(':name is :age years old.', ['name' => 'Bob', 'age' => '65']); + * ``` + * Returns: Bob is 65 years old. + * + * Available $options are: + * + * - before: The character or string in front of the name of the variable placeholder (Defaults to `:`) + * - after: The character or string after the name of the variable placeholder (Defaults to null) + * - escape: The character or string used to escape the before character / string (Defaults to `\`) + * - format: A regex to use for matching variable placeholders. Default is: `/(? val array where each key stands for a placeholder variable name + * to be replaced with val + * @param array $options An array of options, see description above + * @return string + */ + public static function insert(string $str, array $data, array $options = []): string + { + $defaults = [ + 'before' => ':', 'after' => '', 'escape' => '\\', 'format' => null, 'clean' => false, + ]; + $options += $defaults; + if (empty($data)) { + return $options['clean'] ? static::cleanInsert($str, $options) : $str; + } + + if (strpos($str, '?') !== false && is_numeric(key($data))) { + deprecationWarning( + 'Using Text::insert() with `?` placeholders is deprecated. ' . + 'Use sprintf() with `%s` placeholders instead.' + ); + + $offset = 0; + while (($pos = strpos($str, '?', $offset)) !== false) { + $val = array_shift($data); + $offset = $pos + strlen($val); + $str = substr_replace($str, $val, $pos, 1); + } + + return $options['clean'] ? static::cleanInsert($str, $options) : $str; + } + + $format = $options['format']; + if ($format === null) { + $format = sprintf( + '/(? $tempData */ + $tempData = array_combine($dataKeys, $hashKeys); + krsort($tempData); + + foreach ($tempData as $key => $hashVal) { + $key = sprintf($format, preg_quote($key, '/')); + $str = preg_replace($key, $hashVal, $str); + } + /** @var array $dataReplacements */ + $dataReplacements = array_combine($hashKeys, array_values($data)); + foreach ($dataReplacements as $tmpHash => $tmpValue) { + $tmpValue = is_array($tmpValue) ? '' : (string)$tmpValue; + $str = str_replace($tmpHash, $tmpValue, $str); + } + + if (!isset($options['format']) && isset($options['before'])) { + $str = str_replace($options['escape'] . $options['before'], $options['before'], $str); + } + + return $options['clean'] ? static::cleanInsert($str, $options) : $str; + } + + /** + * Cleans up a Text::insert() formatted string with given $options depending on the 'clean' key in + * $options. The default method used is text but html is also available. The goal of this function + * is to replace all whitespace and unneeded markup around placeholders that did not get replaced + * by Text::insert(). + * + * @param string $str String to clean. + * @param array $options Options list. + * @return string + * @see \Cake\Utility\Text::insert() + */ + public static function cleanInsert(string $str, array $options): string + { + $clean = $options['clean']; + if (!$clean) { + return $str; + } + if ($clean === true) { + $clean = ['method' => 'text']; + } + if (!is_array($clean)) { + $clean = ['method' => $options['clean']]; + } + switch ($clean['method']) { + case 'html': + $clean += [ + 'word' => '[\w,.]+', + 'andText' => true, + 'replacement' => '', + ]; + $kleenex = sprintf( + '/[\s]*[a-z]+=(")(%s%s%s[\s]*)+\\1/i', + preg_quote($options['before'], '/'), + $clean['word'], + preg_quote($options['after'], '/') + ); + $str = preg_replace($kleenex, $clean['replacement'], $str); + if ($clean['andText']) { + $options['clean'] = ['method' => 'text']; + $str = static::cleanInsert($str, $options); + } + break; + case 'text': + $clean += [ + 'word' => '[\w,.]+', + 'gap' => '[\s]*(?:(?:and|or)[\s]*)?', + 'replacement' => '', + ]; + + $kleenex = sprintf( + '/(%s%s%s%s|%s%s%s%s)/', + preg_quote($options['before'], '/'), + $clean['word'], + preg_quote($options['after'], '/'), + $clean['gap'], + $clean['gap'], + preg_quote($options['before'], '/'), + $clean['word'], + preg_quote($options['after'], '/') + ); + $str = preg_replace($kleenex, $clean['replacement'], $str); + break; + } + + return $str; + } + + /** + * Wraps text to a specific width, can optionally wrap at word breaks. + * + * ### Options + * + * - `width` The width to wrap to. Defaults to 72. + * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true. + * - `indent` String to indent with. Defaults to null. + * - `indentAt` 0 based index to start indenting at. Defaults to 0. + * + * @param string $text The text to format. + * @param array|int $options Array of options to use, or an integer to wrap the text to. + * @return string Formatted text. + */ + public static function wrap(string $text, $options = []): string + { + if (is_numeric($options)) { + $options = ['width' => $options]; + } + $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0]; + if ($options['wordWrap']) { + $wrapped = self::wordWrap($text, $options['width'], "\n"); + } else { + $wrapped = trim(chunk_split($text, $options['width'] - 1, "\n")); + } + if (!empty($options['indent'])) { + $chunks = explode("\n", $wrapped); + for ($i = $options['indentAt'], $len = count($chunks); $i < $len; $i++) { + $chunks[$i] = $options['indent'] . $chunks[$i]; + } + $wrapped = implode("\n", $chunks); + } + + return $wrapped; + } + + /** + * Wraps a complete block of text to a specific width, can optionally wrap + * at word breaks. + * + * ### Options + * + * - `width` The width to wrap to. Defaults to 72. + * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true. + * - `indent` String to indent with. Defaults to null. + * - `indentAt` 0 based index to start indenting at. Defaults to 0. + * + * @param string $text The text to format. + * @param array|int $options Array of options to use, or an integer to wrap the text to. + * @return string Formatted text. + */ + public static function wrapBlock(string $text, $options = []): string + { + if (is_numeric($options)) { + $options = ['width' => $options]; + } + $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0]; + + if (!empty($options['indentAt']) && $options['indentAt'] === 0) { + $indentLength = !empty($options['indent']) ? strlen($options['indent']) : 0; + $options['width'] -= $indentLength; + + return self::wrap($text, $options); + } + + $wrapped = self::wrap($text, $options); + + if (!empty($options['indent'])) { + $indentationLength = mb_strlen($options['indent']); + $chunks = explode("\n", $wrapped); + $count = count($chunks); + if ($count < 2) { + return $wrapped; + } + $toRewrap = ''; + for ($i = $options['indentAt']; $i < $count; $i++) { + $toRewrap .= mb_substr($chunks[$i], $indentationLength) . ' '; + unset($chunks[$i]); + } + $options['width'] -= $indentationLength; + $options['indentAt'] = 0; + $rewrapped = self::wrap($toRewrap, $options); + $newChunks = explode("\n", $rewrapped); + + $chunks = array_merge($chunks, $newChunks); + $wrapped = implode("\n", $chunks); + } + + return $wrapped; + } + + /** + * Unicode and newline aware version of wordwrap. + * + * @phpstan-param non-empty-string $break + * @param string $text The text to format. + * @param int $width The width to wrap to. Defaults to 72. + * @param string $break The line is broken using the optional break parameter. Defaults to '\n'. + * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width. + * @return string Formatted text. + */ + public static function wordWrap(string $text, int $width = 72, string $break = "\n", bool $cut = false): string + { + $paragraphs = explode($break, $text); + foreach ($paragraphs as &$paragraph) { + $paragraph = static::_wordWrap($paragraph, $width, $break, $cut); + } + + return implode($break, $paragraphs); + } + + /** + * Unicode aware version of wordwrap as helper method. + * + * @param string $text The text to format. + * @param int $width The width to wrap to. Defaults to 72. + * @param string $break The line is broken using the optional break parameter. Defaults to '\n'. + * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width. + * @return string Formatted text. + */ + protected static function _wordWrap(string $text, int $width = 72, string $break = "\n", bool $cut = false): string + { + $parts = []; + if ($cut) { + while (mb_strlen($text) > 0) { + $part = mb_substr($text, 0, $width); + $parts[] = trim($part); + $text = trim(mb_substr($text, mb_strlen($part))); + } + + return implode($break, $parts); + } + + while (mb_strlen($text) > 0) { + if ($width >= mb_strlen($text)) { + $parts[] = trim($text); + break; + } + + $part = mb_substr($text, 0, $width); + $nextChar = mb_substr($text, $width, 1); + if ($nextChar !== ' ') { + $breakAt = mb_strrpos($part, ' '); + if ($breakAt === false) { + $breakAt = mb_strpos($text, ' ', $width); + } + if ($breakAt === false) { + $parts[] = trim($text); + break; + } + $part = mb_substr($text, 0, $breakAt); + } + + $part = trim($part); + $parts[] = $part; + $text = trim(mb_substr($text, mb_strlen($part))); + } + + return implode($break, $parts); + } + + /** + * Highlights a given phrase in a text. You can specify any expression in highlighter that + * may include the \1 expression to include the $phrase found. + * + * ### Options: + * + * - `format` The piece of HTML with that the phrase will be highlighted + * - `html` If true, will ignore any HTML tags, ensuring that only the correct text is highlighted + * - `regex` A custom regex rule that is used to match words, default is '|$tag|iu' + * - `limit` A limit, optional, defaults to -1 (none) + * + * @param string $text Text to search the phrase in. + * @param array|string $phrase The phrase or phrases that will be searched. + * @param array $options An array of HTML attributes and options. + * @return string The highlighted text + * @link https://book.cakephp.org/4/en/core-libraries/text.html#highlighting-substrings + */ + public static function highlight(string $text, $phrase, array $options = []): string + { + if (empty($phrase)) { + return $text; + } + + $defaults = [ + 'format' => '\1', + 'html' => false, + 'regex' => '|%s|iu', + 'limit' => -1, + ]; + $options += $defaults; + + if (is_array($phrase)) { + $replace = []; + $with = []; + + foreach ($phrase as $key => $segment) { + $segment = '(' . preg_quote($segment, '|') . ')'; + if ($options['html']) { + $segment = "(?![^<]+>)$segment(?![^<]+>)"; + } + + $with[] = is_array($options['format']) ? $options['format'][$key] : $options['format']; + $replace[] = sprintf($options['regex'], $segment); + } + + return preg_replace($replace, $with, $text, $options['limit']); + } + + $phrase = '(' . preg_quote($phrase, '|') . ')'; + if ($options['html']) { + $phrase = "(?![^<]+>)$phrase(?![^<]+>)"; + } + + return preg_replace( + sprintf($options['regex'], $phrase), + $options['format'], + $text, + $options['limit'] + ); + } + + /** + * Truncates text starting from the end. + * + * Cuts a string to the length of $length and replaces the first characters + * with the ellipsis if the text is longer than length. + * + * ### Options: + * + * - `ellipsis` Will be used as beginning and prepended to the trimmed string + * - `exact` If false, $text will not be cut mid-word + * + * @param string $text String to truncate. + * @param int $length Length of returned string, including ellipsis. + * @param array $options An array of options. + * @return string Trimmed string. + */ + public static function tail(string $text, int $length = 100, array $options = []): string + { + $default = [ + 'ellipsis' => '...', 'exact' => true, + ]; + $options += $default; + $ellipsis = $options['ellipsis']; + + if (mb_strlen($text) <= $length) { + return $text; + } + + $truncate = mb_substr($text, mb_strlen($text) - $length + mb_strlen($ellipsis)); + if (!$options['exact']) { + $spacepos = mb_strpos($truncate, ' '); + $truncate = $spacepos === false ? '' : trim(mb_substr($truncate, $spacepos)); + } + + return $ellipsis . $truncate; + } + + /** + * Truncates text. + * + * Cuts a string to the length of $length and replaces the last characters + * with the ellipsis if the text is longer than length. + * + * ### Options: + * + * - `ellipsis` Will be used as ending and appended to the trimmed string + * - `exact` If false, $text will not be cut mid-word + * - `html` If true, HTML tags would be handled correctly + * - `trimWidth` If true, $text will be truncated with the width + * + * @param string $text String to truncate. + * @param int $length Length of returned string, including ellipsis. + * @param array $options An array of HTML attributes and options. + * @return string Trimmed string. + * @link https://book.cakephp.org/4/en/core-libraries/text.html#truncating-text + */ + public static function truncate(string $text, int $length = 100, array $options = []): string + { + $default = [ + 'ellipsis' => '...', 'exact' => true, 'html' => false, 'trimWidth' => false, + ]; + if (!empty($options['html']) && strtolower((string)mb_internal_encoding()) === 'utf-8') { + $default['ellipsis'] = "\xe2\x80\xa6"; + } + $options += $default; + + $prefix = ''; + $suffix = $options['ellipsis']; + + if ($options['html']) { + $ellipsisLength = self::_strlen(strip_tags($options['ellipsis']), $options); + + $truncateLength = 0; + $totalLength = 0; + $openTags = []; + $truncate = ''; + + preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER); + foreach ($tags as $tag) { + $contentLength = 0; + if (!in_array($tag[2], static::$_defaultHtmlNoCount, true)) { + $contentLength = self::_strlen($tag[3], $options); + } + + if ($truncate === '') { + if ( + !preg_match( + '/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/i', + $tag[2] + ) + ) { + if (preg_match('/<[\w]+[^>]*>/', $tag[0])) { + array_unshift($openTags, $tag[2]); + } elseif (preg_match('/<\/([\w]+)[^>]*>/', $tag[0], $closeTag)) { + $pos = array_search($closeTag[1], $openTags, true); + if ($pos !== false) { + array_splice($openTags, $pos, 1); + } + } + } + + $prefix .= $tag[1]; + + if ($totalLength + $contentLength + $ellipsisLength > $length) { + $truncate = $tag[3]; + $truncateLength = $length - $totalLength; + } else { + $prefix .= $tag[3]; + } + } + + $totalLength += $contentLength; + if ($totalLength > $length) { + break; + } + } + + if ($totalLength <= $length) { + return $text; + } + + $text = $truncate; + $length = $truncateLength; + + foreach ($openTags as $tag) { + $suffix .= ''; + } + } else { + if (self::_strlen($text, $options) <= $length) { + return $text; + } + $ellipsisLength = self::_strlen($options['ellipsis'], $options); + } + + $result = self::_substr($text, 0, $length - $ellipsisLength, $options); + + if (!$options['exact']) { + if (self::_substr($text, $length - $ellipsisLength, 1, $options) !== ' ') { + $result = self::_removeLastWord($result); + } + + // If result is empty, then we don't need to count ellipsis in the cut. + if ($result === '') { + $result = self::_substr($text, 0, $length, $options); + } + } + + return $prefix . $result . $suffix; + } + + /** + * Truncate text with specified width. + * + * @param string $text String to truncate. + * @param int $length Length of returned string, including ellipsis. + * @param array $options An array of HTML attributes and options. + * @return string Trimmed string. + * @see \Cake\Utility\Text::truncate() + */ + public static function truncateByWidth(string $text, int $length = 100, array $options = []): string + { + return static::truncate($text, $length, ['trimWidth' => true] + $options); + } + + /** + * Get string length. + * + * ### Options: + * + * - `html` If true, HTML entities will be handled as decoded characters. + * - `trimWidth` If true, the width will return. + * + * @param string $text The string being checked for length + * @param array $options An array of options. + * @return int + */ + protected static function _strlen(string $text, array $options): int + { + if (empty($options['trimWidth'])) { + $strlen = 'mb_strlen'; + } else { + $strlen = 'mb_strwidth'; + } + + if (empty($options['html'])) { + return $strlen($text); + } + + $pattern = '/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i'; + $replace = preg_replace_callback( + $pattern, + function ($match) use ($strlen) { + $utf8 = html_entity_decode($match[0], ENT_HTML5 | ENT_QUOTES, 'UTF-8'); + + return str_repeat(' ', $strlen($utf8, 'UTF-8')); + }, + $text + ); + + return $strlen($replace); + } + + /** + * Return part of a string. + * + * ### Options: + * + * - `html` If true, HTML entities will be handled as decoded characters. + * - `trimWidth` If true, will be truncated with specified width. + * + * @param string $text The input string. + * @param int $start The position to begin extracting. + * @param int|null $length The desired length. + * @param array $options An array of options. + * @return string + */ + protected static function _substr(string $text, int $start, ?int $length, array $options): string + { + if (empty($options['trimWidth'])) { + $substr = 'mb_substr'; + } else { + $substr = 'mb_strimwidth'; + } + + $maxPosition = self::_strlen($text, ['trimWidth' => false] + $options); + if ($start < 0) { + $start += $maxPosition; + if ($start < 0) { + $start = 0; + } + } + if ($start >= $maxPosition) { + return ''; + } + + if ($length === null) { + $length = self::_strlen($text, $options); + } + + if ($length < 0) { + $text = self::_substr($text, $start, null, $options); + $start = 0; + $length += self::_strlen($text, $options); + } + + if ($length <= 0) { + return ''; + } + + if (empty($options['html'])) { + return (string)$substr($text, $start, $length); + } + + $totalOffset = 0; + $totalLength = 0; + $result = ''; + + $pattern = '/(&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};)/i'; + $parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + $offset = 0; + + if ($totalOffset < $start) { + $len = self::_strlen($part, ['trimWidth' => false] + $options); + if ($totalOffset + $len <= $start) { + $totalOffset += $len; + continue; + } + + $offset = $start - $totalOffset; + $totalOffset = $start; + } + + $len = self::_strlen($part, $options); + if ($offset !== 0 || $totalLength + $len > $length) { + if ( + strpos($part, '&') === 0 + && preg_match($pattern, $part) + && $part !== html_entity_decode($part, ENT_HTML5 | ENT_QUOTES, 'UTF-8') + ) { + // Entities cannot be passed substr. + continue; + } + + $part = $substr($part, $offset, $length - $totalLength); + $len = self::_strlen($part, $options); + } + + $result .= $part; + $totalLength += $len; + if ($totalLength >= $length) { + break; + } + } + + return $result; + } + + /** + * Removes the last word from the input text. + * + * @param string $text The input text + * @return string + */ + protected static function _removeLastWord(string $text): string + { + $spacepos = mb_strrpos($text, ' '); + + if ($spacepos !== false) { + $lastWord = mb_substr($text, $spacepos); + + // Some languages are written without word separation. + // We recognize a string as a word if it doesn't contain any full-width characters. + if (mb_strwidth($lastWord) === mb_strlen($lastWord)) { + $text = mb_substr($text, 0, $spacepos); + } + + return $text; + } + + return ''; + } + + /** + * Extracts an excerpt from the text surrounding the phrase with a number of characters on each side + * determined by radius. + * + * @param string $text String to search the phrase in + * @param string $phrase Phrase that will be searched for + * @param int $radius The amount of characters that will be returned on each side of the founded phrase + * @param string $ellipsis Ending that will be appended + * @return string Modified string + * @link https://book.cakephp.org/4/en/core-libraries/text.html#extracting-an-excerpt + */ + public static function excerpt(string $text, string $phrase, int $radius = 100, string $ellipsis = '...'): string + { + if (empty($text) || empty($phrase)) { + return static::truncate($text, $radius * 2, ['ellipsis' => $ellipsis]); + } + + $append = $prepend = $ellipsis; + + $phraseLen = mb_strlen($phrase); + $textLen = mb_strlen($text); + + $pos = mb_stripos($text, $phrase); + if ($pos === false) { + return mb_substr($text, 0, $radius) . $ellipsis; + } + + $startPos = $pos - $radius; + if ($startPos <= 0) { + $startPos = 0; + $prepend = ''; + } + + $endPos = $pos + $phraseLen + $radius; + if ($endPos >= $textLen) { + $endPos = $textLen; + $append = ''; + } + + $excerpt = mb_substr($text, $startPos, $endPos - $startPos); + $excerpt = $prepend . $excerpt . $append; + + return $excerpt; + } + + /** + * Creates a comma separated list where the last two items are joined with 'and', forming natural language. + * + * @param array $list The list to be joined. + * @param string|null $and The word used to join the last and second last items together with. Defaults to 'and'. + * @param string $separator The separator used to join all the other items together. Defaults to ', '. + * @return string The glued together string. + * @link https://book.cakephp.org/4/en/core-libraries/text.html#converting-an-array-to-sentence-form + */ + public static function toList(array $list, ?string $and = null, string $separator = ', '): string + { + if ($and === null) { + $and = __d('cake', 'and'); + } + if (count($list) > 1) { + return implode($separator, array_slice($list, 0, -1)) . ' ' . $and . ' ' . array_pop($list); + } + + return (string)array_pop($list); + } + + /** + * Check if the string contain multibyte characters + * + * @param string $string value to test + * @return bool + */ + public static function isMultibyte(string $string): bool + { + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $value = ord($string[$i]); + if ($value > 128) { + return true; + } + } + + return false; + } + + /** + * Converts a multibyte character string + * to the decimal value of the character + * + * @param string $string String to convert. + * @return array + */ + public static function utf8(string $string): array + { + $map = []; + + $values = []; + $find = 1; + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $value = ord($string[$i]); + + if ($value < 128) { + $map[] = $value; + } else { + if (empty($values)) { + $find = $value < 224 ? 2 : 3; + } + $values[] = $value; + + if (count($values) === $find) { + if ($find === 3) { + $map[] = (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64); + } else { + $map[] = (($values[0] % 32) * 64) + ($values[1] % 64); + } + $values = []; + $find = 1; + } + } + } + + return $map; + } + + /** + * Converts the decimal value of a multibyte character string + * to a string + * + * @param array $array Array + * @return string + */ + public static function ascii(array $array): string + { + $ascii = ''; + + foreach ($array as $utf8) { + if ($utf8 < 128) { + $ascii .= chr($utf8); + } elseif ($utf8 < 2048) { + $ascii .= chr(192 + (($utf8 - ($utf8 % 64)) / 64)); + $ascii .= chr(128 + ($utf8 % 64)); + } else { + $ascii .= chr(224 + (($utf8 - ($utf8 % 4096)) / 4096)); + $ascii .= chr(128 + ((($utf8 % 4096) - ($utf8 % 64)) / 64)); + $ascii .= chr(128 + ($utf8 % 64)); + } + } + + return $ascii; + } + + /** + * Converts filesize from human readable string to bytes + * + * @param string $size Size in human readable string like '5MB', '5M', '500B', '50kb' etc. + * @param mixed $default Value to be returned when invalid size was used, for example 'Unknown type' + * @return mixed Number of bytes as integer on success, `$default` on failure if not false + * @throws \InvalidArgumentException On invalid Unit type. + * @link https://book.cakephp.org/4/en/core-libraries/text.html#Cake\Utility\Text::parseFileSize + */ + public static function parseFileSize(string $size, $default = false) + { + if (ctype_digit($size)) { + return (int)$size; + } + $size = strtoupper($size); + + $l = -2; + $i = array_search(substr($size, -2), ['KB', 'MB', 'GB', 'TB', 'PB'], true); + if ($i === false) { + $l = -1; + $i = array_search(substr($size, -1), ['K', 'M', 'G', 'T', 'P'], true); + } + if ($i !== false) { + $size = (float)substr($size, 0, $l); + + return (int)($size * pow(1024, $i + 1)); + } + + if (substr($size, -1) === 'B' && ctype_digit(substr($size, 0, -1))) { + $size = substr($size, 0, -1); + + return (int)$size; + } + + if ($default !== false) { + return $default; + } + throw new InvalidArgumentException('No unit type.'); + } + + /** + * Get the default transliterator. + * + * @return \Transliterator|null Either a Transliterator instance, or `null` + * in case no transliterator has been set yet. + */ + public static function getTransliterator(): ?Transliterator + { + return static::$_defaultTransliterator; + } + + /** + * Set the default transliterator. + * + * @param \Transliterator $transliterator A `Transliterator` instance. + * @return void + */ + public static function setTransliterator(Transliterator $transliterator): void + { + static::$_defaultTransliterator = $transliterator; + } + + /** + * Get default transliterator identifier string. + * + * @return string Transliterator identifier. + */ + public static function getTransliteratorId(): string + { + return static::$_defaultTransliteratorId; + } + + /** + * Set default transliterator identifier string. + * + * @param string $transliteratorId Transliterator identifier. + * @return void + */ + public static function setTransliteratorId(string $transliteratorId): void + { + $transliterator = transliterator_create($transliteratorId); + if ($transliterator === null) { + throw new CakeException('Unable to create transliterator for id: ' . $transliteratorId); + } + + static::setTransliterator($transliterator); + static::$_defaultTransliteratorId = $transliteratorId; + } + + /** + * Transliterate string. + * + * @param string $string String to transliterate. + * @param \Transliterator|string|null $transliterator Either a Transliterator + * instance, or a transliterator identifier string. If `null`, the default + * transliterator (identifier) set via `setTransliteratorId()` or + * `setTransliterator()` will be used. + * @return string + * @see https://secure.php.net/manual/en/transliterator.transliterate.php + */ + public static function transliterate(string $string, $transliterator = null): string + { + if (empty($transliterator)) { + $transliterator = static::$_defaultTransliterator ?: static::$_defaultTransliteratorId; + } + + $return = transliterator_transliterate($transliterator, $string); + if ($return === false) { + throw new CakeException(sprintf('Unable to transliterate string: %s', $string)); + } + + return $return; + } + + /** + * Returns a string with all spaces converted to dashes (by default), + * characters transliterated to ASCII characters, and non word characters removed. + * + * ### Options: + * + * - `replacement`: Replacement string. Default '-'. + * - `transliteratorId`: A valid transliterator id string. + * If `null` (default) the transliterator (identifier) set via + * `setTransliteratorId()` or `setTransliterator()` will be used. + * If `false` no transliteration will be done, only non words will be removed. + * - `preserve`: Specific non-word character to preserve. Default `null`. + * For e.g. this option can be set to '.' to generate clean file names. + * + * @param string $string the string you want to slug + * @param array|string $options If string it will be use as replacement character + * or an array of options. + * @return string + * @see setTransliterator() + * @see setTransliteratorId() + */ + public static function slug(string $string, $options = []): string + { + if (is_string($options)) { + $options = ['replacement' => $options]; + } + $options += [ + 'replacement' => '-', + 'transliteratorId' => null, + 'preserve' => null, + ]; + + if ($options['transliteratorId'] !== false) { + $string = static::transliterate($string, $options['transliteratorId']); + } + + $regex = '^\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}'; + if ($options['preserve']) { + $regex .= preg_quote($options['preserve'], '/'); + } + $quotedReplacement = preg_quote((string)$options['replacement'], '/'); + $map = [ + '/[' . $regex . ']/mu' => $options['replacement'], + sprintf('/^[%s]+|[%s]+$/', $quotedReplacement, $quotedReplacement) => '', + ]; + if (is_string($options['replacement']) && $options['replacement'] !== '') { + $map[sprintf('/[%s]+/mu', $quotedReplacement)] = $options['replacement']; + } + $string = preg_replace(array_keys($map), $map, $string); + + return $string; + } +} diff --git a/vendor/cakephp/utility/Xml.php b/vendor/cakephp/utility/Xml.php new file mode 100644 index 0000000..d02f591 --- /dev/null +++ b/vendor/cakephp/utility/Xml.php @@ -0,0 +1,503 @@ +text'); + * ``` + * + * Building XML from string (output DOMDocument): + * + * ``` + * $xml = Xml::build('text', ['return' => 'domdocument']); + * ``` + * + * Building XML from a file path: + * + * ``` + * $xml = Xml::build('/path/to/an/xml/file.xml'); + * ``` + * + * Building XML from a remote URL: + * + * ``` + * use Cake\Http\Client; + * + * $http = new Client(); + * $response = $http->get('http://example.com/example.xml'); + * $xml = Xml::build($response->body()); + * ``` + * + * Building from an array: + * + * ``` + * $value = [ + * 'tags' => [ + * 'tag' => [ + * [ + * 'id' => '1', + * 'name' => 'defect' + * ], + * [ + * 'id' => '2', + * 'name' => 'enhancement' + * ] + * ] + * ] + * ]; + * $xml = Xml::build($value); + * ``` + * + * When building XML from an array ensure that there is only one top level element. + * + * ### Options + * + * - `return` Can be 'simplexml' to return object of SimpleXMLElement or 'domdocument' to return DOMDocument. + * - `loadEntities` Defaults to false. Set to true to enable loading of ` $options The options to use + * @return \SimpleXMLElement|\DOMDocument SimpleXMLElement or DOMDocument + * @throws \Cake\Utility\Exception\XmlException + */ + public static function build($input, array $options = []) + { + $defaults = [ + 'return' => 'simplexml', + 'loadEntities' => false, + 'readFile' => false, + 'parseHuge' => false, + ]; + $options += $defaults; + + if (is_array($input) || is_object($input)) { + return static::fromArray($input, $options); + } + + if ($options['readFile'] && file_exists($input)) { + return static::_loadXml(file_get_contents($input), $options); + } + + if (!is_string($input)) { + $type = gettype($input); + throw new XmlException("Invalid input. {$type} cannot be parsed as XML."); + } + + if (strpos($input, '<') !== false) { + return static::_loadXml($input, $options); + } + + throw new XmlException('XML cannot be read.'); + } + + /** + * Parse the input data and create either a SimpleXmlElement object or a DOMDocument. + * + * @param string $input The input to load. + * @param array $options The options to use. See Xml::build() + * @return \SimpleXMLElement|\DOMDocument + * @throws \Cake\Utility\Exception\XmlException + */ + protected static function _loadXml(string $input, array $options) + { + return static::load( + $input, + $options, + function ($input, $options, $flags) { + if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') { + $flags |= LIBXML_NOCDATA; + $xml = new SimpleXMLElement($input, $flags); + } else { + $xml = new DOMDocument(); + $xml->loadXML($input, $flags); + } + + return $xml; + } + ); + } + + /** + * Parse the input html string and create either a SimpleXmlElement object or a DOMDocument. + * + * @param string $input The input html string to load. + * @param array $options The options to use. See Xml::build() + * @return \SimpleXMLElement|\DOMDocument + * @throws \Cake\Utility\Exception\XmlException + */ + public static function loadHtml(string $input, array $options = []) + { + $defaults = [ + 'return' => 'simplexml', + 'loadEntities' => false, + ]; + $options += $defaults; + + return static::load( + $input, + $options, + function ($input, $options, $flags) { + $xml = new DOMDocument(); + $xml->loadHTML($input, $flags); + + if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') { + $xml = simplexml_import_dom($xml); + } + + return $xml; + } + ); + } + + /** + * Parse the input data and create either a SimpleXmlElement object or a DOMDocument. + * + * @param string $input The input to load. + * @param array $options The options to use. See Xml::build() + * @param \Closure $callable Closure that should return SimpleXMLElement or DOMDocument instance. + * @return \SimpleXMLElement|\DOMDocument + * @throws \Cake\Utility\Exception\XmlException + */ + protected static function load(string $input, array $options, Closure $callable) + { + $flags = 0; + if (!empty($options['parseHuge'])) { + $flags |= LIBXML_PARSEHUGE; + } + + $internalErrors = libxml_use_internal_errors(true); + if (LIBXML_VERSION < 20900 && !$options['loadEntities']) { + $previousDisabledEntityLoader = libxml_disable_entity_loader(true); + } elseif ($options['loadEntities']) { + $flags |= LIBXML_NOENT; + } + + try { + return $callable($input, $options, $flags); + } catch (Exception $e) { + throw new XmlException('Xml cannot be read. ' . $e->getMessage(), null, $e); + } finally { + if (isset($previousDisabledEntityLoader)) { + libxml_disable_entity_loader($previousDisabledEntityLoader); + } + libxml_use_internal_errors($internalErrors); + } + } + + /** + * Transform an array into a SimpleXMLElement + * + * ### Options + * + * - `format` If create children ('tags') or attributes ('attributes'). + * - `pretty` Returns formatted Xml when set to `true`. Defaults to `false` + * - `version` Version of XML document. Default is 1.0. + * - `encoding` Encoding of XML document. If null remove from XML header. + * Defaults to the application's encoding + * - `return` If return object of SimpleXMLElement ('simplexml') + * or DOMDocument ('domdocument'). Default is SimpleXMLElement. + * + * Using the following data: + * + * ``` + * $value = [ + * 'root' => [ + * 'tag' => [ + * 'id' => 1, + * 'value' => 'defect', + * '@' => 'description' + * ] + * ] + * ]; + * ``` + * + * Calling `Xml::fromArray($value, 'tags');` Will generate: + * + * `1defectdescription` + * + * And calling `Xml::fromArray($value, 'attributes');` Will generate: + * + * `description` + * + * @param object|array $input Array with data or a collection instance. + * @param array $options The options to use. + * @return \SimpleXMLElement|\DOMDocument SimpleXMLElement or DOMDocument + * @throws \Cake\Utility\Exception\XmlException + */ + public static function fromArray($input, array $options = []) + { + if (is_object($input) && method_exists($input, 'toArray') && is_callable([$input, 'toArray'])) { + $input = $input->toArray(); + } + if (!is_array($input) || count($input) !== 1) { + throw new XmlException('Invalid input.'); + } + $key = key($input); + if (is_int($key)) { + throw new XmlException('The key of input must be alphanumeric'); + } + + $defaults = [ + 'format' => 'tags', + 'version' => '1.0', + 'encoding' => mb_internal_encoding(), + 'return' => 'simplexml', + 'pretty' => false, + ]; + $options += $defaults; + + $dom = new DOMDocument($options['version'], $options['encoding']); + if ($options['pretty']) { + $dom->formatOutput = true; + } + self::_fromArray($dom, $dom, $input, $options['format']); + + $options['return'] = strtolower($options['return']); + if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') { + return new SimpleXMLElement($dom->saveXML()); + } + + return $dom; + } + + /** + * Recursive method to create children from array + * + * @param \DOMDocument $dom Handler to DOMDocument + * @param \DOMDocument|\DOMElement $node Handler to DOMElement (child) + * @param array $data Array of data to append to the $node. + * @param string $format Either 'attributes' or 'tags'. This determines where nested keys go. + * @return void + * @throws \Cake\Utility\Exception\XmlException + */ + protected static function _fromArray(DOMDocument $dom, $node, &$data, $format): void + { + if (empty($data) || !is_array($data)) { + return; + } + foreach ($data as $key => $value) { + if (is_string($key)) { + if (is_object($value) && method_exists($value, 'toArray') && is_callable([$value, 'toArray'])) { + $value = $value->toArray(); + } + + if (!is_array($value)) { + if (is_bool($value)) { + $value = (int)$value; + } elseif ($value === null) { + $value = ''; + } + $isNamespace = strpos($key, 'xmlns:'); + if ($isNamespace !== false) { + /** @psalm-suppress PossiblyUndefinedMethod */ + $node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, (string)$value); + continue; + } + if ($key[0] !== '@' && $format === 'tags') { + if (!is_numeric($value)) { + // Escape special characters + // https://www.w3.org/TR/REC-xml/#syntax + // https://bugs.php.net/bug.php?id=36795 + $child = $dom->createElement($key, ''); + $child->appendChild(new DOMText((string)$value)); + } else { + $child = $dom->createElement($key, (string)$value); + } + $node->appendChild($child); + } else { + if ($key[0] === '@') { + $key = substr($key, 1); + } + $attribute = $dom->createAttribute($key); + $attribute->appendChild($dom->createTextNode((string)$value)); + $node->appendChild($attribute); + } + } else { + if ($key[0] === '@') { + throw new XmlException('Invalid array'); + } + if (is_numeric(implode('', array_keys($value)))) { +// List + foreach ($value as $item) { + $itemData = compact('dom', 'node', 'key', 'format'); + $itemData['value'] = $item; + static::_createChild($itemData); + } + } else { +// Struct + static::_createChild(compact('dom', 'node', 'key', 'value', 'format')); + } + } + } else { + throw new XmlException('Invalid array'); + } + } + } + + /** + * Helper to _fromArray(). It will create children of arrays + * + * @param array $data Array with information to create children + * @return void + */ + protected static function _createChild(array $data): void + { + $data += [ + 'dom' => null, + 'node' => null, + 'key' => null, + 'value' => null, + 'format' => null, + ]; + + $value = $data['value']; + $dom = $data['dom']; + $key = $data['key']; + $format = $data['format']; + $node = $data['node']; + + $childNS = $childValue = null; + if (is_object($value) && method_exists($value, 'toArray') && is_callable([$value, 'toArray'])) { + $value = $value->toArray(); + } + if (is_array($value)) { + if (isset($value['@'])) { + $childValue = (string)$value['@']; + unset($value['@']); + } + if (isset($value['xmlns:'])) { + $childNS = $value['xmlns:']; + unset($value['xmlns:']); + } + } elseif (!empty($value) || $value === 0 || $value === '0') { + $childValue = (string)$value; + } + + $child = $dom->createElement($key); + if ($childValue !== null) { + $child->appendChild($dom->createTextNode($childValue)); + } + if ($childNS) { + $child->setAttribute('xmlns', $childNS); + } + + static::_fromArray($dom, $child, $value, $format); + $node->appendChild($child); + } + + /** + * Returns this XML structure as an array. + * + * @param \SimpleXMLElement|\DOMDocument|\DOMNode $obj SimpleXMLElement, DOMDocument or DOMNode instance + * @return array Array representation of the XML structure. + * @throws \Cake\Utility\Exception\XmlException + */ + public static function toArray($obj): array + { + if ($obj instanceof DOMNode) { + $obj = simplexml_import_dom($obj); + } + if (!($obj instanceof SimpleXMLElement)) { + throw new XmlException('The input is not instance of SimpleXMLElement, DOMDocument or DOMNode.'); + } + $result = []; + $namespaces = array_merge(['' => ''], $obj->getNamespaces(true)); + static::_toArray($obj, $result, '', array_keys($namespaces)); + + return $result; + } + + /** + * Recursive method to toArray + * + * @param \SimpleXMLElement $xml SimpleXMLElement object + * @param array $parentData Parent array with data + * @param string $ns Namespace of current child + * @param array $namespaces List of namespaces in XML + * @return void + */ + protected static function _toArray(SimpleXMLElement $xml, array &$parentData, string $ns, array $namespaces): void + { + $data = []; + + foreach ($namespaces as $namespace) { + /** + * @psalm-suppress PossiblyNullIterator + * @var string $key + */ + foreach ($xml->attributes($namespace, true) as $key => $value) { + if (!empty($namespace)) { + $key = $namespace . ':' . $key; + } + $data['@' . $key] = (string)$value; + } + + foreach ($xml->children($namespace, true) as $child) { + /** @psalm-suppress PossiblyNullArgument */ + static::_toArray($child, $data, $namespace, $namespaces); + } + } + + $asString = trim((string)$xml); + if (empty($data)) { + $data = $asString; + } elseif ($asString !== '') { + $data['@'] = $asString; + } + + if (!empty($ns)) { + $ns .= ':'; + } + $name = $ns . $xml->getName(); + if (isset($parentData[$name])) { + if (!is_array($parentData[$name]) || !isset($parentData[$name][0])) { + $parentData[$name] = [$parentData[$name]]; + } + $parentData[$name][] = $data; + } else { + $parentData[$name] = $data; + } + } +} diff --git a/vendor/cakephp/utility/bootstrap.php b/vendor/cakephp/utility/bootstrap.php new file mode 100644 index 0000000..d335b75 --- /dev/null +++ b/vendor/cakephp/utility/bootstrap.php @@ -0,0 +1,21 @@ +=7.2.0", + "cakephp/core": "^4.0" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()", + "lib-ICU": "To use Text::transliterate() or Text::slug()" + }, + "autoload": { + "psr-4": { + "Cake\\Utility\\": "." + }, + "files": [ + "bootstrap.php" + ] + } +} diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php new file mode 100644 index 0000000..afef3fa --- /dev/null +++ b/vendor/composer/ClassLoader.php @@ -0,0 +1,572 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var ?string */ + private $vendorDir; + + // PSR-4 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array[] + * @psalm-var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixesPsr0 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var string[] + * @psalm-var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var bool[] + * @psalm-var array + */ + private $missingClasses = array(); + + /** @var ?string */ + private $apcuPrefix; + + /** + * @var self[] + */ + private static $registeredLoaders = array(); + + /** + * @param ?string $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + + /** + * @return string[] + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array[] + * @psalm-return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return string[] Array of classname => path + * @psalm-return array + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + * @private + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..d50e0c9 --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,350 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints($constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = require __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + $installed[] = self::$installed; + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..fc2f83d --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,17 @@ + $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'JsonException' => $vendorDir . '/symfony/polyfill-php73/Resources/stubs/JsonException.php', + 'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', + 'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php', + 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', +); diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php new file mode 100644 index 0000000..64f7e84 --- /dev/null +++ b/vendor/composer/autoload_files.php @@ -0,0 +1,21 @@ + $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '72142d7b40a3a0b14e91825290b5ad82' => $vendorDir . '/cakephp/core/functions.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', + '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', + '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', + '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', + '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php', + '253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php', + '948ad5488880985ff1c06721a4e447fe' => $vendorDir . '/cakephp/utility/bootstrap.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..c3cd022 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,10 @@ + array($vendorDir . '/pimple/pimple/src'), +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..6f0bfe4 --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,32 @@ + array($vendorDir . '/symfony/polyfill-php81'), + 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), + 'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'), + 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'), + 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), + 'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'), + 'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'), + 'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'), + 'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'), + 'Symfony\\Component\\Config\\' => array($vendorDir . '/symfony/config'), + 'Slim\\' => array($vendorDir . '/slim/slim/Slim'), + 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), + 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), + 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), + 'Phinx\\' => array($vendorDir . '/robmorgan/phinx/src/Phinx'), + 'FastRoute\\' => array($vendorDir . '/nikic/fast-route/src'), + 'Cake\\Utility\\' => array($vendorDir . '/cakephp/utility'), + 'Cake\\Datasource\\' => array($vendorDir . '/cakephp/datasource'), + 'Cake\\Database\\' => array($vendorDir . '/cakephp/database'), + 'Cake\\Core\\' => array($vendorDir . '/cakephp/core'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..3f93844 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,80 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit5e6edd765fd6f7c1dba51eaca02c61f0::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInit5e6edd765fd6f7c1dba51eaca02c61f0::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequire5e6edd765fd6f7c1dba51eaca02c61f0($fileIdentifier, $file); + } + + return $loader; + } +} + +/** + * @param string $fileIdentifier + * @param string $file + * @return void + */ +function composerRequire5e6edd765fd6f7c1dba51eaca02c61f0($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..1c67e30 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,188 @@ + __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '72142d7b40a3a0b14e91825290b5ad82' => __DIR__ . '/..' . '/cakephp/core/functions.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', + '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', + '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', + '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php', + '253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php', + '948ad5488880985ff1c06721a4e447fe' => __DIR__ . '/..' . '/cakephp/utility/bootstrap.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'S' => + array ( + 'Symfony\\Polyfill\\Php81\\' => 23, + 'Symfony\\Polyfill\\Php80\\' => 23, + 'Symfony\\Polyfill\\Php73\\' => 23, + 'Symfony\\Polyfill\\Mbstring\\' => 26, + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33, + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => 31, + 'Symfony\\Polyfill\\Ctype\\' => 23, + 'Symfony\\Contracts\\Service\\' => 26, + 'Symfony\\Component\\String\\' => 25, + 'Symfony\\Component\\Filesystem\\' => 29, + 'Symfony\\Component\\Console\\' => 26, + 'Symfony\\Component\\Config\\' => 25, + 'Slim\\' => 5, + ), + 'P' => + array ( + 'Psr\\SimpleCache\\' => 16, + 'Psr\\Log\\' => 8, + 'Psr\\Http\\Message\\' => 17, + 'Psr\\Container\\' => 14, + 'Phinx\\' => 6, + ), + 'F' => + array ( + 'FastRoute\\' => 10, + ), + 'C' => + array ( + 'Cake\\Utility\\' => 13, + 'Cake\\Datasource\\' => 16, + 'Cake\\Database\\' => 14, + 'Cake\\Core\\' => 10, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Symfony\\Polyfill\\Php81\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php81', + ), + 'Symfony\\Polyfill\\Php80\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', + ), + 'Symfony\\Polyfill\\Php73\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php73', + ), + 'Symfony\\Polyfill\\Mbstring\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', + ), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer', + ), + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme', + ), + 'Symfony\\Polyfill\\Ctype\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', + ), + 'Symfony\\Contracts\\Service\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/service-contracts', + ), + 'Symfony\\Component\\String\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/string', + ), + 'Symfony\\Component\\Filesystem\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/filesystem', + ), + 'Symfony\\Component\\Console\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/console', + ), + 'Symfony\\Component\\Config\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/config', + ), + 'Slim\\' => + array ( + 0 => __DIR__ . '/..' . '/slim/slim/Slim', + ), + 'Psr\\SimpleCache\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/simple-cache/src', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', + ), + 'Psr\\Http\\Message\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-message/src', + ), + 'Psr\\Container\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/container/src', + ), + 'Phinx\\' => + array ( + 0 => __DIR__ . '/..' . '/robmorgan/phinx/src/Phinx', + ), + 'FastRoute\\' => + array ( + 0 => __DIR__ . '/..' . '/nikic/fast-route/src', + ), + 'Cake\\Utility\\' => + array ( + 0 => __DIR__ . '/..' . '/cakephp/utility', + ), + 'Cake\\Datasource\\' => + array ( + 0 => __DIR__ . '/..' . '/cakephp/datasource', + ), + 'Cake\\Database\\' => + array ( + 0 => __DIR__ . '/..' . '/cakephp/database', + ), + 'Cake\\Core\\' => + array ( + 0 => __DIR__ . '/..' . '/cakephp/core', + ), + ); + + public static $prefixesPsr0 = array ( + 'P' => + array ( + 'Pimple' => + array ( + 0 => __DIR__ . '/..' . '/pimple/pimple/src', + ), + ), + ); + + public static $classMap = array ( + 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'JsonException' => __DIR__ . '/..' . '/symfony/polyfill-php73/Resources/stubs/JsonException.php', + 'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', + 'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php', + 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit5e6edd765fd6f7c1dba51eaca02c61f0::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit5e6edd765fd6f7c1dba51eaca02c61f0::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInit5e6edd765fd6f7c1dba51eaca02c61f0::$prefixesPsr0; + $loader->classMap = ComposerStaticInit5e6edd765fd6f7c1dba51eaca02c61f0::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..b23b89b --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,1845 @@ +{ + "packages": [ + { + "name": "cakephp/core", + "version": "4.3.3", + "version_normalized": "4.3.3.0", + "source": { + "type": "git", + "url": "https://github.com/cakephp/core.git", + "reference": "d80bcd1d94e1e90741ad7dbae882db8711239d07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/core/zipball/d80bcd1d94e1e90741ad7dbae882db8711239d07", + "reference": "d80bcd1d94e1e90741ad7dbae882db8711239d07", + "shasum": "" + }, + "require": { + "cakephp/utility": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "cakephp/cache": "To use Configure::store() and restore().", + "cakephp/event": "To use PluginApplicationInterface or plugin applications.", + "league/container": "To use Container and ServiceProvider classes" + }, + "time": "2021-12-17T14:28:04+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Cake\\Core\\": "." + }, + "files": [ + "functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/core/graphs/contributors" + } + ], + "description": "CakePHP Framework Core classes", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "core", + "framework" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/core" + }, + "install-path": "../cakephp/core" + }, + { + "name": "cakephp/database", + "version": "4.3.3", + "version_normalized": "4.3.3.0", + "source": { + "type": "git", + "url": "https://github.com/cakephp/database.git", + "reference": "02197e9b1517ecdea546b8e7780682b34c967d3f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/database/zipball/02197e9b1517ecdea546b8e7780682b34c967d3f", + "reference": "02197e9b1517ecdea546b8e7780682b34c967d3f", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "cakephp/datasource": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "cakephp/i18n": "If you are using locale-aware datetime formats or Chronos types." + }, + "time": "2021-12-17T14:28:04+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Cake\\Database\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/database/graphs/contributors" + } + ], + "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API", + "homepage": "https://cakephp.org", + "keywords": [ + "abstraction", + "cakephp", + "database", + "database abstraction", + "pdo" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/database" + }, + "install-path": "../cakephp/database" + }, + { + "name": "cakephp/datasource", + "version": "4.3.3", + "version_normalized": "4.3.3.0", + "source": { + "type": "git", + "url": "https://github.com/cakephp/datasource.git", + "reference": "efd2a22555d982c2ab129c5e9e053971ed606624" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/datasource/zipball/efd2a22555d982c2ab129c5e9e053971ed606624", + "reference": "efd2a22555d982c2ab129c5e9e053971ed606624", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "php": ">=7.2.0", + "psr/log": "^1.1", + "psr/simple-cache": "^1.0" + }, + "suggest": { + "cakephp/cache": "If you decide to use Query caching.", + "cakephp/collection": "If you decide to use ResultSetInterface.", + "cakephp/utility": "If you decide to use EntityTrait." + }, + "time": "2021-12-17T14:28:04+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Cake\\Datasource\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/datasource/graphs/contributors" + } + ], + "description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "connection management", + "datasource", + "entity", + "query" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/datasource" + }, + "install-path": "../cakephp/datasource" + }, + { + "name": "cakephp/utility", + "version": "4.3.3", + "version_normalized": "4.3.3.0", + "source": { + "type": "git", + "url": "https://github.com/cakephp/utility.git", + "reference": "00410267c41036e20a43fbf0cb2ddedbd893a8a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/utility/zipball/00410267c41036e20a43fbf0cb2ddedbd893a8a5", + "reference": "00410267c41036e20a43fbf0cb2ddedbd893a8a5", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()", + "lib-ICU": "To use Text::transliterate() or Text::slug()" + }, + "time": "2021-12-17T14:28:04+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Cake\\Utility\\": "." + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/utility/graphs/contributors" + } + ], + "description": "CakePHP Utility classes such as Inflector, String, Hash, and Security", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "hash", + "inflector", + "security", + "string", + "utility" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/utility" + }, + "install-path": "../cakephp/utility" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "version_normalized": "1.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "time": "2018-02-13T20:26:39+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "install-path": "../nikic/fast-route" + }, + { + "name": "pimple/pimple", + "version": "v3.5.0", + "version_normalized": "3.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4@dev" + }, + "time": "2021-10-28T11:13:42+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" + }, + "install-path": "../pimple/pimple" + }, + { + "name": "psr/container", + "version": "1.1.2", + "version_normalized": "1.1.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "time": "2021-11-05T16:50:12+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "install-path": "../psr/container" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2016-08-06T14:39:51+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "install-path": "../psr/http-message" + }, + { + "name": "psr/log", + "version": "1.1.4", + "version_normalized": "1.1.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2021-05-03T11:20:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "install-path": "../psr/log" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2017-10-23T01:57:42+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "install-path": "../psr/simple-cache" + }, + { + "name": "robmorgan/phinx", + "version": "0.12.9", + "version_normalized": "0.12.9.0", + "source": { + "type": "git", + "url": "https://github.com/cakephp/phinx.git", + "reference": "5a0146a74c1bc195d1f5da86afa3b68badf7d90e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/phinx/zipball/5a0146a74c1bc195d1f5da86afa3b68badf7d90e", + "reference": "5a0146a74c1bc195d1f5da86afa3b68badf7d90e", + "shasum": "" + }, + "require": { + "cakephp/database": "^4.0", + "php": ">=7.2", + "psr/container": "^1.0 || ^2.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^4.0", + "ext-json": "*", + "ext-pdo": "*", + "phpunit/phpunit": "^8.5|^9.3", + "sebastian/comparator": ">=1.2.3", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "ext-json": "Install if using JSON configuration format", + "ext-pdo": "PDO extension is needed", + "symfony/yaml": "Install if using YAML configuration format" + }, + "time": "2021-10-12T10:49:25+00:00", + "bin": [ + "bin/phinx" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Phinx\\": "src/Phinx/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Morgan", + "email": "robbym@gmail.com", + "homepage": "https://robmorgan.id.au", + "role": "Lead Developer" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://shadowhand.me", + "role": "Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Developer" + }, + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/phinx/graphs/contributors", + "role": "Developer" + } + ], + "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.", + "homepage": "https://phinx.org", + "keywords": [ + "database", + "database migrations", + "db", + "migrations", + "phinx" + ], + "support": { + "issues": "https://github.com/cakephp/phinx/issues", + "source": "https://github.com/cakephp/phinx/tree/0.12.9" + }, + "install-path": "../robmorgan/phinx" + }, + { + "name": "slim/slim", + "version": "3.12.3", + "version_normalized": "3.12.3.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "1c9318a84ffb890900901136d620b4f03a59da38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/1c9318a84ffb890900901136d620b4f03a59da38", + "reference": "1c9318a84ffb890900901136d620b4f03a59da38", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "nikic/fast-route": "^1.0", + "php": ">=5.5.0", + "pimple/pimple": "^3.0", + "psr/container": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.5" + }, + "time": "2019-11-28T17:40:33+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "support": { + "issues": "https://github.com/slimphp/Slim/issues", + "source": "https://github.com/slimphp/Slim/tree/3.x" + }, + "install-path": "../slim/slim" + }, + { + "name": "symfony/config", + "version": "v5.4.2", + "version_normalized": "5.4.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "2e082dae50da563c639119b7b52347a2a3db4ba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/2e082dae50da563c639119b7b52347a2a3db4ba5", + "reference": "2e082dae50da563c639119b7b52347a2a3db4ba5", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" + }, + "conflict": { + "symfony/finder": "<4.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "time": "2021-12-15T11:06:13+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v5.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/config" + }, + { + "name": "symfony/console", + "version": "v5.4.2", + "version_normalized": "5.4.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a2c6b7ced2eb7799a35375fb9022519282b5405e", + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "time": "2021-12-20T16:11:12+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/console" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.0.0", + "version_normalized": "3.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/c726b64c1ccfe2896cb7df2e1331c357ad1c8ced", + "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced", + "shasum": "" + }, + "require": { + "php": ">=8.0.2" + }, + "time": "2021-11-01T23:48:49+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/deprecation-contracts" + }, + { + "name": "symfony/filesystem", + "version": "v6.0.0", + "version_normalized": "6.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "52b3c9cce673b014915445a432339f282e002ce6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/52b3c9cce673b014915445a432339f282e002ce6", + "reference": "52b3c9cce673b014915445a432339f282e002ce6", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "time": "2021-10-29T07:35:21+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/filesystem" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.24.0", + "version_normalized": "1.24.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "time": "2021-10-20T20:35:02+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-ctype" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.24.0", + "version_normalized": "1.24.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2021-11-23T21:10:46+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-grapheme" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.24.0", + "version_normalized": "1.24.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2021-02-19T12:13:01+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-normalizer" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.24.0", + "version_normalized": "1.24.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "time": "2021-11-30T18:21:41+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-mbstring" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.24.0", + "version_normalized": "1.24.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "time": "2021-06-05T21:20:04+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php73" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.24.0", + "version_normalized": "1.24.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", + "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "time": "2021-09-13T13:58:33+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php80" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.24.0", + "version_normalized": "1.24.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "time": "2021-09-13T13:58:11+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php81" + }, + { + "name": "symfony/service-contracts", + "version": "v2.4.1", + "version_normalized": "2.4.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d664541b99d6fb0247ec5ff32e87238582236204" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d664541b99d6fb0247ec5ff32e87238582236204", + "reference": "d664541b99d6fb0247ec5ff32e87238582236204", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "time": "2021-11-04T16:37:19+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/service-contracts" + }, + { + "name": "symfony/string", + "version": "v6.0.2", + "version_normalized": "6.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "bae261d0c3ac38a1f802b4dfed42094296100631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/bae261d0c3ac38a1f802b4dfed42094296100631", + "reference": "bae261d0c3ac38a1f802b4dfed42094296100631", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "time": "2021-12-16T22:13:01+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/string" + } + ], + "dev": true, + "dev-package-names": [ + "cakephp/core", + "cakephp/database", + "cakephp/datasource", + "cakephp/utility", + "psr/log", + "psr/simple-cache", + "robmorgan/phinx", + "symfony/config", + "symfony/console", + "symfony/deprecation-contracts", + "symfony/filesystem", + "symfony/polyfill-ctype", + "symfony/polyfill-intl-grapheme", + "symfony/polyfill-intl-normalizer", + "symfony/polyfill-mbstring", + "symfony/polyfill-php73", + "symfony/polyfill-php80", + "symfony/polyfill-php81", + "symfony/service-contracts", + "symfony/string" + ] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000..5a0d33b --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,260 @@ + array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'reference' => '41f83eb2e96503bcc35b6d75efcfde671184c9d7', + 'name' => '__root__', + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'reference' => '41f83eb2e96503bcc35b6d75efcfde671184c9d7', + 'dev_requirement' => false, + ), + 'cakephp/core' => array( + 'pretty_version' => '4.3.3', + 'version' => '4.3.3.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../cakephp/core', + 'aliases' => array(), + 'reference' => 'd80bcd1d94e1e90741ad7dbae882db8711239d07', + 'dev_requirement' => true, + ), + 'cakephp/database' => array( + 'pretty_version' => '4.3.3', + 'version' => '4.3.3.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../cakephp/database', + 'aliases' => array(), + 'reference' => '02197e9b1517ecdea546b8e7780682b34c967d3f', + 'dev_requirement' => true, + ), + 'cakephp/datasource' => array( + 'pretty_version' => '4.3.3', + 'version' => '4.3.3.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../cakephp/datasource', + 'aliases' => array(), + 'reference' => 'efd2a22555d982c2ab129c5e9e053971ed606624', + 'dev_requirement' => true, + ), + 'cakephp/utility' => array( + 'pretty_version' => '4.3.3', + 'version' => '4.3.3.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../cakephp/utility', + 'aliases' => array(), + 'reference' => '00410267c41036e20a43fbf0cb2ddedbd893a8a5', + 'dev_requirement' => true, + ), + 'nikic/fast-route' => array( + 'pretty_version' => 'v1.3.0', + 'version' => '1.3.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../nikic/fast-route', + 'aliases' => array(), + 'reference' => '181d480e08d9476e61381e04a71b34dc0432e812', + 'dev_requirement' => false, + ), + 'pimple/pimple' => array( + 'pretty_version' => 'v3.5.0', + 'version' => '3.5.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../pimple/pimple', + 'aliases' => array(), + 'reference' => 'a94b3a4db7fb774b3d78dad2315ddc07629e1bed', + 'dev_requirement' => false, + ), + 'psr/container' => array( + 'pretty_version' => '1.1.2', + 'version' => '1.1.2.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/container', + 'aliases' => array(), + 'reference' => '513e0666f7216c7459170d56df27dfcefe1689ea', + 'dev_requirement' => false, + ), + 'psr/http-message' => array( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-message', + 'aliases' => array(), + 'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363', + 'dev_requirement' => false, + ), + 'psr/http-message-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/log' => array( + 'pretty_version' => '1.1.4', + 'version' => '1.1.4.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/log', + 'aliases' => array(), + 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', + 'dev_requirement' => true, + ), + 'psr/log-implementation' => array( + 'dev_requirement' => true, + 'provided' => array( + 0 => '1.0|2.0', + ), + ), + 'psr/simple-cache' => array( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/simple-cache', + 'aliases' => array(), + 'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b', + 'dev_requirement' => true, + ), + 'robmorgan/phinx' => array( + 'pretty_version' => '0.12.9', + 'version' => '0.12.9.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../robmorgan/phinx', + 'aliases' => array(), + 'reference' => '5a0146a74c1bc195d1f5da86afa3b68badf7d90e', + 'dev_requirement' => true, + ), + 'slim/slim' => array( + 'pretty_version' => '3.12.3', + 'version' => '3.12.3.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../slim/slim', + 'aliases' => array(), + 'reference' => '1c9318a84ffb890900901136d620b4f03a59da38', + 'dev_requirement' => false, + ), + 'symfony/config' => array( + 'pretty_version' => 'v5.4.2', + 'version' => '5.4.2.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/config', + 'aliases' => array(), + 'reference' => '2e082dae50da563c639119b7b52347a2a3db4ba5', + 'dev_requirement' => true, + ), + 'symfony/console' => array( + 'pretty_version' => 'v5.4.2', + 'version' => '5.4.2.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/console', + 'aliases' => array(), + 'reference' => 'a2c6b7ced2eb7799a35375fb9022519282b5405e', + 'dev_requirement' => true, + ), + 'symfony/deprecation-contracts' => array( + 'pretty_version' => 'v3.0.0', + 'version' => '3.0.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', + 'aliases' => array(), + 'reference' => 'c726b64c1ccfe2896cb7df2e1331c357ad1c8ced', + 'dev_requirement' => true, + ), + 'symfony/filesystem' => array( + 'pretty_version' => 'v6.0.0', + 'version' => '6.0.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/filesystem', + 'aliases' => array(), + 'reference' => '52b3c9cce673b014915445a432339f282e002ce6', + 'dev_requirement' => true, + ), + 'symfony/polyfill-ctype' => array( + 'pretty_version' => 'v1.24.0', + 'version' => '1.24.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-ctype', + 'aliases' => array(), + 'reference' => '30885182c981ab175d4d034db0f6f469898070ab', + 'dev_requirement' => true, + ), + 'symfony/polyfill-intl-grapheme' => array( + 'pretty_version' => 'v1.24.0', + 'version' => '1.24.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme', + 'aliases' => array(), + 'reference' => '81b86b50cf841a64252b439e738e97f4a34e2783', + 'dev_requirement' => true, + ), + 'symfony/polyfill-intl-normalizer' => array( + 'pretty_version' => 'v1.24.0', + 'version' => '1.24.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer', + 'aliases' => array(), + 'reference' => '8590a5f561694770bdcd3f9b5c69dde6945028e8', + 'dev_requirement' => true, + ), + 'symfony/polyfill-mbstring' => array( + 'pretty_version' => 'v1.24.0', + 'version' => '1.24.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', + 'aliases' => array(), + 'reference' => '0abb51d2f102e00a4eefcf46ba7fec406d245825', + 'dev_requirement' => true, + ), + 'symfony/polyfill-php73' => array( + 'pretty_version' => 'v1.24.0', + 'version' => '1.24.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php73', + 'aliases' => array(), + 'reference' => 'cc5db0e22b3cb4111010e48785a97f670b350ca5', + 'dev_requirement' => true, + ), + 'symfony/polyfill-php80' => array( + 'pretty_version' => 'v1.24.0', + 'version' => '1.24.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php80', + 'aliases' => array(), + 'reference' => '57b712b08eddb97c762a8caa32c84e037892d2e9', + 'dev_requirement' => true, + ), + 'symfony/polyfill-php81' => array( + 'pretty_version' => 'v1.24.0', + 'version' => '1.24.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php81', + 'aliases' => array(), + 'reference' => '5de4ba2d41b15f9bd0e19b2ab9674135813ec98f', + 'dev_requirement' => true, + ), + 'symfony/service-contracts' => array( + 'pretty_version' => 'v2.4.1', + 'version' => '2.4.1.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/service-contracts', + 'aliases' => array(), + 'reference' => 'd664541b99d6fb0247ec5ff32e87238582236204', + 'dev_requirement' => true, + ), + 'symfony/string' => array( + 'pretty_version' => 'v6.0.2', + 'version' => '6.0.2.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/string', + 'aliases' => array(), + 'reference' => 'bae261d0c3ac38a1f802b4dfed42094296100631', + 'dev_requirement' => true, + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..580fa96 --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 70400)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + 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 + ); +} diff --git a/vendor/nikic/fast-route/.gitignore b/vendor/nikic/fast-route/.gitignore new file mode 100644 index 0000000..e378a07 --- /dev/null +++ b/vendor/nikic/fast-route/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +.idea/ + +# ignore lock file since we have no extra dependencies +composer.lock diff --git a/vendor/nikic/fast-route/.hhconfig b/vendor/nikic/fast-route/.hhconfig new file mode 100644 index 0000000..0c2153c --- /dev/null +++ b/vendor/nikic/fast-route/.hhconfig @@ -0,0 +1 @@ +assume_php=false diff --git a/vendor/nikic/fast-route/.travis.yml b/vendor/nikic/fast-route/.travis.yml new file mode 100644 index 0000000..10f8381 --- /dev/null +++ b/vendor/nikic/fast-route/.travis.yml @@ -0,0 +1,20 @@ +sudo: false +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - hhvm + +script: + - ./vendor/bin/phpunit + +before_install: + - travis_retry composer self-update + +install: + - composer install diff --git a/vendor/nikic/fast-route/FastRoute.hhi b/vendor/nikic/fast-route/FastRoute.hhi new file mode 100644 index 0000000..8d50738 --- /dev/null +++ b/vendor/nikic/fast-route/FastRoute.hhi @@ -0,0 +1,126 @@ +; + } + + class RouteCollector { + public function __construct(RouteParser $routeParser, DataGenerator $dataGenerator); + public function addRoute(mixed $httpMethod, string $route, mixed $handler): void; + public function getData(): array; + } + + class Route { + public function __construct(string $httpMethod, mixed $handler, string $regex, array $variables); + public function matches(string $str): bool; + } + + interface DataGenerator { + public function addRoute(string $httpMethod, array $routeData, mixed $handler); + public function getData(): array; + } + + interface Dispatcher { + const int NOT_FOUND = 0; + const int FOUND = 1; + const int METHOD_NOT_ALLOWED = 2; + public function dispatch(string $httpMethod, string $uri): array; + } + + function simpleDispatcher( + (function(RouteCollector): void) $routeDefinitionCallback, + shape( + ?'routeParser' => classname, + ?'dataGenerator' => classname, + ?'dispatcher' => classname, + ?'routeCollector' => classname, + ) $options = shape()): Dispatcher; + + function cachedDispatcher( + (function(RouteCollector): void) $routeDefinitionCallback, + shape( + ?'routeParser' => classname, + ?'dataGenerator' => classname, + ?'dispatcher' => classname, + ?'routeCollector' => classname, + ?'cacheDisabled' => bool, + ?'cacheFile' => string, + ) $options = shape()): Dispatcher; +} + +namespace FastRoute\DataGenerator { + abstract class RegexBasedAbstract implements \FastRoute\DataGenerator { + protected abstract function getApproxChunkSize(); + protected abstract function processChunk($regexToRoutesMap); + + public function addRoute(string $httpMethod, array $routeData, mixed $handler): void; + public function getData(): array; + } + + class CharCountBased extends RegexBasedAbstract { + protected function getApproxChunkSize(): int; + protected function processChunk(array $regexToRoutesMap): array; + } + + class GroupCountBased extends RegexBasedAbstract { + protected function getApproxChunkSize(): int; + protected function processChunk(array $regexToRoutesMap): array; + } + + class GroupPosBased extends RegexBasedAbstract { + protected function getApproxChunkSize(): int; + protected function processChunk(array $regexToRoutesMap): array; + } + + class MarkBased extends RegexBasedAbstract { + protected function getApproxChunkSize(): int; + protected function processChunk(array $regexToRoutesMap): array; + } +} + +namespace FastRoute\Dispatcher { + abstract class RegexBasedAbstract implements \FastRoute\Dispatcher { + protected abstract function dispatchVariableRoute(array $routeData, string $uri): array; + + public function dispatch(string $httpMethod, string $uri): array; + } + + class GroupPosBased extends RegexBasedAbstract { + public function __construct(array $data); + protected function dispatchVariableRoute(array $routeData, string $uri): array; + } + + class GroupCountBased extends RegexBasedAbstract { + public function __construct(array $data); + protected function dispatchVariableRoute(array $routeData, string $uri): array; + } + + class CharCountBased extends RegexBasedAbstract { + public function __construct(array $data); + protected function dispatchVariableRoute(array $routeData, string $uri): array; + } + + class MarkBased extends RegexBasedAbstract { + public function __construct(array $data); + protected function dispatchVariableRoute(array $routeData, string $uri): array; + } +} + +namespace FastRoute\RouteParser { + class Std implements \FastRoute\RouteParser { + const string VARIABLE_REGEX = <<<'REGEX' +\{ + \s* ([a-zA-Z][a-zA-Z0-9_]*) \s* + (?: + : \s* ([^{}]*(?:\{(?-1)\}[^{}]*)*) + )? +\} +REGEX; + const string DEFAULT_DISPATCH_REGEX = '[^/]+'; + public function parse(string $route): array; + } +} diff --git a/vendor/nikic/fast-route/LICENSE b/vendor/nikic/fast-route/LICENSE new file mode 100644 index 0000000..478e764 --- /dev/null +++ b/vendor/nikic/fast-route/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2013 by Nikita Popov. + +Some 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. + + * The names of the contributors may not 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 +OWNER 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/vendor/nikic/fast-route/README.md b/vendor/nikic/fast-route/README.md new file mode 100644 index 0000000..91bd466 --- /dev/null +++ b/vendor/nikic/fast-route/README.md @@ -0,0 +1,313 @@ +FastRoute - Fast request router for PHP +======================================= + +This library provides a fast implementation of a regular expression based router. [Blog post explaining how the +implementation works and why it is fast.][blog_post] + +Install +------- + +To install with composer: + +```sh +composer require nikic/fast-route +``` + +Requires PHP 5.4 or newer. + +Usage +----- + +Here's a basic usage example: + +```php +addRoute('GET', '/users', 'get_all_users_handler'); + // {id} must be a number (\d+) + $r->addRoute('GET', '/user/{id:\d+}', 'get_user_handler'); + // The /{title} suffix is optional + $r->addRoute('GET', '/articles/{id:\d+}[/{title}]', 'get_article_handler'); +}); + +// Fetch method and URI from somewhere +$httpMethod = $_SERVER['REQUEST_METHOD']; +$uri = $_SERVER['REQUEST_URI']; + +// Strip query string (?foo=bar) and decode URI +if (false !== $pos = strpos($uri, '?')) { + $uri = substr($uri, 0, $pos); +} +$uri = rawurldecode($uri); + +$routeInfo = $dispatcher->dispatch($httpMethod, $uri); +switch ($routeInfo[0]) { + case FastRoute\Dispatcher::NOT_FOUND: + // ... 404 Not Found + break; + case FastRoute\Dispatcher::METHOD_NOT_ALLOWED: + $allowedMethods = $routeInfo[1]; + // ... 405 Method Not Allowed + break; + case FastRoute\Dispatcher::FOUND: + $handler = $routeInfo[1]; + $vars = $routeInfo[2]; + // ... call $handler with $vars + break; +} +``` + +### Defining routes + +The routes are defined by calling the `FastRoute\simpleDispatcher()` function, which accepts +a callable taking a `FastRoute\RouteCollector` instance. The routes are added by calling +`addRoute()` on the collector instance: + +```php +$r->addRoute($method, $routePattern, $handler); +``` + +The `$method` is an uppercase HTTP method string for which a certain route should match. It +is possible to specify multiple valid methods using an array: + +```php +// These two calls +$r->addRoute('GET', '/test', 'handler'); +$r->addRoute('POST', '/test', 'handler'); +// Are equivalent to this one call +$r->addRoute(['GET', 'POST'], '/test', 'handler'); +``` + +By default the `$routePattern` uses a syntax where `{foo}` specifies a placeholder with name `foo` +and matching the regex `[^/]+`. To adjust the pattern the placeholder matches, you can specify +a custom pattern by writing `{bar:[0-9]+}`. Some examples: + +```php +// Matches /user/42, but not /user/xyz +$r->addRoute('GET', '/user/{id:\d+}', 'handler'); + +// Matches /user/foobar, but not /user/foo/bar +$r->addRoute('GET', '/user/{name}', 'handler'); + +// Matches /user/foo/bar as well +$r->addRoute('GET', '/user/{name:.+}', 'handler'); +``` + +Custom patterns for route placeholders cannot use capturing groups. For example `{lang:(en|de)}` +is not a valid placeholder, because `()` is a capturing group. Instead you can use either +`{lang:en|de}` or `{lang:(?:en|de)}`. + +Furthermore parts of the route enclosed in `[...]` are considered optional, so that `/foo[bar]` +will match both `/foo` and `/foobar`. Optional parts are only supported in a trailing position, +not in the middle of a route. + +```php +// This route +$r->addRoute('GET', '/user/{id:\d+}[/{name}]', 'handler'); +// Is equivalent to these two routes +$r->addRoute('GET', '/user/{id:\d+}', 'handler'); +$r->addRoute('GET', '/user/{id:\d+}/{name}', 'handler'); + +// Multiple nested optional parts are possible as well +$r->addRoute('GET', '/user[/{id:\d+}[/{name}]]', 'handler'); + +// This route is NOT valid, because optional parts can only occur at the end +$r->addRoute('GET', '/user[/{id:\d+}]/{name}', 'handler'); +``` + +The `$handler` parameter does not necessarily have to be a callback, it could also be a controller +class name or any other kind of data you wish to associate with the route. FastRoute only tells you +which handler corresponds to your URI, how you interpret it is up to you. + +#### Shorcut methods for common request methods + +For the `GET`, `POST`, `PUT`, `PATCH`, `DELETE` and `HEAD` request methods shortcut methods are available. For example: + +```php +$r->get('/get-route', 'get_handler'); +$r->post('/post-route', 'post_handler'); +``` + +Is equivalent to: + +```php +$r->addRoute('GET', '/get-route', 'get_handler'); +$r->addRoute('POST', '/post-route', 'post_handler'); +``` + +#### Route Groups + +Additionally, you can specify routes inside of a group. All routes defined inside a group will have a common prefix. + +For example, defining your routes as: + +```php +$r->addGroup('/admin', function (RouteCollector $r) { + $r->addRoute('GET', '/do-something', 'handler'); + $r->addRoute('GET', '/do-another-thing', 'handler'); + $r->addRoute('GET', '/do-something-else', 'handler'); +}); +``` + +Will have the same result as: + + ```php +$r->addRoute('GET', '/admin/do-something', 'handler'); +$r->addRoute('GET', '/admin/do-another-thing', 'handler'); +$r->addRoute('GET', '/admin/do-something-else', 'handler'); + ``` + +Nested groups are also supported, in which case the prefixes of all the nested groups are combined. + +### Caching + +The reason `simpleDispatcher` accepts a callback for defining the routes is to allow seamless +caching. By using `cachedDispatcher` instead of `simpleDispatcher` you can cache the generated +routing data and construct the dispatcher from the cached information: + +```php +addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); +}, [ + 'cacheFile' => __DIR__ . '/route.cache', /* required */ + 'cacheDisabled' => IS_DEBUG_ENABLED, /* optional, enabled by default */ +]); +``` + +The second parameter to the function is an options array, which can be used to specify the cache +file location, among other things. + +### Dispatching a URI + +A URI is dispatched by calling the `dispatch()` method of the created dispatcher. This method +accepts the HTTP method and a URI. Getting those two bits of information (and normalizing them +appropriately) is your job - this library is not bound to the PHP web SAPIs. + +The `dispatch()` method returns an array whose first element contains a status code. It is one +of `Dispatcher::NOT_FOUND`, `Dispatcher::METHOD_NOT_ALLOWED` and `Dispatcher::FOUND`. For the +method not allowed status the second array element contains a list of HTTP methods allowed for +the supplied URI. For example: + + [FastRoute\Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'POST']] + +> **NOTE:** The HTTP specification requires that a `405 Method Not Allowed` response include the +`Allow:` header to detail available methods for the requested resource. Applications using FastRoute +should use the second array element to add this header when relaying a 405 response. + +For the found status the second array element is the handler that was associated with the route +and the third array element is a dictionary of placeholder names to their values. For example: + + /* Routing against GET /user/nikic/42 */ + + [FastRoute\Dispatcher::FOUND, 'handler0', ['name' => 'nikic', 'id' => '42']] + +### Overriding the route parser and dispatcher + +The routing process makes use of three components: A route parser, a data generator and a +dispatcher. The three components adhere to the following interfaces: + +```php + 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', +]); +``` + +The above options array corresponds to the defaults. By replacing `GroupCountBased` by +`GroupPosBased` you could switch to a different dispatching strategy. + +### A Note on HEAD Requests + +The HTTP spec requires servers to [support both GET and HEAD methods][2616-511]: + +> The methods GET and HEAD MUST be supported by all general-purpose servers + +To avoid forcing users to manually register HEAD routes for each resource we fallback to matching an +available GET route for a given resource. The PHP web SAPI transparently removes the entity body +from HEAD responses so this behavior has no effect on the vast majority of users. + +However, implementers using FastRoute outside the web SAPI environment (e.g. a custom server) MUST +NOT send entity bodies generated in response to HEAD requests. If you are a non-SAPI user this is +*your responsibility*; FastRoute has no purview to prevent you from breaking HTTP in such cases. + +Finally, note that applications MAY always specify their own HEAD method route for a given +resource to bypass this behavior entirely. + +### Credits + +This library is based on a router that [Levi Morrison][levi] implemented for the Aerys server. + +A large number of tests, as well as HTTP compliance considerations, were provided by [Daniel Lowrey][rdlowrey]. + + +[2616-511]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1 "RFC 2616 Section 5.1.1" +[blog_post]: http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html +[levi]: https://github.com/morrisonlevi +[rdlowrey]: https://github.com/rdlowrey diff --git a/vendor/nikic/fast-route/composer.json b/vendor/nikic/fast-route/composer.json new file mode 100644 index 0000000..fb446a2 --- /dev/null +++ b/vendor/nikic/fast-route/composer.json @@ -0,0 +1,24 @@ +{ + "name": "nikic/fast-route", + "description": "Fast request router for PHP", + "keywords": ["routing", "router"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": ["src/functions.php"] + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + } +} diff --git a/vendor/nikic/fast-route/phpunit.xml b/vendor/nikic/fast-route/phpunit.xml new file mode 100644 index 0000000..3c807b6 --- /dev/null +++ b/vendor/nikic/fast-route/phpunit.xml @@ -0,0 +1,24 @@ + + + + + + ./test/ + + + + + + ./src/ + + + diff --git a/vendor/nikic/fast-route/psalm.xml b/vendor/nikic/fast-route/psalm.xml new file mode 100644 index 0000000..0dca5d7 --- /dev/null +++ b/vendor/nikic/fast-route/psalm.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/nikic/fast-route/src/BadRouteException.php b/vendor/nikic/fast-route/src/BadRouteException.php new file mode 100644 index 0000000..62262ec --- /dev/null +++ b/vendor/nikic/fast-route/src/BadRouteException.php @@ -0,0 +1,7 @@ + $route) { + $suffixLen++; + $suffix .= "\t"; + + $regexes[] = '(?:' . $regex . '/(\t{' . $suffixLen . '})\t{' . ($count - $suffixLen) . '})'; + $routeMap[$suffix] = [$route->handler, $route->variables]; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'suffix' => '/' . $suffix, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php b/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php new file mode 100644 index 0000000..54d9a05 --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php @@ -0,0 +1,30 @@ + $route) { + $numVariables = count($route->variables); + $numGroups = max($numGroups, $numVariables); + + $regexes[] = $regex . str_repeat('()', $numGroups - $numVariables); + $routeMap[$numGroups + 1] = [$route->handler, $route->variables]; + + ++$numGroups; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php b/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php new file mode 100644 index 0000000..fc4dc0a --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php @@ -0,0 +1,27 @@ + $route) { + $regexes[] = $regex; + $routeMap[$offset] = [$route->handler, $route->variables]; + + $offset += count($route->variables); + } + + $regex = '~^(?:' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php b/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php new file mode 100644 index 0000000..0aebed9 --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php @@ -0,0 +1,27 @@ + $route) { + $regexes[] = $regex . '(*MARK:' . $markName . ')'; + $routeMap[$markName] = [$route->handler, $route->variables]; + + ++$markName; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php b/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php new file mode 100644 index 0000000..6457290 --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php @@ -0,0 +1,186 @@ +isStaticRoute($routeData)) { + $this->addStaticRoute($httpMethod, $routeData, $handler); + } else { + $this->addVariableRoute($httpMethod, $routeData, $handler); + } + } + + /** + * @return mixed[] + */ + public function getData() + { + if (empty($this->methodToRegexToRoutesMap)) { + return [$this->staticRoutes, []]; + } + + return [$this->staticRoutes, $this->generateVariableRouteData()]; + } + + /** + * @return mixed[] + */ + private function generateVariableRouteData() + { + $data = []; + foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) { + $chunkSize = $this->computeChunkSize(count($regexToRoutesMap)); + $chunks = array_chunk($regexToRoutesMap, $chunkSize, true); + $data[$method] = array_map([$this, 'processChunk'], $chunks); + } + return $data; + } + + /** + * @param int + * @return int + */ + private function computeChunkSize($count) + { + $numParts = max(1, round($count / $this->getApproxChunkSize())); + return (int) ceil($count / $numParts); + } + + /** + * @param mixed[] + * @return bool + */ + private function isStaticRoute($routeData) + { + return count($routeData) === 1 && is_string($routeData[0]); + } + + private function addStaticRoute($httpMethod, $routeData, $handler) + { + $routeStr = $routeData[0]; + + if (isset($this->staticRoutes[$httpMethod][$routeStr])) { + throw new BadRouteException(sprintf( + 'Cannot register two routes matching "%s" for method "%s"', + $routeStr, $httpMethod + )); + } + + if (isset($this->methodToRegexToRoutesMap[$httpMethod])) { + foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) { + if ($route->matches($routeStr)) { + throw new BadRouteException(sprintf( + 'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"', + $routeStr, $route->regex, $httpMethod + )); + } + } + } + + $this->staticRoutes[$httpMethod][$routeStr] = $handler; + } + + private function addVariableRoute($httpMethod, $routeData, $handler) + { + list($regex, $variables) = $this->buildRegexForRoute($routeData); + + if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) { + throw new BadRouteException(sprintf( + 'Cannot register two routes matching "%s" for method "%s"', + $regex, $httpMethod + )); + } + + $this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route( + $httpMethod, $handler, $regex, $variables + ); + } + + /** + * @param mixed[] + * @return mixed[] + */ + private function buildRegexForRoute($routeData) + { + $regex = ''; + $variables = []; + foreach ($routeData as $part) { + if (is_string($part)) { + $regex .= preg_quote($part, '~'); + continue; + } + + list($varName, $regexPart) = $part; + + if (isset($variables[$varName])) { + throw new BadRouteException(sprintf( + 'Cannot use the same placeholder "%s" twice', $varName + )); + } + + if ($this->regexHasCapturingGroups($regexPart)) { + throw new BadRouteException(sprintf( + 'Regex "%s" for parameter "%s" contains a capturing group', + $regexPart, $varName + )); + } + + $variables[$varName] = $varName; + $regex .= '(' . $regexPart . ')'; + } + + return [$regex, $variables]; + } + + /** + * @param string + * @return bool + */ + private function regexHasCapturingGroups($regex) + { + if (false === strpos($regex, '(')) { + // Needs to have at least a ( to contain a capturing group + return false; + } + + // Semi-accurate detection for capturing groups + return (bool) preg_match( + '~ + (?: + \(\?\( + | \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \] + | \\\\ . + ) (*SKIP)(*FAIL) | + \( + (?! + \? (?! <(?![!=]) | P< | \' ) + | \* + ) + ~x', + $regex + ); + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher.php b/vendor/nikic/fast-route/src/Dispatcher.php new file mode 100644 index 0000000..4ae72a3 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher.php @@ -0,0 +1,26 @@ + 'value', ...]] + * + * @param string $httpMethod + * @param string $uri + * + * @return array + */ + public function dispatch($httpMethod, $uri); +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php b/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php new file mode 100644 index 0000000..ef1eec1 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php @@ -0,0 +1,31 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) + { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri . $data['suffix'], $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][end($matches)]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php b/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php new file mode 100644 index 0000000..493e7a9 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php @@ -0,0 +1,31 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) + { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][count($matches)]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php b/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php new file mode 100644 index 0000000..498220e --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php @@ -0,0 +1,33 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) + { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + // find first non-empty match + for ($i = 1; '' === $matches[$i]; ++$i); + + list($handler, $varNames) = $data['routeMap'][$i]; + + $vars = []; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[$i++]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php b/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php new file mode 100644 index 0000000..22eb09b --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php @@ -0,0 +1,31 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) + { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][$matches['MARK']]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php b/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php new file mode 100644 index 0000000..206e879 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php @@ -0,0 +1,88 @@ +staticRouteMap[$httpMethod][$uri])) { + $handler = $this->staticRouteMap[$httpMethod][$uri]; + return [self::FOUND, $handler, []]; + } + + $varRouteData = $this->variableRouteData; + if (isset($varRouteData[$httpMethod])) { + $result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri); + if ($result[0] === self::FOUND) { + return $result; + } + } + + // For HEAD requests, attempt fallback to GET + if ($httpMethod === 'HEAD') { + if (isset($this->staticRouteMap['GET'][$uri])) { + $handler = $this->staticRouteMap['GET'][$uri]; + return [self::FOUND, $handler, []]; + } + if (isset($varRouteData['GET'])) { + $result = $this->dispatchVariableRoute($varRouteData['GET'], $uri); + if ($result[0] === self::FOUND) { + return $result; + } + } + } + + // If nothing else matches, try fallback routes + if (isset($this->staticRouteMap['*'][$uri])) { + $handler = $this->staticRouteMap['*'][$uri]; + return [self::FOUND, $handler, []]; + } + if (isset($varRouteData['*'])) { + $result = $this->dispatchVariableRoute($varRouteData['*'], $uri); + if ($result[0] === self::FOUND) { + return $result; + } + } + + // Find allowed methods for this URI by matching against all other HTTP methods as well + $allowedMethods = []; + + foreach ($this->staticRouteMap as $method => $uriMap) { + if ($method !== $httpMethod && isset($uriMap[$uri])) { + $allowedMethods[] = $method; + } + } + + foreach ($varRouteData as $method => $routeData) { + if ($method === $httpMethod) { + continue; + } + + $result = $this->dispatchVariableRoute($routeData, $uri); + if ($result[0] === self::FOUND) { + $allowedMethods[] = $method; + } + } + + // If there are no allowed methods the route simply does not exist + if ($allowedMethods) { + return [self::METHOD_NOT_ALLOWED, $allowedMethods]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Route.php b/vendor/nikic/fast-route/src/Route.php new file mode 100644 index 0000000..e1bf7dd --- /dev/null +++ b/vendor/nikic/fast-route/src/Route.php @@ -0,0 +1,47 @@ +httpMethod = $httpMethod; + $this->handler = $handler; + $this->regex = $regex; + $this->variables = $variables; + } + + /** + * Tests whether this route matches the given string. + * + * @param string $str + * + * @return bool + */ + public function matches($str) + { + $regex = '~^' . $this->regex . '$~'; + return (bool) preg_match($regex, $str); + } +} diff --git a/vendor/nikic/fast-route/src/RouteCollector.php b/vendor/nikic/fast-route/src/RouteCollector.php new file mode 100644 index 0000000..c1c1762 --- /dev/null +++ b/vendor/nikic/fast-route/src/RouteCollector.php @@ -0,0 +1,152 @@ +routeParser = $routeParser; + $this->dataGenerator = $dataGenerator; + $this->currentGroupPrefix = ''; + } + + /** + * Adds a route to the collection. + * + * The syntax used in the $route string depends on the used route parser. + * + * @param string|string[] $httpMethod + * @param string $route + * @param mixed $handler + */ + public function addRoute($httpMethod, $route, $handler) + { + $route = $this->currentGroupPrefix . $route; + $routeDatas = $this->routeParser->parse($route); + foreach ((array) $httpMethod as $method) { + foreach ($routeDatas as $routeData) { + $this->dataGenerator->addRoute($method, $routeData, $handler); + } + } + } + + /** + * Create a route group with a common prefix. + * + * All routes created in the passed callback will have the given group prefix prepended. + * + * @param string $prefix + * @param callable $callback + */ + public function addGroup($prefix, callable $callback) + { + $previousGroupPrefix = $this->currentGroupPrefix; + $this->currentGroupPrefix = $previousGroupPrefix . $prefix; + $callback($this); + $this->currentGroupPrefix = $previousGroupPrefix; + } + + /** + * Adds a GET route to the collection + * + * This is simply an alias of $this->addRoute('GET', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function get($route, $handler) + { + $this->addRoute('GET', $route, $handler); + } + + /** + * Adds a POST route to the collection + * + * This is simply an alias of $this->addRoute('POST', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function post($route, $handler) + { + $this->addRoute('POST', $route, $handler); + } + + /** + * Adds a PUT route to the collection + * + * This is simply an alias of $this->addRoute('PUT', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function put($route, $handler) + { + $this->addRoute('PUT', $route, $handler); + } + + /** + * Adds a DELETE route to the collection + * + * This is simply an alias of $this->addRoute('DELETE', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function delete($route, $handler) + { + $this->addRoute('DELETE', $route, $handler); + } + + /** + * Adds a PATCH route to the collection + * + * This is simply an alias of $this->addRoute('PATCH', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function patch($route, $handler) + { + $this->addRoute('PATCH', $route, $handler); + } + + /** + * Adds a HEAD route to the collection + * + * This is simply an alias of $this->addRoute('HEAD', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function head($route, $handler) + { + $this->addRoute('HEAD', $route, $handler); + } + + /** + * Returns the collected route data, as provided by the data generator. + * + * @return array + */ + public function getData() + { + return $this->dataGenerator->getData(); + } +} diff --git a/vendor/nikic/fast-route/src/RouteParser.php b/vendor/nikic/fast-route/src/RouteParser.php new file mode 100644 index 0000000..6a7685c --- /dev/null +++ b/vendor/nikic/fast-route/src/RouteParser.php @@ -0,0 +1,37 @@ + $segment) { + if ($segment === '' && $n !== 0) { + throw new BadRouteException('Empty optional part'); + } + + $currentRoute .= $segment; + $routeDatas[] = $this->parsePlaceholders($currentRoute); + } + return $routeDatas; + } + + /** + * Parses a route string that does not contain optional segments. + * + * @param string + * @return mixed[] + */ + private function parsePlaceholders($route) + { + if (!preg_match_all( + '~' . self::VARIABLE_REGEX . '~x', $route, $matches, + PREG_OFFSET_CAPTURE | PREG_SET_ORDER + )) { + return [$route]; + } + + $offset = 0; + $routeData = []; + foreach ($matches as $set) { + if ($set[0][1] > $offset) { + $routeData[] = substr($route, $offset, $set[0][1] - $offset); + } + $routeData[] = [ + $set[1][0], + isset($set[2]) ? trim($set[2][0]) : self::DEFAULT_DISPATCH_REGEX + ]; + $offset = $set[0][1] + strlen($set[0][0]); + } + + if ($offset !== strlen($route)) { + $routeData[] = substr($route, $offset); + } + + return $routeData; + } +} diff --git a/vendor/nikic/fast-route/src/bootstrap.php b/vendor/nikic/fast-route/src/bootstrap.php new file mode 100644 index 0000000..0bce3a4 --- /dev/null +++ b/vendor/nikic/fast-route/src/bootstrap.php @@ -0,0 +1,12 @@ + 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', + 'routeCollector' => 'FastRoute\\RouteCollector', + ]; + + /** @var RouteCollector $routeCollector */ + $routeCollector = new $options['routeCollector']( + new $options['routeParser'], new $options['dataGenerator'] + ); + $routeDefinitionCallback($routeCollector); + + return new $options['dispatcher']($routeCollector->getData()); + } + + /** + * @param callable $routeDefinitionCallback + * @param array $options + * + * @return Dispatcher + */ + function cachedDispatcher(callable $routeDefinitionCallback, array $options = []) + { + $options += [ + 'routeParser' => 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', + 'routeCollector' => 'FastRoute\\RouteCollector', + 'cacheDisabled' => false, + ]; + + if (!isset($options['cacheFile'])) { + throw new \LogicException('Must specify "cacheFile" option'); + } + + if (!$options['cacheDisabled'] && file_exists($options['cacheFile'])) { + $dispatchData = require $options['cacheFile']; + if (!is_array($dispatchData)) { + throw new \RuntimeException('Invalid cache file "' . $options['cacheFile'] . '"'); + } + return new $options['dispatcher']($dispatchData); + } + + $routeCollector = new $options['routeCollector']( + new $options['routeParser'], new $options['dataGenerator'] + ); + $routeDefinitionCallback($routeCollector); + + /** @var RouteCollector $routeCollector */ + $dispatchData = $routeCollector->getData(); + if (!$options['cacheDisabled']) { + file_put_contents( + $options['cacheFile'], + ' $this->getDataGeneratorClass(), + 'dispatcher' => $this->getDispatcherClass() + ]; + } + + /** + * @dataProvider provideFoundDispatchCases + */ + public function testFoundDispatches($method, $uri, $callback, $handler, $argDict) + { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $info = $dispatcher->dispatch($method, $uri); + $this->assertSame($dispatcher::FOUND, $info[0]); + $this->assertSame($handler, $info[1]); + $this->assertSame($argDict, $info[2]); + } + + /** + * @dataProvider provideNotFoundDispatchCases + */ + public function testNotFoundDispatches($method, $uri, $callback) + { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $routeInfo = $dispatcher->dispatch($method, $uri); + $this->assertArrayNotHasKey(1, $routeInfo, + 'NOT_FOUND result must only contain a single element in the returned info array' + ); + $this->assertSame($dispatcher::NOT_FOUND, $routeInfo[0]); + } + + /** + * @dataProvider provideMethodNotAllowedDispatchCases + */ + public function testMethodNotAllowedDispatches($method, $uri, $callback, $availableMethods) + { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $routeInfo = $dispatcher->dispatch($method, $uri); + $this->assertArrayHasKey(1, $routeInfo, + 'METHOD_NOT_ALLOWED result must return an array of allowed methods at index 1' + ); + + list($routedStatus, $methodArray) = $dispatcher->dispatch($method, $uri); + $this->assertSame($dispatcher::METHOD_NOT_ALLOWED, $routedStatus); + $this->assertSame($availableMethods, $methodArray); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot use the same placeholder "test" twice + */ + public function testDuplicateVariableNameError() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/foo/{test}/{test:\d+}', 'handler0'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot register two routes matching "/user/([^/]+)" for method "GET" + */ + public function testDuplicateVariableRoute() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/user/{id}', 'handler0'); // oops, forgot \d+ restriction ;) + $r->addRoute('GET', '/user/{name}', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot register two routes matching "/user" for method "GET" + */ + public function testDuplicateStaticRoute() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/user', 'handler0'); + $r->addRoute('GET', '/user', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Static route "/user/nikic" is shadowed by previously defined variable route "/user/([^/]+)" for method "GET" + */ + public function testShadowedStaticRoute() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/nikic', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Regex "(en|de)" for parameter "lang" contains a capturing group + */ + public function testCapturing() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/{lang:(en|de)}', 'handler0'); + }, $this->generateDispatcherOptions()); + } + + public function provideFoundDispatchCases() + { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'GET'; + $uri = '/resource/123/456'; + $handler = 'handler0'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 1 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/handler0', 'handler0'); + $r->addRoute('GET', '/handler1', 'handler1'); + $r->addRoute('GET', '/handler2', 'handler2'); + }; + + $method = 'GET'; + $uri = '/handler2'; + $handler = 'handler2'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 2 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey'; + $handler = 'handler2'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 3 --------------------------------------------------------------------------------------> + + // reuse $callback from #2 + + $method = 'GET'; + $uri = '/user/12345'; + $handler = 'handler1'; + $argDict = ['id' => '12345']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 4 --------------------------------------------------------------------------------------> + + // reuse $callback from #3 + + $method = 'GET'; + $uri = '/user/NaN'; + $handler = 'handler2'; + $argDict = ['name' => 'NaN']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 5 --------------------------------------------------------------------------------------> + + // reuse $callback from #4 + + $method = 'GET'; + $uri = '/user/rdlowrey/12345'; + $handler = 'handler0'; + $argDict = ['name' => 'rdlowrey', 'id' => '12345']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 6 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/12345/extension', 'handler1'); + $r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', 'handler2'); + }; + + $method = 'GET'; + $uri = '/user/12345.svg'; + $handler = 'handler2'; + $argDict = ['id' => '12345', 'extension' => 'svg']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 7 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/static0', 'handler2'); + $r->addRoute('GET', '/static1', 'handler3'); + $r->addRoute('HEAD', '/static1', 'handler4'); + }; + + $method = 'HEAD'; + $uri = '/user/rdlowrey'; + $handler = 'handler0'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 8 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + // reuse $callback from #7 + + $method = 'HEAD'; + $uri = '/user/rdlowrey/1234'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey', 'id' => '1234']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 9 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + // reuse $callback from #8 + + $method = 'HEAD'; + $uri = '/static0'; + $handler = 'handler2'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 10 ---- Test existing HEAD route used if available (no fallback) -----------------------> + + // reuse $callback from #9 + + $method = 'HEAD'; + $uri = '/static1'; + $handler = 'handler4'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 11 ---- More specified routes are not shadowed by less specific of another method ------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1'); + }; + + $method = 'POST'; + $uri = '/user/rdlowrey'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 12 ---- Handler of more specific routes is used, if it occurs first --------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1'); + $r->addRoute('POST', '/user/{name}', 'handler2'); + }; + + $method = 'POST'; + $uri = '/user/rdlowrey'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 13 ---- Route with constant suffix -----------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/{name}/edit', 'handler1'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey/edit'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 14 ---- Handle multiple methods with the same handler ----------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost'); + $r->addRoute(['DELETE'], '/user', 'handlerDelete'); + $r->addRoute([], '/user', 'handlerNone'); + }; + + $argDict = []; + $cases[] = ['GET', '/user', $callback, 'handlerGetPost', $argDict]; + $cases[] = ['POST', '/user', $callback, 'handlerGetPost', $argDict]; + $cases[] = ['DELETE', '/user', $callback, 'handlerDelete', $argDict]; + + // 17 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('POST', '/user.json', 'handler0'); + $r->addRoute('GET', '/{entity}.json', 'handler1'); + }; + + $cases[] = ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user']]; + + // 18 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '', 'handler0'); + }; + + $cases[] = ['GET', '', $callback, 'handler0', []]; + + // 19 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('HEAD', '/a/{foo}', 'handler0'); + $r->addRoute('GET', '/b/{foo}', 'handler1'); + }; + + $cases[] = ['HEAD', '/b/bar', $callback, 'handler1', ['foo' => 'bar']]; + + // 20 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('HEAD', '/a', 'handler0'); + $r->addRoute('GET', '/b', 'handler1'); + }; + + $cases[] = ['HEAD', '/b', $callback, 'handler1', []]; + + // 21 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/foo', 'handler0'); + $r->addRoute('HEAD', '/{bar}', 'handler1'); + }; + + $cases[] = ['HEAD', '/foo', $callback, 'handler1', ['bar' => 'foo']]; + + // 22 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('*', '/user', 'handler0'); + $r->addRoute('*', '/{user}', 'handler1'); + $r->addRoute('GET', '/user', 'handler2'); + }; + + $cases[] = ['GET', '/user', $callback, 'handler2', []]; + + // 23 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('*', '/user', 'handler0'); + $r->addRoute('GET', '/user', 'handler1'); + }; + + $cases[] = ['POST', '/user', $callback, 'handler0', []]; + + // 24 ---- + + $cases[] = ['HEAD', '/user', $callback, 'handler1', []]; + + // 25 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/{bar}', 'handler0'); + $r->addRoute('*', '/foo', 'handler1'); + }; + + $cases[] = ['GET', '/foo', $callback, 'handler0', ['bar' => 'foo']]; + + // 26 ---- + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user', 'handler0'); + $r->addRoute('*', '/{foo:.*}', 'handler1'); + }; + + $cases[] = ['POST', '/bar', $callback, 'handler1', ['foo' => 'bar']]; + + // x --------------------------------------------------------------------------------------> + + return $cases; + } + + public function provideNotFoundDispatchCases() + { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 1 --------------------------------------------------------------------------------------> + + // reuse callback from #0 + $method = 'POST'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 2 --------------------------------------------------------------------------------------> + + // reuse callback from #1 + $method = 'PUT'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 3 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/handler0', 'handler0'); + $r->addRoute('GET', '/handler1', 'handler1'); + $r->addRoute('GET', '/handler2', 'handler2'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 4 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 5 --------------------------------------------------------------------------------------> + + // reuse callback from #4 + $method = 'GET'; + $uri = '/user/rdlowrey/12345/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 6 --------------------------------------------------------------------------------------> + + // reuse callback from #5 + $method = 'HEAD'; + + $cases[] = [$method, $uri, $callback]; + + // x --------------------------------------------------------------------------------------> + + return $cases; + } + + public function provideMethodNotAllowedDispatchCases() + { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'POST'; + $uri = '/resource/123/456'; + $allowedMethods = ['GET']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 1 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + $r->addRoute('POST', '/resource/123/456', 'handler1'); + $r->addRoute('PUT', '/resource/123/456', 'handler2'); + $r->addRoute('*', '/', 'handler3'); + }; + + $method = 'DELETE'; + $uri = '/resource/123/456'; + $allowedMethods = ['GET', 'POST', 'PUT']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 2 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('POST', '/user/{name}/{id:[0-9]+}', 'handler1'); + $r->addRoute('PUT', '/user/{name}/{id:[0-9]+}', 'handler2'); + $r->addRoute('PATCH', '/user/{name}/{id:[0-9]+}', 'handler3'); + }; + + $method = 'DELETE'; + $uri = '/user/rdlowrey/42'; + $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 3 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('POST', '/user/{name}', 'handler1'); + $r->addRoute('PUT', '/user/{name:[a-z]+}', 'handler2'); + $r->addRoute('PATCH', '/user/{name:[a-z]+}', 'handler3'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey'; + $allowedMethods = ['POST', 'PUT', 'PATCH']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 4 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost'); + $r->addRoute(['DELETE'], '/user', 'handlerDelete'); + $r->addRoute([], '/user', 'handlerNone'); + }; + + $cases[] = ['PUT', '/user', $callback, ['GET', 'POST', 'DELETE']]; + + // 5 + + $callback = function (RouteCollector $r) { + $r->addRoute('POST', '/user.json', 'handler0'); + $r->addRoute('GET', '/{entity}.json', 'handler1'); + }; + + $cases[] = ['PUT', '/user.json', $callback, ['POST', 'GET']]; + + // x --------------------------------------------------------------------------------------> + + return $cases; + } +} diff --git a/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php b/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php new file mode 100644 index 0000000..f821ef5 --- /dev/null +++ b/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php @@ -0,0 +1,16 @@ +markTestSkipped('PHP 5.6 required for MARK support'); + } + } + + protected function getDispatcherClass() + { + return 'FastRoute\\Dispatcher\\MarkBased'; + } + + protected function getDataGeneratorClass() + { + return 'FastRoute\\DataGenerator\\MarkBased'; + } +} diff --git a/vendor/nikic/fast-route/test/HackTypechecker/HackTypecheckerTest.php b/vendor/nikic/fast-route/test/HackTypechecker/HackTypecheckerTest.php new file mode 100644 index 0000000..b6fc53f --- /dev/null +++ b/vendor/nikic/fast-route/test/HackTypechecker/HackTypecheckerTest.php @@ -0,0 +1,44 @@ +markTestSkipped('HHVM only'); + } + if (!version_compare(HHVM_VERSION, '3.9.0', '>=')) { + $this->markTestSkipped('classname requires HHVM 3.9+'); + } + + // The typechecker recurses the whole tree, so it makes sure + // that everything in fixtures/ is valid when this runs. + + $output = []; + $exit_code = null; + exec( + 'hh_server --check ' . escapeshellarg(__DIR__ . '/../../') . ' 2>&1', + $output, + $exit_code + ); + if ($exit_code === self::SERVER_ALREADY_RUNNING_CODE) { + $this->assertTrue( + $recurse, + 'Typechecker still running after running hh_client stop' + ); + // Server already running - 3.10 => 3.11 regression: + // https://github.com/facebook/hhvm/issues/6646 + exec('hh_client stop 2>/dev/null'); + $this->testTypechecks(/* recurse = */ false); + return; + + } + $this->assertSame(0, $exit_code, implode("\n", $output)); + } +} diff --git a/vendor/nikic/fast-route/test/HackTypechecker/fixtures/all_options.php b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/all_options.php new file mode 100644 index 0000000..05a9af2 --- /dev/null +++ b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/all_options.php @@ -0,0 +1,29 @@ + {}, + shape( + 'routeParser' => \FastRoute\RouteParser\Std::class, + 'dataGenerator' => \FastRoute\DataGenerator\GroupCountBased::class, + 'dispatcher' => \FastRoute\Dispatcher\GroupCountBased::class, + 'routeCollector' => \FastRoute\RouteCollector::class, + ), + ); +} + +function all_options_cached(): \FastRoute\Dispatcher { + return \FastRoute\cachedDispatcher( + $collector ==> {}, + shape( + 'routeParser' => \FastRoute\RouteParser\Std::class, + 'dataGenerator' => \FastRoute\DataGenerator\GroupCountBased::class, + 'dispatcher' => \FastRoute\Dispatcher\GroupCountBased::class, + 'routeCollector' => \FastRoute\RouteCollector::class, + 'cacheFile' => '/dev/null', + 'cacheDisabled' => false, + ), + ); +} diff --git a/vendor/nikic/fast-route/test/HackTypechecker/fixtures/empty_options.php b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/empty_options.php new file mode 100644 index 0000000..61eb541 --- /dev/null +++ b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/empty_options.php @@ -0,0 +1,11 @@ + {}, shape()); +} + +function empty_options_cached(): \FastRoute\Dispatcher { + return \FastRoute\cachedDispatcher($collector ==> {}, shape()); +} diff --git a/vendor/nikic/fast-route/test/HackTypechecker/fixtures/no_options.php b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/no_options.php new file mode 100644 index 0000000..44b5422 --- /dev/null +++ b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/no_options.php @@ -0,0 +1,11 @@ + {}); +} + +function no_options_cached(): \FastRoute\Dispatcher { + return \FastRoute\cachedDispatcher($collector ==> {}); +} diff --git a/vendor/nikic/fast-route/test/RouteCollectorTest.php b/vendor/nikic/fast-route/test/RouteCollectorTest.php new file mode 100644 index 0000000..cc54407 --- /dev/null +++ b/vendor/nikic/fast-route/test/RouteCollectorTest.php @@ -0,0 +1,108 @@ +delete('/delete', 'delete'); + $r->get('/get', 'get'); + $r->head('/head', 'head'); + $r->patch('/patch', 'patch'); + $r->post('/post', 'post'); + $r->put('/put', 'put'); + + $expected = [ + ['DELETE', '/delete', 'delete'], + ['GET', '/get', 'get'], + ['HEAD', '/head', 'head'], + ['PATCH', '/patch', 'patch'], + ['POST', '/post', 'post'], + ['PUT', '/put', 'put'], + ]; + + $this->assertSame($expected, $r->routes); + } + + public function testGroups() + { + $r = new DummyRouteCollector(); + + $r->delete('/delete', 'delete'); + $r->get('/get', 'get'); + $r->head('/head', 'head'); + $r->patch('/patch', 'patch'); + $r->post('/post', 'post'); + $r->put('/put', 'put'); + + $r->addGroup('/group-one', function (DummyRouteCollector $r) { + $r->delete('/delete', 'delete'); + $r->get('/get', 'get'); + $r->head('/head', 'head'); + $r->patch('/patch', 'patch'); + $r->post('/post', 'post'); + $r->put('/put', 'put'); + + $r->addGroup('/group-two', function (DummyRouteCollector $r) { + $r->delete('/delete', 'delete'); + $r->get('/get', 'get'); + $r->head('/head', 'head'); + $r->patch('/patch', 'patch'); + $r->post('/post', 'post'); + $r->put('/put', 'put'); + }); + }); + + $r->addGroup('/admin', function (DummyRouteCollector $r) { + $r->get('-some-info', 'admin-some-info'); + }); + $r->addGroup('/admin-', function (DummyRouteCollector $r) { + $r->get('more-info', 'admin-more-info'); + }); + + $expected = [ + ['DELETE', '/delete', 'delete'], + ['GET', '/get', 'get'], + ['HEAD', '/head', 'head'], + ['PATCH', '/patch', 'patch'], + ['POST', '/post', 'post'], + ['PUT', '/put', 'put'], + ['DELETE', '/group-one/delete', 'delete'], + ['GET', '/group-one/get', 'get'], + ['HEAD', '/group-one/head', 'head'], + ['PATCH', '/group-one/patch', 'patch'], + ['POST', '/group-one/post', 'post'], + ['PUT', '/group-one/put', 'put'], + ['DELETE', '/group-one/group-two/delete', 'delete'], + ['GET', '/group-one/group-two/get', 'get'], + ['HEAD', '/group-one/group-two/head', 'head'], + ['PATCH', '/group-one/group-two/patch', 'patch'], + ['POST', '/group-one/group-two/post', 'post'], + ['PUT', '/group-one/group-two/put', 'put'], + ['GET', '/admin-some-info', 'admin-some-info'], + ['GET', '/admin-more-info', 'admin-more-info'], + ]; + + $this->assertSame($expected, $r->routes); + } +} + +class DummyRouteCollector extends RouteCollector +{ + public $routes = []; + + public function __construct() + { + } + + public function addRoute($method, $route, $handler) + { + $route = $this->currentGroupPrefix . $route; + $this->routes[] = [$method, $route, $handler]; + } +} diff --git a/vendor/nikic/fast-route/test/RouteParser/StdTest.php b/vendor/nikic/fast-route/test/RouteParser/StdTest.php new file mode 100644 index 0000000..e13e4de --- /dev/null +++ b/vendor/nikic/fast-route/test/RouteParser/StdTest.php @@ -0,0 +1,154 @@ +parse($routeString); + $this->assertSame($expectedRouteDatas, $routeDatas); + } + + /** @dataProvider provideTestParseError */ + public function testParseError($routeString, $expectedExceptionMessage) + { + $parser = new Std(); + $this->setExpectedException('FastRoute\\BadRouteException', $expectedExceptionMessage); + $parser->parse($routeString); + } + + public function provideTestParse() + { + return [ + [ + '/test', + [ + ['/test'], + ] + ], + [ + '/test/{param}', + [ + ['/test/', ['param', '[^/]+']], + ] + ], + [ + '/te{ param }st', + [ + ['/te', ['param', '[^/]+'], 'st'] + ] + ], + [ + '/test/{param1}/test2/{param2}', + [ + ['/test/', ['param1', '[^/]+'], '/test2/', ['param2', '[^/]+']] + ] + ], + [ + '/test/{param:\d+}', + [ + ['/test/', ['param', '\d+']] + ] + ], + [ + '/test/{ param : \d{1,9} }', + [ + ['/test/', ['param', '\d{1,9}']] + ] + ], + [ + '/test[opt]', + [ + ['/test'], + ['/testopt'], + ] + ], + [ + '/test[/{param}]', + [ + ['/test'], + ['/test/', ['param', '[^/]+']], + ] + ], + [ + '/{param}[opt]', + [ + ['/', ['param', '[^/]+']], + ['/', ['param', '[^/]+'], 'opt'] + ] + ], + [ + '/test[/{name}[/{id:[0-9]+}]]', + [ + ['/test'], + ['/test/', ['name', '[^/]+']], + ['/test/', ['name', '[^/]+'], '/', ['id', '[0-9]+']], + ] + ], + [ + '', + [ + [''], + ] + ], + [ + '[test]', + [ + [''], + ['test'], + ] + ], + [ + '/{foo-bar}', + [ + ['/', ['foo-bar', '[^/]+']] + ] + ], + [ + '/{_foo:.*}', + [ + ['/', ['_foo', '.*']] + ] + ], + ]; + } + + public function provideTestParseError() + { + return [ + [ + '/test[opt', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/test[opt[opt2]', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/testopt]', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/test[]', + 'Empty optional part' + ], + [ + '/test[[opt]]', + 'Empty optional part' + ], + [ + '[[test]]', + 'Empty optional part' + ], + [ + '/test[/opt]/required', + 'Optional segments can only occur at the end of a route' + ], + ]; + } +} diff --git a/vendor/nikic/fast-route/test/bootstrap.php b/vendor/nikic/fast-route/test/bootstrap.php new file mode 100644 index 0000000..3023f41 --- /dev/null +++ b/vendor/nikic/fast-route/test/bootstrap.php @@ -0,0 +1,11 @@ +setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + '@PHPUnit75Migration:risky' => true, + 'php_unit_dedicate_assert' => true, + 'array_syntax' => ['syntax' => 'short'], + 'php_unit_fqcn_annotation' => true, + 'no_unreachable_default_argument_value' => false, + 'braces' => ['allow_single_line_closure' => true], + 'heredoc_to_nowdoc' => false, + 'ordered_imports' => true, + 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], + 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'all'], + ]) + ->setRiskyAllowed(true) + ->setFinder(PhpCsFixer\Finder::create()->in(__DIR__.'/src')) +; diff --git a/vendor/pimple/pimple/CHANGELOG b/vendor/pimple/pimple/CHANGELOG new file mode 100644 index 0000000..08059e5 --- /dev/null +++ b/vendor/pimple/pimple/CHANGELOG @@ -0,0 +1,72 @@ +* 3.4.0 (2021-03-06) + + * Implement version 1.1 of PSR-11 + +* 3.3.1 (2020-11-24) + + * Add support for PHP 8 + +* 3.3.0 (2020-03-03) + + * Drop PHP extension + * Bump min PHP version to 7.2.5 + +* 3.2.3 (2018-01-21) + + * prefixed all function calls with \ for extra speed + +* 3.2.2 (2017-07-23) + + * reverted extending a protected closure throws an exception (deprecated it instead) + +* 3.2.1 (2017-07-17) + + * fixed PHP error + +* 3.2.0 (2017-07-17) + + * added a PSR-11 service locator + * added a PSR-11 wrapper + * added ServiceIterator + * fixed extending a protected closure (now throws InvalidServiceIdentifierException) + +* 3.1.0 (2017-07-03) + + * deprecated the C extension + * added support for PSR-11 exceptions + +* 3.0.2 (2015-09-11) + + * refactored the C extension + * minor non-significant changes + +* 3.0.1 (2015-07-30) + + * simplified some code + * fixed a segfault in the C extension + +* 3.0.0 (2014-07-24) + + * removed the Pimple class alias (use Pimple\Container instead) + +* 2.1.1 (2014-07-24) + + * fixed compiler warnings for the C extension + * fixed code when dealing with circular references + +* 2.1.0 (2014-06-24) + + * moved the Pimple to Pimple\Container (with a BC layer -- Pimple is now a + deprecated alias which will be removed in Pimple 3.0) + * added Pimple\ServiceProviderInterface (and Pimple::register()) + +* 2.0.0 (2014-02-10) + + * changed extend to automatically re-assign the extended service and keep it as shared or factory + (to keep BC, extend still returns the extended service) + * changed services to be shared by default (use factory() for factory + services) + +* 1.0.0 + + * initial version diff --git a/vendor/pimple/pimple/LICENSE b/vendor/pimple/pimple/LICENSE new file mode 100644 index 0000000..3e2a9e1 --- /dev/null +++ b/vendor/pimple/pimple/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2009-2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/pimple/pimple/README.rst b/vendor/pimple/pimple/README.rst new file mode 100644 index 0000000..7081839 --- /dev/null +++ b/vendor/pimple/pimple/README.rst @@ -0,0 +1,332 @@ +Pimple +====== + +.. caution:: + + Pimple is now closed for changes. No new features will be added and no + cosmetic changes will be accepted either. The only accepted changes are + compatibility with newer PHP versions and security issue fixes. + +.. caution:: + + This is the documentation for Pimple 3.x. If you are using Pimple 1.x, read + the `Pimple 1.x documentation`_. Reading the Pimple 1.x code is also a good + way to learn more about how to create a simple Dependency Injection + Container (recent versions of Pimple are more focused on performance). + +Pimple is a small Dependency Injection Container for PHP. + +Installation +------------ + +Before using Pimple in your project, add it to your ``composer.json`` file: + +.. code-block:: bash + + $ ./composer.phar require pimple/pimple "^3.0" + +Usage +----- + +Creating a container is a matter of creating a ``Container`` instance: + +.. code-block:: php + + use Pimple\Container; + + $container = new Container(); + +As many other dependency injection containers, Pimple manages two different +kind of data: **services** and **parameters**. + +Defining Services +~~~~~~~~~~~~~~~~~ + +A service is an object that does something as part of a larger system. Examples +of services: a database connection, a templating engine, or a mailer. Almost +any **global** object can be a service. + +Services are defined by **anonymous functions** that return an instance of an +object: + +.. code-block:: php + + // define some services + $container['session_storage'] = function ($c) { + return new SessionStorage('SESSION_ID'); + }; + + $container['session'] = function ($c) { + return new Session($c['session_storage']); + }; + +Notice that the anonymous function has access to the current container +instance, allowing references to other services or parameters. + +As objects are only created when you get them, the order of the definitions +does not matter. + +Using the defined services is also very easy: + +.. code-block:: php + + // get the session object + $session = $container['session']; + + // the above call is roughly equivalent to the following code: + // $storage = new SessionStorage('SESSION_ID'); + // $session = new Session($storage); + +Defining Factory Services +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, each time you get a service, Pimple returns the **same instance** +of it. If you want a different instance to be returned for all calls, wrap your +anonymous function with the ``factory()`` method + +.. code-block:: php + + $container['session'] = $container->factory(function ($c) { + return new Session($c['session_storage']); + }); + +Now, each call to ``$container['session']`` returns a new instance of the +session. + +Defining Parameters +~~~~~~~~~~~~~~~~~~~ + +Defining a parameter allows to ease the configuration of your container from +the outside and to store global values: + +.. code-block:: php + + // define some parameters + $container['cookie_name'] = 'SESSION_ID'; + $container['session_storage_class'] = 'SessionStorage'; + +If you change the ``session_storage`` service definition like below: + +.. code-block:: php + + $container['session_storage'] = function ($c) { + return new $c['session_storage_class']($c['cookie_name']); + }; + +You can now easily change the cookie name by overriding the +``cookie_name`` parameter instead of redefining the service +definition. + +Protecting Parameters +~~~~~~~~~~~~~~~~~~~~~ + +Because Pimple sees anonymous functions as service definitions, you need to +wrap anonymous functions with the ``protect()`` method to store them as +parameters: + +.. code-block:: php + + $container['random_func'] = $container->protect(function () { + return rand(); + }); + +Modifying Services after Definition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In some cases you may want to modify a service definition after it has been +defined. You can use the ``extend()`` method to define additional code to be +run on your service just after it is created: + +.. code-block:: php + + $container['session_storage'] = function ($c) { + return new $c['session_storage_class']($c['cookie_name']); + }; + + $container->extend('session_storage', function ($storage, $c) { + $storage->...(); + + return $storage; + }); + +The first argument is the name of the service to extend, the second a function +that gets access to the object instance and the container. + +Extending a Container +~~~~~~~~~~~~~~~~~~~~~ + +If you use the same libraries over and over, you might want to reuse some +services from one project to the next one; package your services into a +**provider** by implementing ``Pimple\ServiceProviderInterface``: + +.. code-block:: php + + use Pimple\Container; + + class FooProvider implements Pimple\ServiceProviderInterface + { + public function register(Container $pimple) + { + // register some services and parameters + // on $pimple + } + } + +Then, register the provider on a Container: + +.. code-block:: php + + $pimple->register(new FooProvider()); + +Fetching the Service Creation Function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you access an object, Pimple automatically calls the anonymous function +that you defined, which creates the service object for you. If you want to get +raw access to this function, you can use the ``raw()`` method: + +.. code-block:: php + + $container['session'] = function ($c) { + return new Session($c['session_storage']); + }; + + $sessionFunction = $container->raw('session'); + +PSR-11 compatibility +-------------------- + +For historical reasons, the ``Container`` class does not implement the PSR-11 +``ContainerInterface``. However, Pimple provides a helper class that will let +you decouple your code from the Pimple container class. + +The PSR-11 container class +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``Pimple\Psr11\Container`` class lets you access the content of an +underlying Pimple container using ``Psr\Container\ContainerInterface`` +methods: + +.. code-block:: php + + use Pimple\Container; + use Pimple\Psr11\Container as PsrContainer; + + $container = new Container(); + $container['service'] = function ($c) { + return new Service(); + }; + $psr11 = new PsrContainer($container); + + $controller = function (PsrContainer $container) { + $service = $container->get('service'); + }; + $controller($psr11); + +Using the PSR-11 ServiceLocator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, a service needs access to several other services without being sure +that all of them will actually be used. In those cases, you may want the +instantiation of the services to be lazy. + +The traditional solution is to inject the entire service container to get only +the services really needed. However, this is not recommended because it gives +services a too broad access to the rest of the application and it hides their +actual dependencies. + +The ``ServiceLocator`` is intended to solve this problem by giving access to a +set of predefined services while instantiating them only when actually needed. + +It also allows you to make your services available under a different name than +the one used to register them. For instance, you may want to use an object +that expects an instance of ``EventDispatcherInterface`` to be available under +the name ``event_dispatcher`` while your event dispatcher has been +registered under the name ``dispatcher``: + +.. code-block:: php + + use Monolog\Logger; + use Pimple\Psr11\ServiceLocator; + use Psr\Container\ContainerInterface; + use Symfony\Component\EventDispatcher\EventDispatcher; + + class MyService + { + /** + * "logger" must be an instance of Psr\Log\LoggerInterface + * "event_dispatcher" must be an instance of Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + private $services; + + public function __construct(ContainerInterface $services) + { + $this->services = $services; + } + } + + $container['logger'] = function ($c) { + return new Monolog\Logger(); + }; + $container['dispatcher'] = function () { + return new EventDispatcher(); + }; + + $container['service'] = function ($c) { + $locator = new ServiceLocator($c, array('logger', 'event_dispatcher' => 'dispatcher')); + + return new MyService($locator); + }; + +Referencing a Collection of Services Lazily +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Passing a collection of services instances in an array may prove inefficient +if the class that consumes the collection only needs to iterate over it at a +later stage, when one of its method is called. It can also lead to problems +if there is a circular dependency between one of the services stored in the +collection and the class that consumes it. + +The ``ServiceIterator`` class helps you solve these issues. It receives a +list of service names during instantiation and will retrieve the services +when iterated over: + +.. code-block:: php + + use Pimple\Container; + use Pimple\ServiceIterator; + + class AuthorizationService + { + private $voters; + + public function __construct($voters) + { + $this->voters = $voters; + } + + public function canAccess($resource) + { + foreach ($this->voters as $voter) { + if (true === $voter->canAccess($resource)) { + return true; + } + } + + return false; + } + } + + $container = new Container(); + + $container['voter1'] = function ($c) { + return new SomeVoter(); + } + $container['voter2'] = function ($c) { + return new SomeOtherVoter($c['auth']); + } + $container['auth'] = function ($c) { + return new AuthorizationService(new ServiceIterator($c, array('voter1', 'voter2')); + } + +.. _Pimple 1.x documentation: https://github.com/silexphp/Pimple/tree/1.1 diff --git a/vendor/pimple/pimple/composer.json b/vendor/pimple/pimple/composer.json new file mode 100644 index 0000000..ca6a581 --- /dev/null +++ b/vendor/pimple/pimple/composer.json @@ -0,0 +1,29 @@ +{ + "name": "pimple/pimple", + "type": "library", + "description": "Pimple, a simple Dependency Injection Container", + "keywords": ["dependency injection", "container"], + "homepage": "https://pimple.symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4@dev" + }, + "autoload": { + "psr-0": { "Pimple": "src/" } + }, + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + } +} diff --git a/vendor/pimple/pimple/phpunit.xml.dist b/vendor/pimple/pimple/phpunit.xml.dist new file mode 100644 index 0000000..8990202 --- /dev/null +++ b/vendor/pimple/pimple/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + + ./src/Pimple/Tests + + + + + + + diff --git a/vendor/pimple/pimple/src/Pimple/Container.php b/vendor/pimple/pimple/src/Pimple/Container.php new file mode 100644 index 0000000..586a0b7 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Container.php @@ -0,0 +1,305 @@ +factories = new \SplObjectStorage(); + $this->protected = new \SplObjectStorage(); + + foreach ($values as $key => $value) { + $this->offsetSet($key, $value); + } + } + + /** + * Sets a parameter or an object. + * + * Objects must be defined as Closures. + * + * Allowing any PHP callable leads to difficult to debug problems + * as function names (strings) are callable (creating a function with + * the same name as an existing parameter would break your container). + * + * @param string $id The unique identifier for the parameter or object + * @param mixed $value The value of the parameter or a closure to define an object + * + * @return void + * + * @throws FrozenServiceException Prevent override of a frozen service + */ + #[\ReturnTypeWillChange] + public function offsetSet($id, $value) + { + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + $this->values[$id] = $value; + $this->keys[$id] = true; + } + + /** + * Gets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or an object + * + * @throws UnknownIdentifierException If the identifier is not defined + */ + #[\ReturnTypeWillChange] + public function offsetGet($id) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if ( + isset($this->raw[$id]) + || !\is_object($this->values[$id]) + || isset($this->protected[$this->values[$id]]) + || !\method_exists($this->values[$id], '__invoke') + ) { + return $this->values[$id]; + } + + if (isset($this->factories[$this->values[$id]])) { + return $this->values[$id]($this); + } + + $raw = $this->values[$id]; + $val = $this->values[$id] = $raw($this); + $this->raw[$id] = $raw; + + $this->frozen[$id] = true; + + return $val; + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id The unique identifier for the parameter or object + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($id) + { + return isset($this->keys[$id]); + } + + /** + * Unsets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($id) + { + if (isset($this->keys[$id])) { + if (\is_object($this->values[$id])) { + unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]); + } + + unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]); + } + } + + /** + * Marks a callable as being a factory service. + * + * @param callable $callable A service definition to be used as a factory + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public function factory($callable) + { + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.'); + } + + $this->factories->attach($callable); + + return $callable; + } + + /** + * Protects a callable from being interpreted as a service. + * + * This is useful when you want to store a callable as a parameter. + * + * @param callable $callable A callable to protect from being evaluated + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public function protect($callable) + { + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Callable is not a Closure or invokable object.'); + } + + $this->protected->attach($callable); + + return $callable; + } + + /** + * Gets a parameter or the closure defining an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or the closure defining an object + * + * @throws UnknownIdentifierException If the identifier is not defined + */ + public function raw($id) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if (isset($this->raw[$id])) { + return $this->raw[$id]; + } + + return $this->values[$id]; + } + + /** + * Extends an object definition. + * + * Useful when you want to extend an existing object definition, + * without necessarily loading that object. + * + * @param string $id The unique identifier for the object + * @param callable $callable A service definition to extend the original + * + * @return callable The wrapped callable + * + * @throws UnknownIdentifierException If the identifier is not defined + * @throws FrozenServiceException If the service is frozen + * @throws InvalidServiceIdentifierException If the identifier belongs to a parameter + * @throws ExpectedInvokableException If the extension callable is not a closure or an invokable object + */ + public function extend($id, $callable) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + if (!\is_object($this->values[$id]) || !\method_exists($this->values[$id], '__invoke')) { + throw new InvalidServiceIdentifierException($id); + } + + if (isset($this->protected[$this->values[$id]])) { + @\trigger_error(\sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "%s" should be protected?', $id), E_USER_DEPRECATED); + } + + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.'); + } + + $factory = $this->values[$id]; + + $extended = function ($c) use ($callable, $factory) { + return $callable($factory($c), $c); + }; + + if (isset($this->factories[$factory])) { + $this->factories->detach($factory); + $this->factories->attach($extended); + } + + return $this[$id] = $extended; + } + + /** + * Returns all defined value names. + * + * @return array An array of value names + */ + public function keys() + { + return \array_keys($this->values); + } + + /** + * Registers a service provider. + * + * @param array $values An array of values that customizes the provider + * + * @return static + */ + public function register(ServiceProviderInterface $provider, array $values = []) + { + $provider->register($this); + + foreach ($values as $key => $value) { + $this[$key] = $value; + } + + return $this; + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php b/vendor/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php new file mode 100644 index 0000000..7228421 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php @@ -0,0 +1,38 @@ + + */ +class ExpectedInvokableException extends \InvalidArgumentException implements ContainerExceptionInterface +{ +} diff --git a/vendor/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php b/vendor/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php new file mode 100644 index 0000000..e4d2f6d --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php @@ -0,0 +1,45 @@ + + */ +class FrozenServiceException extends \RuntimeException implements ContainerExceptionInterface +{ + /** + * @param string $id Identifier of the frozen service + */ + public function __construct($id) + { + parent::__construct(\sprintf('Cannot override frozen service "%s".', $id)); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php b/vendor/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php new file mode 100644 index 0000000..91e82f9 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php @@ -0,0 +1,45 @@ + + */ +class InvalidServiceIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + /** + * @param string $id The invalid identifier + */ + public function __construct($id) + { + parent::__construct(\sprintf('Identifier "%s" does not contain an object definition.', $id)); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php b/vendor/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php new file mode 100644 index 0000000..fb6b626 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php @@ -0,0 +1,45 @@ + + */ +class UnknownIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + /** + * @param string $id The unknown identifier + */ + public function __construct($id) + { + parent::__construct(\sprintf('Identifier "%s" is not defined.', $id)); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Psr11/Container.php b/vendor/pimple/pimple/src/Pimple/Psr11/Container.php new file mode 100644 index 0000000..e18592e --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Psr11/Container.php @@ -0,0 +1,55 @@ + + */ +final class Container implements ContainerInterface +{ + private $pimple; + + public function __construct(PimpleContainer $pimple) + { + $this->pimple = $pimple; + } + + public function get(string $id) + { + return $this->pimple[$id]; + } + + public function has(string $id): bool + { + return isset($this->pimple[$id]); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php b/vendor/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php new file mode 100644 index 0000000..714b882 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php @@ -0,0 +1,75 @@ + + */ +class ServiceLocator implements ContainerInterface +{ + private $container; + private $aliases = []; + + /** + * @param PimpleContainer $container The Container instance used to locate services + * @param array $ids Array of service ids that can be located. String keys can be used to define aliases + */ + public function __construct(PimpleContainer $container, array $ids) + { + $this->container = $container; + + foreach ($ids as $key => $id) { + $this->aliases[\is_int($key) ? $id : $key] = $id; + } + } + + /** + * {@inheritdoc} + */ + public function get(string $id) + { + if (!isset($this->aliases[$id])) { + throw new UnknownIdentifierException($id); + } + + return $this->container[$this->aliases[$id]]; + } + + /** + * {@inheritdoc} + */ + public function has(string $id): bool + { + return isset($this->aliases[$id]) && isset($this->container[$this->aliases[$id]]); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/ServiceIterator.php b/vendor/pimple/pimple/src/Pimple/ServiceIterator.php new file mode 100644 index 0000000..ebafac1 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/ServiceIterator.php @@ -0,0 +1,89 @@ + + */ +final class ServiceIterator implements \Iterator +{ + private $container; + private $ids; + + public function __construct(Container $container, array $ids) + { + $this->container = $container; + $this->ids = $ids; + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + \reset($this->ids); + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->container[\current($this->ids)]; + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return \current($this->ids); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + \next($this->ids); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + return null !== \key($this->ids); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php b/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php new file mode 100644 index 0000000..abf90d8 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/ServiceProviderInterface.php @@ -0,0 +1,44 @@ +value = $value; + + return $service; + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php new file mode 100644 index 0000000..33cd4e5 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php @@ -0,0 +1,34 @@ +factory(function () { + return new Service(); + }); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php new file mode 100644 index 0000000..d71b184 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Service.php @@ -0,0 +1,35 @@ + + */ +class Service +{ + public $value; +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php b/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php new file mode 100644 index 0000000..097a7fd --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/PimpleServiceProviderInterfaceTest.php @@ -0,0 +1,77 @@ + + */ +class PimpleServiceProviderInterfaceTest extends TestCase +{ + public function testProvider() + { + $pimple = new Container(); + + $pimpleServiceProvider = new Fixtures\PimpleServiceProvider(); + $pimpleServiceProvider->register($pimple); + + $this->assertEquals('value', $pimple['param']); + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + + $serviceOne = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } + + public function testProviderWithRegisterMethod() + { + $pimple = new Container(); + + $pimple->register(new Fixtures\PimpleServiceProvider(), [ + 'anotherParameter' => 'anotherValue', + ]); + + $this->assertEquals('value', $pimple['param']); + $this->assertEquals('anotherValue', $pimple['anotherParameter']); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + + $serviceOne = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['factory']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php b/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php new file mode 100644 index 0000000..ffa50a6 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/PimpleTest.php @@ -0,0 +1,610 @@ + + */ +class PimpleTest extends TestCase +{ + public function testWithString() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + + $this->assertEquals('value', $pimple['param']); + } + + public function testWithClosure() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['service']); + } + + public function testServicesShouldBeDifferent() + { + $pimple = new Container(); + $pimple['service'] = $pimple->factory(function () { + return new Fixtures\Service(); + }); + + $serviceOne = $pimple['service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } + + public function testShouldPassContainerAsParameter() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $pimple['container'] = function ($container) { + return $container; + }; + + $this->assertNotSame($pimple, $pimple['service']); + $this->assertSame($pimple, $pimple['container']); + } + + public function testIsset() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + $pimple['null'] = null; + + $this->assertTrue(isset($pimple['param'])); + $this->assertTrue(isset($pimple['service'])); + $this->assertTrue(isset($pimple['null'])); + $this->assertFalse(isset($pimple['non_existent'])); + } + + public function testConstructorInjection() + { + $params = ['param' => 'value']; + $pimple = new Container($params); + + $this->assertSame($params['param'], $pimple['param']); + } + + public function testOffsetGetValidatesKeyIsPresent() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + echo $pimple['foo']; + } + + /** + * @group legacy + */ + public function testLegacyOffsetGetValidatesKeyIsPresent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + echo $pimple['foo']; + } + + public function testOffsetGetHonorsNullValues() + { + $pimple = new Container(); + $pimple['foo'] = null; + $this->assertNull($pimple['foo']); + } + + public function testUnset() + { + $pimple = new Container(); + $pimple['param'] = 'value'; + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + + unset($pimple['param'], $pimple['service']); + $this->assertFalse(isset($pimple['param'])); + $this->assertFalse(isset($pimple['service'])); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testShare($service) + { + $pimple = new Container(); + $pimple['shared_service'] = $service; + + $serviceOne = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + + $this->assertSame($serviceOne, $serviceTwo); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testProtect($service) + { + $pimple = new Container(); + $pimple['protected'] = $pimple->protect($service); + + $this->assertSame($service, $pimple['protected']); + } + + public function testGlobalFunctionNameAsParameterValue() + { + $pimple = new Container(); + $pimple['global_function'] = 'strlen'; + $this->assertSame('strlen', $pimple['global_function']); + } + + public function testRaw() + { + $pimple = new Container(); + $pimple['service'] = $definition = $pimple->factory(function () { + return 'foo'; + }); + $this->assertSame($definition, $pimple->raw('service')); + } + + public function testRawHonorsNullValues() + { + $pimple = new Container(); + $pimple['foo'] = null; + $this->assertNull($pimple->raw('foo')); + } + + public function testFluentRegister() + { + $pimple = new Container(); + $this->assertSame($pimple, $pimple->register($this->getMockBuilder('Pimple\ServiceProviderInterface')->getMock())); + } + + public function testRawValidatesKeyIsPresent() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple->raw('foo'); + } + + /** + * @group legacy + */ + public function testLegacyRawValidatesKeyIsPresent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple->raw('foo'); + } + + /** + * @dataProvider serviceDefinitionProvider + */ + public function testExtend($service) + { + $pimple = new Container(); + $pimple['shared_service'] = function () { + return new Fixtures\Service(); + }; + $pimple['factory_service'] = $pimple->factory(function () { + return new Fixtures\Service(); + }); + + $pimple->extend('shared_service', $service); + $serviceOne = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + $serviceTwo = $pimple['shared_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + $this->assertSame($serviceOne, $serviceTwo); + $this->assertSame($serviceOne->value, $serviceTwo->value); + + $pimple->extend('factory_service', $service); + $serviceOne = $pimple['factory_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceOne); + $serviceTwo = $pimple['factory_service']; + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $serviceTwo); + $this->assertNotSame($serviceOne, $serviceTwo); + $this->assertNotSame($serviceOne->value, $serviceTwo->value); + } + + public function testExtendDoesNotLeakWithFactories() + { + if (\extension_loaded('pimple')) { + $this->markTestSkipped('Pimple extension does not support this test'); + } + $pimple = new Container(); + + $pimple['foo'] = $pimple->factory(function () { + return; + }); + $pimple['foo'] = $pimple->extend('foo', function ($foo, $pimple) { + return; + }); + unset($pimple['foo']); + + $p = new \ReflectionProperty($pimple, 'values'); + $p->setAccessible(true); + $this->assertEmpty($p->getValue($pimple)); + + $p = new \ReflectionProperty($pimple, 'factories'); + $p->setAccessible(true); + $this->assertCount(0, $p->getValue($pimple)); + } + + public function testExtendValidatesKeyIsPresent() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple->extend('foo', function () { + }); + } + + /** + * @group legacy + */ + public function testLegacyExtendValidatesKeyIsPresent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple->extend('foo', function () { + }); + } + + public function testKeys() + { + $pimple = new Container(); + $pimple['foo'] = 123; + $pimple['bar'] = 123; + + $this->assertEquals(['foo', 'bar'], $pimple->keys()); + } + + /** @test */ + public function settingAnInvokableObjectShouldTreatItAsFactory() + { + $pimple = new Container(); + $pimple['invokable'] = new Fixtures\Invokable(); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\Service', $pimple['invokable']); + } + + /** @test */ + public function settingNonInvokableObjectShouldTreatItAsParameter() + { + $pimple = new Container(); + $pimple['non_invokable'] = new Fixtures\NonInvokable(); + + $this->assertInstanceOf('Pimple\Tests\Fixtures\NonInvokable', $pimple['non_invokable']); + } + + /** + * @dataProvider badServiceDefinitionProvider + */ + public function testFactoryFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\Pimple\Exception\ExpectedInvokableException::class); + $this->expectExceptionMessage('Service definition is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple->factory($service); + } + + /** + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyFactoryFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Service definition is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple->factory($service); + } + + /** + * @dataProvider badServiceDefinitionProvider + */ + public function testProtectFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\Pimple\Exception\ExpectedInvokableException::class); + $this->expectExceptionMessage('Callable is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple->protect($service); + } + + /** + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyProtectFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Callable is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple->protect($service); + } + + /** + * @dataProvider badServiceDefinitionProvider + */ + public function testExtendFailsForKeysNotContainingServiceDefinitions($service) + { + $this->expectException(\Pimple\Exception\InvalidServiceIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" does not contain an object definition.'); + + $pimple = new Container(); + $pimple['foo'] = $service; + $pimple->extend('foo', function () { + }); + } + + /** + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyExtendFailsForKeysNotContainingServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" does not contain an object definition.'); + + $pimple = new Container(); + $pimple['foo'] = $service; + $pimple->extend('foo', function () { + }); + } + + /** + * @group legacy + * @expectedDeprecation How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "foo" should be protected? + */ + public function testExtendingProtectedClosureDeprecation() + { + $pimple = new Container(); + $pimple['foo'] = $pimple->protect(function () { + return 'bar'; + }); + + $pimple->extend('foo', function ($value) { + return $value.'-baz'; + }); + + $this->assertSame('bar-baz', $pimple['foo']); + } + + /** + * @dataProvider badServiceDefinitionProvider + */ + public function testExtendFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\Pimple\Exception\ExpectedInvokableException::class); + $this->expectExceptionMessage('Extension service definition is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple['foo'] = function () { + }; + $pimple->extend('foo', $service); + } + + /** + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyExtendFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Extension service definition is not a Closure or invokable object.'); + + $pimple = new Container(); + $pimple['foo'] = function () { + }; + $pimple->extend('foo', $service); + } + + public function testExtendFailsIfFrozenServiceIsNonInvokable() + { + $this->expectException(\Pimple\Exception\FrozenServiceException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $pimple = new Container(); + $pimple['foo'] = function () { + return new Fixtures\NonInvokable(); + }; + $foo = $pimple['foo']; + + $pimple->extend('foo', function () { + }); + } + + public function testExtendFailsIfFrozenServiceIsInvokable() + { + $this->expectException(\Pimple\Exception\FrozenServiceException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $pimple = new Container(); + $pimple['foo'] = function () { + return new Fixtures\Invokable(); + }; + $foo = $pimple['foo']; + + $pimple->extend('foo', function () { + }); + } + + /** + * Provider for invalid service definitions. + */ + public function badServiceDefinitionProvider() + { + return [ + [123], + [new Fixtures\NonInvokable()], + ]; + } + + /** + * Provider for service definitions. + */ + public function serviceDefinitionProvider() + { + return [ + [function ($value) { + $service = new Fixtures\Service(); + $service->value = $value; + + return $service; + }], + [new Fixtures\Invokable()], + ]; + } + + public function testDefiningNewServiceAfterFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + $pimple['bar'] = function () { + return 'bar'; + }; + $this->assertSame('bar', $pimple['bar']); + } + + public function testOverridingServiceAfterFreeze() + { + $this->expectException(\Pimple\Exception\FrozenServiceException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + $pimple['foo'] = function () { + return 'bar'; + }; + } + + /** + * @group legacy + */ + public function testLegacyOverridingServiceAfterFreeze() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + $pimple['foo'] = function () { + return 'bar'; + }; + } + + public function testRemovingServiceAfterFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $foo = $pimple['foo']; + + unset($pimple['foo']); + $pimple['foo'] = function () { + return 'bar'; + }; + $this->assertSame('bar', $pimple['foo']); + } + + public function testExtendingService() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) { + return "$foo.bar"; + }); + $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) { + return "$foo.baz"; + }); + $this->assertSame('foo.bar.baz', $pimple['foo']); + } + + public function testExtendingServiceAfterOtherServiceFreeze() + { + $pimple = new Container(); + $pimple['foo'] = function () { + return 'foo'; + }; + $pimple['bar'] = function () { + return 'bar'; + }; + $foo = $pimple['foo']; + + $pimple['bar'] = $pimple->extend('bar', function ($bar, $app) { + return "$bar.baz"; + }); + $this->assertSame('bar.baz', $pimple['bar']); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ContainerTest.php b/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ContainerTest.php new file mode 100644 index 0000000..d47b9c3 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ContainerTest.php @@ -0,0 +1,76 @@ +assertSame($pimple['service'], $psr->get('service')); + } + + public function testGetThrowsExceptionIfServiceIsNotFound() + { + $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); + $this->expectExceptionMessage('Identifier "service" is not defined.'); + + $pimple = new Container(); + $psr = new PsrContainer($pimple); + + $psr->get('service'); + } + + public function testHasReturnsTrueIfServiceExists() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Service(); + }; + $psr = new PsrContainer($pimple); + + $this->assertTrue($psr->has('service')); + } + + public function testHasReturnsFalseIfServiceDoesNotExist() + { + $pimple = new Container(); + $psr = new PsrContainer($pimple); + + $this->assertFalse($psr->has('service')); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ServiceLocatorTest.php b/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ServiceLocatorTest.php new file mode 100644 index 0000000..bd2d335 --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/Psr11/ServiceLocatorTest.php @@ -0,0 +1,131 @@ + + */ +class ServiceLocatorTest extends TestCase +{ + public function testCanAccessServices() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['service']); + + $this->assertSame($pimple['service'], $locator->get('service')); + } + + public function testCanAccessAliasedServices() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['alias' => 'service']); + + $this->assertSame($pimple['service'], $locator->get('alias')); + } + + public function testCannotAccessAliasedServiceUsingRealIdentifier() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "service" is not defined.'); + + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['alias' => 'service']); + + $service = $locator->get('service'); + } + + public function testGetValidatesServiceCanBeLocated() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['alias' => 'service']); + + $service = $locator->get('foo'); + } + + public function testGetValidatesTargetServiceExists() + { + $this->expectException(\Pimple\Exception\UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "invalid" is not defined.'); + + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['alias' => 'invalid']); + + $service = $locator->get('alias'); + } + + public function testHasValidatesServiceCanBeLocated() + { + $pimple = new Container(); + $pimple['service1'] = function () { + return new Fixtures\Service(); + }; + $pimple['service2'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['service1']); + + $this->assertTrue($locator->has('service1')); + $this->assertFalse($locator->has('service2')); + } + + public function testHasChecksIfTargetServiceExists() + { + $pimple = new Container(); + $pimple['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($pimple, ['foo' => 'service', 'bar' => 'invalid']); + + $this->assertTrue($locator->has('foo')); + $this->assertFalse($locator->has('bar')); + } +} diff --git a/vendor/pimple/pimple/src/Pimple/Tests/ServiceIteratorTest.php b/vendor/pimple/pimple/src/Pimple/Tests/ServiceIteratorTest.php new file mode 100644 index 0000000..2bb935f --- /dev/null +++ b/vendor/pimple/pimple/src/Pimple/Tests/ServiceIteratorTest.php @@ -0,0 +1,52 @@ +assertSame(['service1' => $pimple['service1'], 'service2' => $pimple['service2']], iterator_to_array($iterator)); + } +} diff --git a/vendor/psr/container/.gitignore b/vendor/psr/container/.gitignore new file mode 100644 index 0000000..b2395aa --- /dev/null +++ b/vendor/psr/container/.gitignore @@ -0,0 +1,3 @@ +composer.lock +composer.phar +/vendor/ diff --git a/vendor/psr/container/LICENSE b/vendor/psr/container/LICENSE new file mode 100644 index 0000000..2877a48 --- /dev/null +++ b/vendor/psr/container/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2016 container-interop +Copyright (c) 2016 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/psr/container/README.md b/vendor/psr/container/README.md new file mode 100644 index 0000000..1b9d9e5 --- /dev/null +++ b/vendor/psr/container/README.md @@ -0,0 +1,13 @@ +Container interface +============== + +This repository holds all interfaces related to [PSR-11 (Container Interface)][psr-url]. + +Note that this is not a Container implementation of its own. It is merely abstractions that describe the components of a Dependency Injection Container. + +The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. + +[psr-url]: https://www.php-fig.org/psr/psr-11/ +[package-url]: https://packagist.org/packages/psr/container +[implementation-url]: https://packagist.org/providers/psr/container-implementation + diff --git a/vendor/psr/container/composer.json b/vendor/psr/container/composer.json new file mode 100644 index 0000000..017f41e --- /dev/null +++ b/vendor/psr/container/composer.json @@ -0,0 +1,22 @@ +{ + "name": "psr/container", + "type": "library", + "description": "Common Container Interface (PHP FIG PSR-11)", + "keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"], + "homepage": "https://github.com/php-fig/container", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=7.4.0" + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + } +} diff --git a/vendor/psr/container/src/ContainerExceptionInterface.php b/vendor/psr/container/src/ContainerExceptionInterface.php new file mode 100644 index 0000000..0f213f2 --- /dev/null +++ b/vendor/psr/container/src/ContainerExceptionInterface.php @@ -0,0 +1,12 @@ +=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/http-message/src/MessageInterface.php b/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 0000000..dd46e5e --- /dev/null +++ b/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,187 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(); + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name); + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name); + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value); + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value); + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader($name); + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(); + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body); +} diff --git a/vendor/psr/http-message/src/RequestInterface.php b/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 0000000..a96d4fd --- /dev/null +++ b/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,129 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(); + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query); + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(); + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles); + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data); + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(); + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute($name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute($name, $value); + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute($name); +} diff --git a/vendor/psr/http-message/src/StreamInterface.php b/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 0000000..f68f391 --- /dev/null +++ b/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,158 @@ + + * [user-info@]host[:port] + *
+ * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(); + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(); + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(); + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(); + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(); + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(); + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(); + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme($scheme); + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo($user, $password = null); + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost($host); + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort($port); + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath($path); + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery($query); + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment($fragment); + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(); +} diff --git a/vendor/psr/log/LICENSE b/vendor/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/vendor/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/psr/log/Psr/Log/AbstractLogger.php b/vendor/psr/log/Psr/Log/AbstractLogger.php new file mode 100644 index 0000000..e02f9da --- /dev/null +++ b/vendor/psr/log/Psr/Log/AbstractLogger.php @@ -0,0 +1,128 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } +} diff --git a/vendor/psr/log/Psr/Log/InvalidArgumentException.php b/vendor/psr/log/Psr/Log/InvalidArgumentException.php new file mode 100644 index 0000000..67f852d --- /dev/null +++ b/vendor/psr/log/Psr/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +logger = $logger; + } +} diff --git a/vendor/psr/log/Psr/Log/LoggerInterface.php b/vendor/psr/log/Psr/Log/LoggerInterface.php new file mode 100644 index 0000000..2206cfd --- /dev/null +++ b/vendor/psr/log/Psr/Log/LoggerInterface.php @@ -0,0 +1,125 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, $message, array $context = array()); +} diff --git a/vendor/psr/log/Psr/Log/NullLogger.php b/vendor/psr/log/Psr/Log/NullLogger.php new file mode 100644 index 0000000..c8f7293 --- /dev/null +++ b/vendor/psr/log/Psr/Log/NullLogger.php @@ -0,0 +1,30 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/vendor/psr/log/Psr/Log/Test/DummyTest.php b/vendor/psr/log/Psr/Log/Test/DummyTest.php new file mode 100644 index 0000000..9638c11 --- /dev/null +++ b/vendor/psr/log/Psr/Log/Test/DummyTest.php @@ -0,0 +1,18 @@ + ". + * + * Example ->error('Foo') would yield "error Foo". + * + * @return string[] + */ + abstract public function getLogs(); + + public function testImplements() + { + $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger()); + } + + /** + * @dataProvider provideLevelsAndMessages + */ + public function testLogsAtAllLevels($level, $message) + { + $logger = $this->getLogger(); + $logger->{$level}($message, array('user' => 'Bob')); + $logger->log($level, $message, array('user' => 'Bob')); + + $expected = array( + $level.' message of level '.$level.' with context: Bob', + $level.' message of level '.$level.' with context: Bob', + ); + $this->assertEquals($expected, $this->getLogs()); + } + + public function provideLevelsAndMessages() + { + return array( + LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), + LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), + LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), + LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), + LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), + LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), + LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), + LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), + ); + } + + /** + * @expectedException \Psr\Log\InvalidArgumentException + */ + public function testThrowsOnInvalidLevel() + { + $logger = $this->getLogger(); + $logger->log('invalid level', 'Foo'); + } + + public function testContextReplacement() + { + $logger = $this->getLogger(); + $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); + + $expected = array('info {Message {nothing} Bob Bar a}'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testObjectCastToString() + { + if (method_exists($this, 'createPartialMock')) { + $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString')); + } else { + $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString')); + } + $dummy->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('DUMMY')); + + $this->getLogger()->warning($dummy); + + $expected = array('warning DUMMY'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextCanContainAnything() + { + $closed = fopen('php://memory', 'r'); + fclose($closed); + + $context = array( + 'bool' => true, + 'null' => null, + 'string' => 'Foo', + 'int' => 0, + 'float' => 0.5, + 'nested' => array('with object' => new DummyTest), + 'object' => new \DateTime, + 'resource' => fopen('php://memory', 'r'), + 'closed' => $closed, + ); + + $this->getLogger()->warning('Crazy context data', $context); + + $expected = array('warning Crazy context data'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextExceptionKeyCanBeExceptionOrOtherValues() + { + $logger = $this->getLogger(); + $logger->warning('Random message', array('exception' => 'oops')); + $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); + + $expected = array( + 'warning Random message', + 'critical Uncaught Exception!' + ); + $this->assertEquals($expected, $this->getLogs()); + } +} diff --git a/vendor/psr/log/Psr/Log/Test/TestLogger.php b/vendor/psr/log/Psr/Log/Test/TestLogger.php new file mode 100644 index 0000000..1be3230 --- /dev/null +++ b/vendor/psr/log/Psr/Log/Test/TestLogger.php @@ -0,0 +1,147 @@ + $level, + 'message' => $message, + 'context' => $context, + ]; + + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + public function hasRecords($level) + { + return isset($this->recordsByLevel[$level]); + } + + public function hasRecord($record, $level) + { + if (is_string($record)) { + $record = ['message' => $record]; + } + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + return true; + }, $level); + } + + public function hasRecordThatContains($message, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($message) { + return strpos($rec['message'], $message) !== false; + }, $level); + } + + public function hasRecordThatMatches($regex, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($regex) { + return preg_match($regex, $rec['message']) > 0; + }, $level); + } + + public function hasRecordThatPasses(callable $predicate, $level) + { + if (!isset($this->recordsByLevel[$level])) { + return false; + } + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if (call_user_func($predicate, $rec, $i)) { + return true; + } + } + return false; + } + + public function __call($method, $args) + { + if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { + $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; + $level = strtolower($matches[2]); + if (method_exists($this, $genericMethod)) { + $args[] = $level; + return call_user_func_array([$this, $genericMethod], $args); + } + } + throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); + } + + public function reset() + { + $this->records = []; + $this->recordsByLevel = []; + } +} diff --git a/vendor/psr/log/README.md b/vendor/psr/log/README.md new file mode 100644 index 0000000..a9f20c4 --- /dev/null +++ b/vendor/psr/log/README.md @@ -0,0 +1,58 @@ +PSR Log +======= + +This repository holds all interfaces/classes/traits related to +[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +Note that this is not a logger of its own. It is merely an interface that +describes a logger. See the specification for more details. + +Installation +------------ + +```bash +composer require psr/log +``` + +Usage +----- + +If you need a logger, you can use the interface like this: + +```php +logger = $logger; + } + + public function doSomething() + { + if ($this->logger) { + $this->logger->info('Doing work'); + } + + try { + $this->doSomethingElse(); + } catch (Exception $exception) { + $this->logger->error('Oh no!', array('exception' => $exception)); + } + + // do something useful + } +} +``` + +You can then pick one of the implementations of the interface to get a logger. + +If you want to implement the interface, you can require this package and +implement `Psr\Log\LoggerInterface` in your code. Please read the +[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) +for details. diff --git a/vendor/psr/log/composer.json b/vendor/psr/log/composer.json new file mode 100644 index 0000000..ca05695 --- /dev/null +++ b/vendor/psr/log/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/log", + "description": "Common interface for logging libraries", + "keywords": ["psr", "psr-3", "log"], + "homepage": "https://github.com/php-fig/log", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + } +} diff --git a/vendor/psr/simple-cache/.editorconfig b/vendor/psr/simple-cache/.editorconfig new file mode 100644 index 0000000..48542cb --- /dev/null +++ b/vendor/psr/simple-cache/.editorconfig @@ -0,0 +1,12 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/vendor/psr/simple-cache/LICENSE.md b/vendor/psr/simple-cache/LICENSE.md new file mode 100644 index 0000000..e49a7c8 --- /dev/null +++ b/vendor/psr/simple-cache/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2016 PHP Framework Interoperability Group + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/vendor/psr/simple-cache/README.md b/vendor/psr/simple-cache/README.md new file mode 100644 index 0000000..43641d1 --- /dev/null +++ b/vendor/psr/simple-cache/README.md @@ -0,0 +1,8 @@ +PHP FIG Simple Cache PSR +======================== + +This repository holds all interfaces related to PSR-16. + +Note that this is not a cache implementation of its own. It is merely an interface that describes a cache implementation. See [the specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md) for more details. + +You can find implementations of the specification by looking for packages providing the [psr/simple-cache-implementation](https://packagist.org/providers/psr/simple-cache-implementation) virtual package. diff --git a/vendor/psr/simple-cache/composer.json b/vendor/psr/simple-cache/composer.json new file mode 100644 index 0000000..2978fa5 --- /dev/null +++ b/vendor/psr/simple-cache/composer.json @@ -0,0 +1,25 @@ +{ + "name": "psr/simple-cache", + "description": "Common interfaces for simple caching", + "keywords": ["psr", "psr-16", "cache", "simple-cache", "caching"], + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/simple-cache/src/CacheException.php b/vendor/psr/simple-cache/src/CacheException.php new file mode 100644 index 0000000..eba5381 --- /dev/null +++ b/vendor/psr/simple-cache/src/CacheException.php @@ -0,0 +1,10 @@ + value pairs. Cache keys that do not exist or are stale will have $default as value. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function getMultiple($keys, $default = null); + + /** + * Persists a set of key => value pairs in the cache, with an optional TTL. + * + * @param iterable $values A list of key => value pairs for a multiple-set operation. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $values is neither an array nor a Traversable, + * or if any of the $values are not a legal value. + */ + public function setMultiple($values, $ttl = null); + + /** + * Deletes multiple cache items in a single operation. + * + * @param iterable $keys A list of string-based keys to be deleted. + * + * @return bool True if the items were successfully removed. False if there was an error. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if $keys is neither an array nor a Traversable, + * or if any of the $keys are not a legal value. + */ + public function deleteMultiple($keys); + + /** + * Determines whether an item is present in the cache. + * + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * + * @return bool + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function has($key); +} diff --git a/vendor/psr/simple-cache/src/InvalidArgumentException.php b/vendor/psr/simple-cache/src/InvalidArgumentException.php new file mode 100644 index 0000000..6a9524a --- /dev/null +++ b/vendor/psr/simple-cache/src/InvalidArgumentException.php @@ -0,0 +1,13 @@ +). + + +## Misc + +### Version History + +Please read the [release notes](https://github.com/cakephp/phinx/releases). + +### License + +(The MIT license) + +Copyright (c) 2017 Rob Morgan + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/robmorgan/phinx/app/phinx.php b/vendor/robmorgan/phinx/app/phinx.php new file mode 100644 index 0000000..b03ef7e --- /dev/null +++ b/vendor/robmorgan/phinx/app/phinx.php @@ -0,0 +1,36 @@ + 'getStatus', + 'migrate' => 'getMigrate', + 'rollback' => 'getRollback', +]; + +// Extract the requested command from the URL, default to "status". +$command = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'); +if (!$command) { + $command = 'status'; +} + +// Verify that the command exists, or list available commands. +if (!isset($routes[$command])) { + $commands = implode(', ', array_keys($routes)); + header('Content-Type: text/plain', true, 404); + die("Command not found! Valid commands are: {$commands}."); +} + +// Get the environment and target version parameters. +$env = $_GET['e'] ?? null; +$target = $_GET['t'] ?? null; + +// Check if debugging is enabled. +$debug = !empty($_GET['debug']) && filter_var($_GET['debug'], FILTER_VALIDATE_BOOLEAN); + +// Execute the command and determine if it was successful. +$output = call_user_func([$wrap, $routes[$command]], $env, $target); +$error = $wrap->getExitCode() > 0; + +// Finally, display the output of the command. +header('Content-Type: text/plain', true, $error ? 500 : 200); +if ($debug) { + // Show what command was executed based on request parameters. + $args = implode(', ', [var_export($env, true), var_export($target, true)]); + echo "DEBUG: $command($args)" . PHP_EOL . PHP_EOL; +} +echo $output; diff --git a/vendor/robmorgan/phinx/bin/phinx b/vendor/robmorgan/phinx/bin/phinx new file mode 100644 index 0000000..ac37175 --- /dev/null +++ b/vendor/robmorgan/phinx/bin/phinx @@ -0,0 +1,28 @@ +#!/usr/bin/env php +run(); diff --git a/vendor/robmorgan/phinx/bin/phinx.bat b/vendor/robmorgan/phinx/bin/phinx.bat new file mode 100644 index 0000000..e7d7e7d --- /dev/null +++ b/vendor/robmorgan/phinx/bin/phinx.bat @@ -0,0 +1,41 @@ +@echo off + +rem This script will do the following: +rem - check for PHP_COMMAND env, if found, use it. +rem - if not found detect php, if found use it, otherwise err and terminate + +if "%OS%"=="Windows_NT" @setlocal + +rem %~dp0 is expanded pathname of the current script under NT +set DEFAULT_PHINX_HOME=%~dp0.. + +goto init +goto cleanup + +:init + +if "%PHINX_HOME%" == "" set PHINX_HOME=%DEFAULT_PHINX_HOME% +set DEFAULT_PHINX_HOME= + +if "%PHP_COMMAND%" == "" goto no_phpcommand + +goto run +goto cleanup + +:run +"%PHP_COMMAND%" -d html_errors=off -qC "%PHINX_HOME%\bin\phinx" %* +goto cleanup + +:no_phpcommand +rem PHP_COMMAND environment variable not found, assuming php.exe is on path. +set PHP_COMMAND=php.exe +goto init + +:err_home +echo ERROR: Environment var PHINX_HOME not set. Please point this +echo variable to your local phinx installation! +goto cleanup + +:cleanup +if "%OS%"=="Windows_NT" @endlocal +rem pause \ No newline at end of file diff --git a/vendor/robmorgan/phinx/composer.json b/vendor/robmorgan/phinx/composer.json new file mode 100644 index 0000000..8a1b680 --- /dev/null +++ b/vendor/robmorgan/phinx/composer.json @@ -0,0 +1,69 @@ +{ + "name": "robmorgan/phinx", + "type": "library", + "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.", + "keywords": ["phinx", "migrations", "database", "db", "database migrations"], + "homepage": "https://phinx.org", + "license": "MIT", + "authors": [{ + "name": "Rob Morgan", + "email": "robbym@gmail.com", + "homepage": "https://robmorgan.id.au", + "role": "Lead Developer" + }, { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://shadowhand.me", + "role": "Developer" + }, { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Developer" + }, { + "name": "CakePHP Community", + "role": "Developer", + "homepage": "https://github.com/cakephp/phinx/graphs/contributors" + }], + "require": { + "php": ">=7.2", + "cakephp/database": "^4.0", + "psr/container": "^1.0 || ^2.0", + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/config": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "ext-json": "*", + "ext-pdo": "*", + "phpunit/phpunit": "^8.5|^9.3", + "sebastian/comparator": ">=1.2.3", + "cakephp/cakephp-codesniffer": "^4.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "autoload": { + "psr-4": { + "Phinx\\": "src/Phinx/" + } + }, + "autoload-dev": { + "psr-4": { + "Test\\Phinx\\": "tests/Phinx/" + } + }, + "suggest": { + "ext-json": "Install if using JSON configuration format", + "ext-pdo": "PDO extension is needed", + "symfony/yaml": "Install if using YAML configuration format" + }, + "scripts": { + "check": [ + "@test", + "@cs-check" + ], + "cs-check": "phpcs -np app/ src/ tests/", + "cs-fix": "phpcbf -np app/ src/ tests/", + "stan": "phpstan analyse src/", + "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^0.12 && mv composer.backup composer.json", + "test": "phpunit --colors=always" + }, + "bin": ["bin/phinx"] +} diff --git a/vendor/robmorgan/phinx/data/phinx.json.dist b/vendor/robmorgan/phinx/data/phinx.json.dist new file mode 100644 index 0000000..1deeaf1 --- /dev/null +++ b/vendor/robmorgan/phinx/data/phinx.json.dist @@ -0,0 +1,38 @@ +{ + "paths": { + "migrations": "%%PHINX_CONFIG_DIR%%/db/migrations", + "seeds": "%%PHINX_CONFIG_DIR%%/db/seeds" + }, + "environments": { + "default_migration_table": "phinxlog", + "default_environment": "development", + "production": { + "adapter": "mysql", + "host": "localhost", + "name": "production_db", + "user": "root", + "pass": "", + "port": 3306, + "charset": "utf8" + }, + "development": { + "adapter": "mysql", + "host": "localhost", + "name": "development_db", + "user": "root", + "pass": "", + "port": 3306, + "charset": "utf8" + }, + "testing": { + "adapter": "mysql", + "host": "localhost", + "name": "testing_db", + "user": "root", + "pass": "", + "port": 3306, + "charset": "utf8" + } + }, + "version_order": "creation" +} diff --git a/vendor/robmorgan/phinx/data/phinx.php.dist b/vendor/robmorgan/phinx/data/phinx.php.dist new file mode 100644 index 0000000..7ff2b08 --- /dev/null +++ b/vendor/robmorgan/phinx/data/phinx.php.dist @@ -0,0 +1,41 @@ + [ + 'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations', + 'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds' + ], + 'environments' => [ + 'default_migration_table' => 'phinxlog', + 'default_environment' => 'development', + 'production' => [ + 'adapter' => 'mysql', + 'host' => 'localhost', + 'name' => 'production_db', + 'user' => 'root', + 'pass' => '', + 'port' => '3306', + 'charset' => 'utf8', + ], + 'development' => [ + 'adapter' => 'mysql', + 'host' => 'localhost', + 'name' => 'development_db', + 'user' => 'root', + 'pass' => '', + 'port' => '3306', + 'charset' => 'utf8', + ], + 'testing' => [ + 'adapter' => 'mysql', + 'host' => 'localhost', + 'name' => 'testing_db', + 'user' => 'root', + 'pass' => '', + 'port' => '3306', + 'charset' => 'utf8', + ] + ], + 'version_order' => 'creation' +]; diff --git a/vendor/robmorgan/phinx/data/phinx.yml.dist b/vendor/robmorgan/phinx/data/phinx.yml.dist new file mode 100644 index 0000000..857e535 --- /dev/null +++ b/vendor/robmorgan/phinx/data/phinx.yml.dist @@ -0,0 +1,35 @@ +paths: + migrations: '%%PHINX_CONFIG_DIR%%/db/migrations' + seeds: '%%PHINX_CONFIG_DIR%%/db/seeds' + +environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + host: localhost + name: production_db + user: root + pass: '' + port: 3306 + charset: utf8 + + development: + adapter: mysql + host: localhost + name: development_db + user: root + pass: '' + port: 3306 + charset: utf8 + + testing: + adapter: mysql + host: localhost + name: testing_db + user: root + pass: '' + port: 3306 + charset: utf8 + +version_order: creation diff --git a/vendor/robmorgan/phinx/docs/config/__init__.py b/vendor/robmorgan/phinx/docs/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/robmorgan/phinx/docs/config/all.py b/vendor/robmorgan/phinx/docs/config/all.py new file mode 100644 index 0000000..0276971 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/config/all.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Phinx documentation build configuration file, created by +# sphinx-quickstart on Thu Jun 14 17:39:42 2012. +# + +# Import the base theme configuration +from cakephpsphinx.config.all import * + +# The full version, including alpha/beta/rc tags. +release = '0.12.x' + +# The search index version. +search_version = 'phinx-0' + +# The marketing display name for the book. +version_name = '' + +# Project name shown in the black header bar +project = 'Phinx' + +# Other versions that display in the version picker menu. +version_list = [ + {'name': '0.12', 'number': '/phinx/0', 'title': '0.12', 'current': True}, +] + +# Languages available. +languages = ['en', 'es', 'fr', 'ja'] + +# The GitHub branch name for this version of the docs +# for edit links to point at. +branch = 'master' + +# Current version being built +version = '0.12' + +show_root_link = True + +repository = 'cakephp/phinx' + +source_path = 'docs/' + +hide_page_contents = ('search', '404', 'contents') diff --git a/vendor/robmorgan/phinx/docs/en/commands.rst b/vendor/robmorgan/phinx/docs/en/commands.rst new file mode 100644 index 0000000..e93d7a7 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/commands.rst @@ -0,0 +1,411 @@ +.. index:: + single: Commands + +Commands +======== + +Phinx is run using a number of commands. + +The Breakpoint Command +---------------------- + +The Breakpoint command is used to set breakpoints, allowing you to limit +rollbacks. You can toggle the breakpoint of the most recent migration by +not supplying any parameters. + +.. code-block:: bash + + $ phinx breakpoint -e development + +To toggle a breakpoint on a specific version then use the ``--target`` +parameter or ``-t`` for short. + +.. code-block:: bash + + $ phinx breakpoint -e development -t 20120103083322 + +You can remove all the breakpoints by using the ``--remove-all`` parameter +or ``-r`` for short. + +.. code-block:: bash + + $ phinx breakpoint -e development -r + +You can set or unset (rather than just toggle) the breakpoint on the most +recent migration (or on a specific migration when combined with the +``--target`` or ``-t`` parameter) by using ``-set`` or ``--unset``. + +Breakpoints are visible when you run the ``status`` command. + +The Create Command +------------------ + +The Create command is used to create a new migration file. It requires one +argument: the name of the migration. The migration name should be specified in +CamelCase format. + +.. code-block:: bash + + $ phinx create MyNewMigration + +Open the new migration file in your text editor to add your database +transformations. Phinx creates migration files using the path specified in your +phinx configuration file. Please see the :doc:`Configuration ` chapter +for more information. + +You are able to override the template file used by Phinx by supplying an +alternative template filename. + +.. code-block:: bash + + $ phinx create MyNewMigration --template="" + +You can also supply a template generating class. This class must implement the +interface ``Phinx\Migration\CreationInterface``. + +.. code-block:: bash + + $ phinx create MyNewMigration --class="" + +In addition to providing the template for the migration, the class can also define +a callback that will be called once the migration file has been generated from the +template. + +You cannot use ``--template`` and ``--class`` together. + +The Init Command +---------------- + +The Init command (short for initialize) is used to prepare your project for +Phinx. This command generates the phinx configuration file in the root of your +project directory. By default, this file will be named ``phinx.php``. + +.. code-block:: bash + + $ phinx init + +Optionally you can specify a custom location for Phinx's config file: + +.. code-block:: bash + + $ phinx init ./custom/location/ + +You can also specify a custom file name: + +.. code-block:: bash + + $ phinx init custom-config.yml + +As well as a different format from php, yml, and json. For example, to create yml file: + +.. code-block:: bash + + $ phinx init --format yml + +Open this file in your text editor to setup your project configuration. Please +see the :doc:`Configuration ` chapter for more information. + +The Migrate Command +------------------- + +The Migrate command runs all of the available migrations, optionally up to a +specific version. + +.. code-block:: bash + + $ phinx migrate -e development + +To migrate to a specific version then use the ``--target`` parameter or ``-t`` +for short. + +.. code-block:: bash + + $ phinx migrate -e development -t 20110103081132 + +Use ``--dry-run`` to print the queries to standard output without executing them + +.. code-block:: bash + + $ phinx migrate --dry-run + +The Rollback Command +-------------------- + +The Rollback command is used to undo previous migrations executed by Phinx. It +is the opposite of the Migrate command. + +You can rollback to the previous migration by using the ``rollback`` command +with no arguments. + +.. code-block:: bash + + $ phinx rollback -e development + +To rollback all migrations to a specific version then use the ``--target`` +parameter or ``-t`` for short. + +.. code-block:: bash + + $ phinx rollback -e development -t 20120103083322 + +Specifying 0 as the target version will revert all migrations. + +.. code-block:: bash + + $ phinx rollback -e development -t 0 + +To rollback all migrations to a specific date then use the ``--date`` +parameter or ``-d`` for short. + +.. code-block:: bash + + $ phinx rollback -e development -d 2012 + $ phinx rollback -e development -d 201201 + $ phinx rollback -e development -d 20120103 + $ phinx rollback -e development -d 2012010312 + $ phinx rollback -e development -d 201201031205 + $ phinx rollback -e development -d 20120103120530 + +If a breakpoint is set, blocking further rollbacks, you can override the +breakpoint using the ``--force`` parameter or ``-f`` for short. + +.. code-block:: bash + + $ phinx rollback -e development -t 0 -f + +Use ``--dry-run`` to print the queries to standard output without executing them + +.. code-block:: bash + + $ phinx rollback --dry-run + +.. note:: + + When rolling back, Phinx orders the executed migrations using + the order specified in the ``version_order`` option of your + phinx configuration file. + Please see the :doc:`Configuration ` chapter for more information. + +The Status Command +------------------ + +The Status command prints a list of all migrations, along with their current +status. You can use this command to determine which migrations have been run. + +.. code-block:: bash + + $ phinx status -e development + +This command exits with code 0 if the database is up-to-date (ie. all migrations are up) or one of the following codes otherwise: + +* 2: There is at least one missing migration. +* 3: There is at least one down migration. + +An exit code of 1 means an application error has occurred. + +The Seed Create Command +----------------------- + +The Seed Create command can be used to create new database seed classes. It +requires one argument, the name of the class. The class name should be specified +in CamelCase format. + +.. code-block:: bash + + $ phinx seed:create MyNewSeeder + +Open the new seed file in your text editor to add your database seed commands. +Phinx creates seed files using the path specified in your configuration file. +Please see the :doc:`Configuration ` chapter for more information. + +You are able to override the template file used by Phinx by supplying an +alternative template filename. + +.. code-block:: bash + + $ phinx seed:create MyNewSeeder --template="" + +The Seed Run Command +-------------------- + +The Seed Run command runs all of the available seed classes or optionally just +one. + +.. code-block:: bash + + $ phinx seed:run -e development + +To run only one seed class use the ``--seed`` parameter or ``-s`` for short. + +.. code-block:: bash + + $ phinx seed:run -e development -s MyNewSeeder + +Configuration File Parameter +---------------------------- + +When running Phinx from the command line, you may specify a configuration file +using the ``--configuration`` or ``-c`` parameter. In addition to YAML, the +configuration file may be the computed output of a PHP file as a PHP array: + +.. code-block:: php + + [ + "migrations" => "application/migrations" + ], + "environments" => [ + "default_migration_table" => "phinxlog", + "default_environment" => "dev", + "dev" => [ + "adapter" => "mysql", + "host" => $_ENV['DB_HOST'], + "name" => $_ENV['DB_NAME'], + "user" => $_ENV['DB_USER'], + "pass" => $_ENV['DB_PASS'], + "port" => $_ENV['DB_PORT'] + ] + ] + ]; + +Phinx auto-detects which language parser to use for files with ``*.yaml``, ``*.yml``, ``*.json``, and ``*.php`` extensions. +The appropriate parser may also be specified via the ``--parser`` and ``-p`` parameters. Anything other than ``"json"`` or +``"php"`` is treated as YAML. + +When using a PHP array, you can provide a ``connection`` key with an existing PDO instance. It is also important to pass +the database name too, as Phinx requires this for certain methods such as ``hasTable()``: + +.. code-block:: php + + [ + "migrations" => "application/migrations" + ), + "environments" => [ + "default_migration_table" => "phinxlog", + "default_environment" => "dev", + "dev" => [ + "name" => "dev_db", + "connection" => $pdo_instance + ] + ] + ]; + +Running Phinx in a Web App +-------------------------- + +Phinx can also be run inside of a web application by using the ``Phinx\Wrapper\TextWrapper`` +class. An example of this is provided in ``app/web.php``, which can be run as a +standalone server: + +.. code-block:: bash + + $ php -S localhost:8000 vendor/robmorgan/phinx/app/web.php + +This will create local web server at ``__ which will show current +migration status by default. To run migrations up, use ``__ +and to rollback use ``__. + +**The included web app is only an example and should not be used in production!** + +.. note:: + + To modify configuration variables at runtime and override ``%%PHINX_DBNAME%%`` + or other another dynamic option, set ``$_SERVER['PHINX_DBNAME']`` before + running commands. Available options are documented in the Configuration page. + +Wrapping Phinx in another Symfony Console Application +----------------------------------------------------- + +Phinx can be wrapped and run as part of a separate Symfony console application. This +may be desirable to present a unified interface to the user for all aspects of your +application, or because you wish to run multiple Phinx commands. While you could +run the commands through ``exec`` or use the above ``Phinx\Wrapper\TextWrapper``, +though this makes it hard to deal with the return code and output in a similar fashion +as your application. + +Luckily, Symfony makes doing this sort of "meta" command straight-forward: + +.. code-block:: php + + use Symfony\Component\Console\Input\ArrayInput; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + use Phinx\Console\PhinxApplication; + + // ... + + protected function execute(InputInterface $input, OutputInterface $output) + { + + $phinx = new PhinxApplication(); + $command = $phinx->find('migrate'); + + $arguments = [ + 'command' => 'migrate', + '--environment' => 'production', + '--configuration' => '/path/to/phinx/config/file' + ]; + + $input = new ArrayInput($arguments); + $returnCode = $command->run(new ArrayInput($arguments), $output); + // ... + } + +Here, you are instantianting the ``PhinxApplication``, telling it to find the ``migrate`` +command, defining the arguments to pass to it (which match the commandline arguments and flags), +and then finally running the command, passing it the same ``OutputInterface`` that your +application uses. + +See this `Symfony page `_ for more information. + +Using Phinx with PHPUnit +-------------------------- + +Phinx can be used within your unit tests to prepare or seed the database. You can use it programatically : + +.. code-block:: php + + public function setUp () + { + $app = new PhinxApplication(); + $app->setAutoExit(false); + $app->run(new StringInput('migrate'), new NullOutput()); + } + +If you use a memory database, you'll need to give Phinx a specific PDO instance. You can interact with Phinx directly +using the Manager class : + +.. code-block:: php + + use PDO; + use Phinx\Config\Config; + use Phinx\Migration\Manager; + use PHPUnit\Framework\TestCase; + use Symfony\Component\Console\Input\StringInput; + use Symfony\Component\Console\Output\NullOutput; + + class DatabaseTestCase extends TestCase { + + public function setUp () + { + $pdo = new PDO('sqlite::memory:', null, null, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]); + $configArray = require('phinx.php'); + $configArray['environments']['test'] = [ + 'adapter' => 'sqlite', + 'connection' => $pdo + ]; + $config = new Config($configArray); + $manager = new Manager($config, new StringInput(' '), new NullOutput()); + $manager->migrate('test'); + $manager->seed('test'); + // You can change default fetch mode after the seeding + $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + $this->pdo = $pdo; + } + + } diff --git a/vendor/robmorgan/phinx/docs/en/conf.py b/vendor/robmorgan/phinx/docs/en/conf.py new file mode 100644 index 0000000..f638bda --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/conf.py @@ -0,0 +1,9 @@ +import sys, os + +# Append the top level directory of the docs, so we can import from the config dir. +sys.path.insert(0, os.path.abspath('..')) + +# Pull in all the configuration options defined in the global config file.. +from config.all import * + +language = 'en' diff --git a/vendor/robmorgan/phinx/docs/en/configuration.rst b/vendor/robmorgan/phinx/docs/en/configuration.rst new file mode 100644 index 0000000..3768293 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/configuration.rst @@ -0,0 +1,442 @@ +.. index:: + single: Configuration + +Configuration +============= + +When you initialize your project using the :doc:`Init Command`, Phinx +creates a default file in the root of your project directory. By default, this +file uses the YAML data serialization format, but you can use the ``--format`` +command line option to specify either ``yaml``, ``yml``, ``json``, or ``php``. + +If a ``--configuration`` command line option is given, Phinx will load the +specified file. Otherwise, it will attempt to find ``phinx.php``, ``phinx.json``, +``phinx.yml``, or ``phinx.yaml`` and load the first file found. See the +:doc:`Commands ` chapter for more information. + +.. warning:: + + Remember to store the configuration file outside of a publicly accessible + directory on your webserver. This file contains your database credentials + and may be accidentally served as plain text. + +Note that while JSON and YAML files are *parsed*, the PHP file is *included*. +This means that: + +* It must `return` an array of configuration items. +* The variable scope is local, i.e. you would need to explicitly declare + any global variables your initialization file reads or modifies. +* Its standard output is suppressed. +* Unlike with JSON and YAML, it is possible to omit environment connection details + and instead specify ``connection`` which must contain an initialized PDO instance. + This is useful when you want your migrations to interact with your application + and/or share the same connection. However remember to also pass the database name + as Phinx cannot infer this from the PDO connection. + +.. code-block:: php + + $app = require 'app/phinx.php'; + $pdo = $app->getDatabase()->getPdo(); + + return [ + 'environments' => [ + 'default_environment' => 'development', + 'development' => [ + 'name' => 'devdb', + 'connection' => $pdo + ] + ] + ]; + +Migration Paths +--------------- + +The first option specifies the path to your migration directory. Phinx uses +``%%PHINX_CONFIG_DIR%%/db/migrations`` by default. + +.. note:: + + ``%%PHINX_CONFIG_DIR%%`` is a special token and is automatically replaced + with the root directory where your phinx configuration file is stored. + +In order to overwrite the default ``%%PHINX_CONFIG_DIR%%/db/migrations``, you +need to add the following to the configuration. + +.. code-block:: yaml + + paths: + migrations: /your/full/path + +You can also provide multiple migration paths by using an array in your configuration: + +.. code-block:: yaml + + paths: + migrations: + - application/module1/migrations + - application/module2/migrations + + +You can also use the ``%%PHINX_CONFIG_DIR%%`` token in your path. + +.. code-block:: yaml + + paths: + migrations: '%%PHINX_CONFIG_DIR%%/your/relative/path' + +Migrations are captured with ``glob``, so you can define a pattern for multiple +directories. + +.. code-block:: yaml + + paths: + migrations: '%%PHINX_CONFIG_DIR%%/module/*/{data,scripts}/migrations' + +Custom Migration Base +--------------------- + +By default all migrations will extend from Phinx's `AbstractMigration` class. +This can be set to a custom class that extends from `AbstractMigration` by +setting ``migration_base_class`` in your config: + +.. code-block:: yaml + + migration_base_class: MyMagicalMigration + +Seed Paths +---------- + +The second option specifies the path to your seed directory. Phinx uses +``%%PHINX_CONFIG_DIR%%/db/seeds`` by default. + +.. note:: + + ``%%PHINX_CONFIG_DIR%%`` is a special token and is automatically replaced + with the root directory where your configuration file is stored. + +In order to overwrite the default ``%%PHINX_CONFIG_DIR%%/db/seeds``, you +need to add the following to the yaml configuration. + +.. code-block:: yaml + + paths: + seeds: /your/full/path + +You can also provide multiple seed paths by using an array in your configuration: + +.. code-block:: yaml + + paths: + seeds: + - /your/full/path1 + - /your/full/path2 + + +You can also use the ``%%PHINX_CONFIG_DIR%%`` token in your path. + +.. code-block:: yaml + + paths: + seeds: '%%PHINX_CONFIG_DIR%%/your/relative/path' + +Custom Seeder Base +--------------------- + +By default all seeders will extend from Phinx's `AbstractSeed` class. +This can be set to a custom class that extends from `AbstractSeed` by +setting ``seeder_base_class`` in your config: + +.. code-block:: yaml + + seeder_base_class: MyMagicalSeeder + +Environments +------------ + +One of the key features of Phinx is support for multiple database environments. +You can use Phinx to create migrations on your development environment, then +run the same migrations on your production environment. Environments are +specified under the ``environments`` nested collection. For example: + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + host: localhost + name: production_db + user: root + pass: '' + port: 3306 + charset: utf8 + collation: utf8_unicode_ci + +would define a new environment called ``production``. + +In a situation when multiple developers work on the same project and each has +a different environment (e.g. a convention such as ``--``), or when you need to have separate +environments for separate purposes (branches, testing, etc) use environment +variable `PHINX_ENVIRONMENT` to override the default environment in the yaml +file: + +.. code-block:: bash + + export PHINX_ENVIRONMENT=dev-`whoami`-`hostname` + +Migration Table +--------------- + +To keep track of the migration statuses for an environment, phinx creates +a table to store this information. You can customize where this table +is created by configuring ``default_migration_table``: + +.. code-block:: yaml + + environment: + default_migration_table: phinxlog + +If this field is omitted, then it will default to ``phinxlog``. For +databases that support it, e.g. Postgres, the schema name can be prefixed +with a period separator (``.``). For example, ``phinx.log`` will create +the table ``log`` in the ``phinx`` schema instead of ``phinxlog`` in the +``public`` (default) schema. + +Table Prefix and Suffix +----------------------- + +You can define a table prefix and table suffix: + +.. code-block:: yaml + + environments: + development: + .... + table_prefix: dev_ + table_suffix: _v1 + testing: + .... + table_prefix: test_ + table_suffix: _v2 + + +Socket Connections +------------------ + +When using the MySQL adapter, it is also possible to use sockets instead of +network connections. The socket path is configured with ``unix_socket``: + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + name: production_db + user: root + pass: '' + unix_socket: /var/run/mysql/mysql.sock + charset: utf8 + +External Variables +------------------ + +Phinx will automatically grab any environment variable prefixed with ``PHINX_`` +and make it available as a token in the config file. The token will have +exactly the same name as the variable but you must access it by wrapping two +``%%`` symbols on either side. e.g: ``'%%PHINX_DBUSER%%'``. This is especially +useful if you wish to store your secret database credentials directly on the +server and not in a version control system. This feature can be easily +demonstrated by the following example: + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + host: '%%PHINX_DBHOST%%' + name: '%%PHINX_DBNAME%%' + user: '%%PHINX_DBUSER%%' + pass: '%%PHINX_DBPASS%%' + port: 3306 + charset: utf8 + +Data Source Names +----------------- + +Phinx supports the use of data source names (DSN) to specify the connection +options, which can be useful if you use a single environment variable to hold +the database credentials. PDO has a different DSN formats depending on the +underlying driver, so Phinx uses a database-agnostic DSN format used by other +projects (Doctrine, Rails, AMQP, PaaS, etc). + +.. code-block:: text + + ://[[:]@][:]/[?] + +* A DSN requires at least ``adapter``, ``host`` and ``name``. +* You cannot specify a password without a username. +* ``port`` must be a positive integer. +* ``additionalOptions`` takes the form of a query string, and will be passed to + the adapter in the options array. + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + # Example data source name + dsn: mysql://root@localhost:3306/mydb?charset=utf8 + +Once a DSN is parsed, it's values are merged with the already existing +connection options. Values in specified in a DSN will never override any value +specified directly as connection options. + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + development: + dsn: %%DATABASE_URL%% + production: + dsn: %%DATABASE_URL%% + name: production_database + +If the supplied DSN is invalid, then it is completely ignored. + +Supported Adapters +------------------ + +Phinx currently supports the following database adapters natively: + +* `MySQL `_: specify the ``mysql`` adapter. +* `PostgreSQL `_: specify the ``pgsql`` adapter. +* `SQLite `_: specify the ``sqlite`` adapter. +* `SQL Server `_: specify the ``sqlsrv`` adapter. + +For each adapter, you may configure the behavior of the underlying PDO object by setting in your +config object the lowercase version of the constant name. This works for both PDO options +(e.g. ``\PDO::ATTR_CASE`` would be ``attr_case``) and adapter specific options (e.g. for MySQL +you may set ``\PDO::MYSQL_ATTR_IGNORE_SPACE`` as ``mysql_attr_ignore_space``). Please consult +the `PDO documentation `_ for the allowed attributes +and their values. + +For example, to set the above example options: + +.. code-block:: php + + $config = [ + "environments" => [ + "development" => [ + "adapter" => "mysql", + # other adapter settings + "attr_case" => \PDO::ATTR_CASE, + "mysql_attr_ignore_space" => 1, + ], + ], + ]; + +By default, the only attribute that Phinx sets is ``\PDO::ATTR_ERRMODE`` to ``PDO::ERRMODE_EXCEPTION``. It is +not recommended to override this. + +MySQL +````````````````` + +The MySQL adapter has an unfortunate limitation in that it certain actions causes an +`implicit commit `_ regardless of transaction +state. Notably this list includes ``CREATE TABLE``, ``ALTER TABLE``, and ``DROP TABLE``, which are the most +common operations that Phinx will run. This means that unlike other adapters which will attempt to gracefully +rollback a transaction on a failed migration, if a migration fails for MySQL, it may leave your DB in a partially +migrated state. + +SQLite +````````````````` + +Declaring an SQLite database uses a simplified structure: + +.. code-block:: yaml + + environments: + development: + adapter: sqlite + name: ./data/derby + suffix: ".db" # Defaults to ".sqlite3" + testing: + adapter: sqlite + memory: true # Setting memory to *any* value overrides name + +SQL Server +````````````````` + +When using the ``sqlsrv`` adapter and connecting to a named instance you should +omit the ``port`` setting as SQL Server will negotiate the port automatically. +Additionally, omit the ``charset: utf8`` or change to ``charset: 65001`` which +corresponds to UTF8 for SQL Server. + +Custom Adapters +````````````````` + +You can provide a custom adapter by registering an implementation of the `Phinx\\Db\\Adapter\\AdapterInterface` +with `AdapterFactory`: + +.. code-block:: php + + $name = 'fizz'; + $class = 'Acme\Adapter\FizzAdapter'; + + AdapterFactory::instance()->registerAdapter($name, $class); + +Adapters can be registered any time before `$app->run()` is called, which normally +called by `bin/phinx`. + +Aliases +------- + +Template creation class names can be aliased and used with the ``--class`` command line option for the :doc:`Create Command `. + +The aliased classes will still be required to implement the ``Phinx\Migration\CreationInterface`` interface. + +.. code-block:: yaml + + aliases: + permission: \Namespace\Migrations\PermissionMigrationTemplateGenerator + view: \Namespace\Migrations\ViewMigrationTemplateGenerator + +Version Order +------------- + +When rolling back or printing the status of migrations, Phinx orders the executed migrations according to the +``version_order`` option, which can have the following values: + +* ``creation`` (the default): migrations are ordered by their creation time, which is also part of their filename. +* ``execution``: migrations are ordered by their execution time, also known as start time. + +Bootstrap Path +--------------- + +You can provide a path to a `bootstrap` php file that will included before any commands phinx commands are run. Note that +setting External Variables to modify the config will not work because the config has already been parsed by this point. + +.. code-block:: yaml + + paths: + bootstrap: 'phinx-bootstrap.php' + +Within the bootstrap script, the following variables will be available: + +.. code-block:: php + + /** + * @var string $filename The file name as provided by the configuration + * @var string $filePath The absolute, real path to the file + * @var \Symfony\Component\Console\Input\InputInterface $input The executing command's input object + * @var \Symfony\Component\Console\Output\OutputInterface $output The executing command's output object + * @var \Phinx\Console\Command\AbstractCommand $context the executing command object + */ diff --git a/vendor/robmorgan/phinx/docs/en/contents.rst b/vendor/robmorgan/phinx/docs/en/contents.rst new file mode 100644 index 0000000..4723181 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/contents.rst @@ -0,0 +1,15 @@ +Contents +######## + +.. toctree:: + :maxdepth: 2 + :caption: Phinx + + intro + goals + install + migrations + seeding + commands + configuration + copyright diff --git a/vendor/robmorgan/phinx/docs/en/copyright.rst b/vendor/robmorgan/phinx/docs/en/copyright.rst new file mode 100644 index 0000000..71a6dfc --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/copyright.rst @@ -0,0 +1,29 @@ +.. index:: + single: Copyright + +Copyright +========= + +License + +(The MIT license) + +Copyright (c) 2012 Rob Morgan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/robmorgan/phinx/docs/en/goals.rst b/vendor/robmorgan/phinx/docs/en/goals.rst new file mode 100644 index 0000000..aaea245 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/goals.rst @@ -0,0 +1,13 @@ +.. index:: + single: Goals + +Goals +===== + +Phinx was developed with the following goals in mind: + +* Be portable amongst the most popular database vendors. +* Be PHP framework independent. +* Have a simple install process. +* Have an easy to use command-line operation. +* Integrate with various other PHP tools (Phing, PHPUnit) and web frameworks. diff --git a/vendor/robmorgan/phinx/docs/en/index.rst b/vendor/robmorgan/phinx/docs/en/index.rst new file mode 100644 index 0000000..aebe734 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/index.rst @@ -0,0 +1,26 @@ +Phinx Documentation +=================== + +Phinx makes it ridiculously easy to manage the database migrations for your PHP app. In less than 5 minutes, you can install Phinx using Composer and create your first database migration. Phinx is just about migrations without all the bloat of a database ORM system or application framework. + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + intro + goals + install + migrations + seeding + commands + configuration + copyright + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/vendor/robmorgan/phinx/docs/en/install.rst b/vendor/robmorgan/phinx/docs/en/install.rst new file mode 100644 index 0000000..66e1f8f --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/install.rst @@ -0,0 +1,28 @@ +.. index:: + single: Installation + +Installation +============ + +Phinx should be installed using Composer, which is a tool for dependency +management in PHP. Please visit the `Composer `_ +website for more information. + +.. note:: + + Phinx requires at least PHP 7.2 (or later). + +To install Phinx, simply require it using Composer: + +.. code-block:: bash + + php composer.phar require robmorgan/phinx + +Create folders in your project following the structure ``db/migrations`` with adequate permissions. +It is where your migration files will live and should be writable. + +Phinx can now be executed from within your project: + +.. code-block:: bash + + vendor/bin/phinx init diff --git a/vendor/robmorgan/phinx/docs/en/intro.rst b/vendor/robmorgan/phinx/docs/en/intro.rst new file mode 100644 index 0000000..50c4444 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/intro.rst @@ -0,0 +1,16 @@ +.. index:: + single: Introduction + +Introduction +============ + +Good developers always version their code using a SCM system, so why don't they +do the same for their database schema? + +Phinx allows developers to alter and manipulate databases in a clear and +concise way. It avoids the use of writing SQL by hand and instead offers a +powerful API for creating migrations using PHP code. Developers can then +version these migrations using their preferred SCM system. This makes Phinx +migrations portable between different database systems. Phinx keeps track of +which migrations have been run, so you can worry less about the state of your +database and instead focus on building better software. \ No newline at end of file diff --git a/vendor/robmorgan/phinx/docs/en/migrations.rst b/vendor/robmorgan/phinx/docs/en/migrations.rst new file mode 100644 index 0000000..1413aba --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/migrations.rst @@ -0,0 +1,1883 @@ +.. index:: + single: Writing Migrations + +Writing Migrations +================== + +Phinx relies on migrations in order to transform your database. Each migration +is represented by a PHP class in a unique file. It is preferred that you write +your migrations using the Phinx PHP API, but raw SQL is also supported. + +Creating a New Migration +------------------------ +Generating a skeleton migration file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's start by creating a new Phinx migration. Run Phinx using the ``create`` +command: + +.. code-block:: bash + + $ vendor/bin/phinx create MyNewMigration + +This will create a new migration in the format +``YYYYMMDDHHMMSS_my_new_migration.php``, where the first 14 characters are +replaced with the current timestamp down to the second. + +If you have specified multiple migration paths, you will be asked to select +which path to create the new migration in. + +Phinx automatically creates a skeleton migration file with a single method: + +.. code-block:: php + + table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } + } + +When executing this migration, Phinx will create the ``user_logins`` table on +the way up and automatically figure out how to drop the table on the way down. +Please be aware that when a ``change`` method exists, Phinx will automatically +ignore the ``up`` and ``down`` methods. If you need to use these methods it is +recommended to create a separate migration file. + +.. note:: + + When creating or updating tables inside a ``change()`` method you must use + the Table ``create()`` and ``update()`` methods. Phinx cannot automatically + determine whether a ``save()`` call is creating a new table or modifying an + existing one. + +The following actions are reversible when done through the Table API in Phinx, +and will be automatically reversed: + +- Creating a table +- Renaming a table +- Adding a column +- Renaming a column +- Adding an index +- Adding a foreign key + +If a command cannot be reversed then Phinx will throw an +``IrreversibleMigrationException`` when it's migrating down. If you wish to +use a command that cannot be reversed in the change function, you can use an +if statement with ``$this->isMigratingUp()`` to only run things in the +up or down direction. For example: + +.. code-block:: php + + table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + if ($this->isMigratingUp()) { + $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) + ->save(); + } + } + } + +The Up Method +~~~~~~~~~~~~~ + +The up method is automatically run by Phinx when you are migrating up and it +detects the given migration hasn't been executed previously. You should use the +up method to transform the database with your intended changes. + +The Down Method +~~~~~~~~~~~~~~~ + +The down method is automatically run by Phinx when you are migrating down and +it detects the given migration has been executed in the past. You should use +the down method to reverse/undo the transformations described in the up method. + +The Init Method +~~~~~~~~~~~~~~~ + +The ``init()`` method is run by Phinx before the migration methods if it exists. +This can be used for setting common class properties that are then used within +the migration methods. + +Executing Queries +----------------- + +Queries can be executed with the ``execute()`` and ``query()`` methods. The +``execute()`` method returns the number of affected rows whereas the +``query()`` method returns the result as a +`PDOStatement `_ + +.. code-block:: php + + execute('DELETE FROM users'); // returns the number of affected rows + + // query() + $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement + $rows = $stmt->fetchAll(); // returns the result as an array + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +.. note:: + + These commands run using the PHP Data Objects (PDO) extension which + defines a lightweight, consistent interface for accessing databases + in PHP. Always make sure your queries abide with PDOs before using + the ``execute()`` command. This is especially important when using + DELIMITERs during insertion of stored procedures or triggers which + don't support DELIMITERs. + +.. warning:: + + When using ``execute()`` or ``query()`` with a batch of queries, PDO doesn't + throw an exception if there is an issue with one or more of the queries + in the batch. + + As such, the entire batch is assumed to have passed without issue. + + If Phinx was to iterate any potential result sets, looking to see if one + had an error, then Phinx would be denying access to all the results as there + is no facility in PDO to get a previous result set + `nextRowset() `_ - + but no ``previousSet()``). + + So, as a consequence, due to the design decision in PDO to not throw + an exception for batched queries, Phinx is unable to provide the fullest + support for error handling when batches of queries are supplied. + + Fortunately though, all the features of PDO are available, so multiple batches + can be controlled within the migration by calling upon + `nextRowset() `_ + and examining `errorInfo `_. + +Fetching Rows +------------- + +There are two methods available to fetch rows. The ``fetchRow()`` method will +fetch a single row, whilst the ``fetchAll()`` method will return multiple rows. +Both methods accept raw SQL as their only parameter. + +.. code-block:: php + + fetchRow('SELECT * FROM users'); + + // fetch an array of messages + $rows = $this->fetchAll('SELECT * FROM messages'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Inserting Data +-------------- + +Phinx makes it easy to insert data into your tables. Whilst this feature is +intended for the :doc:`seed feature `, you are also free to use the +insert methods in your migrations. + +.. code-block:: php + + 1, + 'name' => 'In Progress' + ]; + + $table = $this->table('status'); + $table->insert($singleRow); + $table->saveData(); + + // inserting multiple rows + $rows = [ + [ + 'id' => 2, + 'name' => 'Stopped' + ], + [ + 'id' => 3, + 'name' => 'Queued' + ] + ]; + + $this->table('status')->insert($rows)->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->execute('DELETE FROM status'); + } + } + +.. note:: + + You cannot use the insert methods inside a `change()` method. Please use the + `up()` and `down()` methods. + +Working With Tables +------------------- + +The Table Object +~~~~~~~~~~~~~~~~ + +The Table object is one of the most useful APIs provided by Phinx. It allows +you to easily manipulate database tables using PHP code. You can retrieve an +instance of the Table object by calling the ``table()`` method from within +your database migration. + +.. code-block:: php + + table('tableName'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +You can then manipulate this table using the methods provided by the Table +object. + +Saving Changes +~~~~~~~~~~~~~~ + +When working with the Table object, Phinx stores certain operations in a +pending changes cache. Once you have made the changes you want to the table, +you must save them. To perform this operation, Phinx provides three methods, +``create()``, ``update()``, and ``save()``. ``create()`` will first create +the table and then run the pending changes. ``update()`` will just run the +pending changes, and should be used when the table already exists. ``save()`` +is a helper function that checks first if the table exists and if it does not +will run ``create()``, else it will run ``update()``. + +As stated above, when using the ``change()`` migration method, you should always +use ``create()`` or ``update()``, and never ``save()`` as otherwise migrating +and rolling back may result in different states, due to ``save()`` calling +``create()`` when running migrate and then ``update()`` on rollback. When +using the ``up()``/``down()`` methods, it is safe to use either ``save()`` or +the more explicit methods. + +When in doubt with working with tables, it is always recommended to call +the appropriate function and commit any pending changes to the database. + +Creating a Table +~~~~~~~~~~~~~~~~ + +Creating a table is really easy using the Table object. Let's create a table to +store a collection of users. + +.. code-block:: php + + table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->create(); + } + } + +Columns are added using the ``addColumn()`` method. We create a unique index +for both the username and email columns using the ``addIndex()`` method. +Finally calling ``create()`` commits the changes to the database. + +.. note:: + + Phinx automatically creates an auto-incrementing primary key column called ``id`` for every + table. + +The ``id`` option sets the name of the automatically created identity field, while the ``primary_key`` +option selects the field or fields used for primary key. ``id`` will always override the ``primary_key`` +option unless it's set to false. If you don't need a primary key set ``id`` to false without +specifying a ``primary_key``, and no primary key will be created. + +To specify an alternate primary key, you can specify the ``primary_key`` option +when accessing the Table object. Let's disable the automatic ``id`` column and +create a primary key using two columns instead: + +.. code-block:: php + + table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } + } + +Setting a single ``primary_key`` doesn't enable the ``AUTO_INCREMENT`` option. +To simply change the name of the primary key, we need to override the default ``id`` field name: + +.. code-block:: php + + table('followers', ['id' => 'user_id']); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } + } + +In addition, the MySQL adapter supports following options: + +========== =========== +Option Description +========== =========== +comment set a text comment on the table +row_format set the table row format +engine define table engine *(defaults to ``InnoDB``)* +collation define table collation *(defaults to ``utf8_general_ci``)* +signed whether the primary key is ``signed`` *(defaults to ``true``)* +limit set the maximum length for the primary key +========== =========== + +By default the primary key is ``signed``. +To simply set it to unsigned just pass ``signed`` option with a ``false`` value: + +.. code-block:: php + + table('followers', ['signed' => false]); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } + } + + +The PostgreSQL adapter supports the following options: + +========= =========== +Option Description +========= =========== +comment set a text comment on the table +========= =========== + +To view available column types and options, see `Valid Column Types`_ for details. + +Determining Whether a Table Exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can determine whether or not a table exists by using the ``hasTable()`` +method. + +.. code-block:: php + + hasTable('users'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Dropping a Table +~~~~~~~~~~~~~~~~ + +Tables can be dropped quite easily using the ``drop()`` method. It is a +good idea to recreate the table again in the ``down()`` method. + +Note that like other methods in the ``Table`` class, ``drop`` also needs ``save()`` +to be called at the end in order to be executed. This allows phinx to intelligently +plan migrations when more than one table is involved. + +.. code-block:: php + + table('users')->drop()->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } + } + +Renaming a Table +~~~~~~~~~~~~~~~~ + +To rename a table access an instance of the Table object then call the +``rename()`` method. + +.. code-block:: php + + table('users'); + $table + ->rename('legacy_users') + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('legacy_users'); + $table + ->rename('users') + ->update(); + } + } + +Changing the Primary Key +~~~~~~~~~~~~~~~~~~~~~~~~ + +To change the primary key on an existing table, use the ``changePrimaryKey()`` method. +Pass in a column name or array of columns names to include in the primary key, or ``null`` to drop the primary key. +Note that the mentioned columns must be added to the table, they will not be added implicitly. + +.. code-block:: php + + table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->addColumn('new_id', 'integer', ['null' => false]) + ->changePrimaryKey(['new_id', 'username']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Changing the Table Comment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To change the comment on an existing table, use the ``changeComment()`` method. +Pass in a string to set as the new table comment, or ``null`` to drop the existing comment. + +.. code-block:: php + + table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->changeComment('This is the table with users auth information, password should be encrypted') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Working With Columns +-------------------- + +.. _valid-column-types: + +Valid Column Types +~~~~~~~~~~~~~~~~~~ + +Column types are specified as strings and can be one of: + +- binary +- boolean +- char +- date +- datetime +- decimal +- float +- double +- smallinteger +- integer +- biginteger +- string +- text +- time +- timestamp +- uuid + +In addition, the MySQL adapter supports ``enum``, ``set``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob``, ``bit`` and ``json`` column types +(``json`` in MySQL 5.7 and above). When providing a limit value and using ``binary``, ``varbinary`` or ``blob`` and its subtypes, the retained column +type will be based on required length (see `Limit Option and MySQL`_ for details); + +In addition, the Postgres adapter supports ``interval``, ``json``, ``jsonb``, ``uuid``, ``cidr``, ``inet`` and ``macaddr`` column types +(PostgreSQL 9.3 and above). + +Valid Column Options +~~~~~~~~~~~~~~~~~~~~ + +The following are valid column options: + +For any column type: + +======= =========== +Option Description +======= =========== +limit set maximum length for strings, also hints column types in adapters (see note below) +length alias for ``limit`` +default set default value or action +null allow ``NULL`` values, defaults to false (should not be used with primary keys!) (see note below) +after specify the column that a new column should be placed after, or use ``\Phinx\Db\Adapter\MysqlAdapter::FIRST`` to place the column at the start of the table *(only applies to MySQL)* +comment set a text comment on the column +======= =========== + +For ``decimal`` columns: + +========= =========== +Option Description +========= =========== +precision combine with ``scale`` set to set decimal accuracy +scale combine with ``precision`` to set decimal accuracy +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +========= =========== + +For ``enum`` and ``set`` columns: + +========= =========== +Option Description +========= =========== +values Can be a comma separated list or an array of values +========= =========== + +For ``integer`` and ``biginteger`` columns: + +======== =========== +Option Description +======== =========== +identity enable or disable automatic incrementing +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +======== =========== + +For ``timestamp`` columns: + +======== =========== +Option Description +======== =========== +default set default value (use with ``CURRENT_TIMESTAMP``) +update set an action to be triggered when the row is updated (use with ``CURRENT_TIMESTAMP``) *(only applies to MySQL)* +timezone enable or disable the ``with time zone`` option for ``time`` and ``timestamp`` columns *(only applies to Postgres)* +======== =========== + +You can add ``created_at`` and ``updated_at`` timestamps to a table using the ``addTimestamps()`` method. This method accepts +three arguments, where the first two allow setting alternative names for the columns while the third argument allows you to +enable the ``timezone`` option for the columns. The defaults for these arguments are ``created_at``, ``updated_at``, and ``true`` +respectively. For the first and second argument, if you provide ``null``, then the default name will be used, and if you provide +``false``, then that column will not be created. Please note that attempting to set both to ``false`` will throw a +``\RuntimeException``. Additionally, you can use the ``addTimestampsWithTimezone()`` method, which is an alias to +``addTimestamps()`` that will always set the third argument to ``true`` (see examples below). The ``created_at`` column will +have a default set to ``CURRENT_TIMESTAMP``. For MySQL only, ``update_at`` column will have update set to +``CURRENT_TIMESTAMP``. + +.. code-block:: php + + table('users')->addTimestamps()->create(); + // Use defaults (with timezones) + $table = $this->table('users')->addTimestampsWithTimezone()->create(); + + // Override the 'created_at' column name with 'recorded_at'. + $table = $this->table('books')->addTimestamps('recorded_at')->create(); + + // Override the 'updated_at' column name with 'amended_at', preserving timezones. + // The two lines below do the same, the second one is simply cleaner. + $table = $this->table('books')->addTimestamps(null, 'amended_at', true)->create(); + $table = $this->table('users')->addTimestampsWithTimezone(null, 'amended_at')->create(); + + // Only add the created_at column to the table + $table = $this->table('books')->addTimestamps(null, false); + // Only add the updated_at column to the table + $table = $this->table('users')->addTimestamps(false); + // Note, setting both false will throw a \RuntimeError + } + } + +For ``boolean`` columns: + +======== =========== +Option Description +======== =========== +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +======== =========== + +For ``string`` and ``text`` columns: + +========= =========== +Option Description +========= =========== +collation set collation that differs from table defaults *(only applies to MySQL)* +encoding set character set that differs from table defaults *(only applies to MySQL)* +========= =========== + +For foreign key definitions: + +====== =========== +Option Description +====== =========== +update set an action to be triggered when the row is updated +delete set an action to be triggered when the row is deleted +====== =========== + +You can pass one or more of these options to any column with the optional +third argument array. + +Limit Option and MySQL +~~~~~~~~~~~~~~~~~~~~~~ + +When using the MySQL adapter, additional hinting of database column type can be +made for ``integer``, ``text``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob`` columns. Using ``limit`` with +one the following options will modify the column type accordingly: + +============ ============== +Limit Column Type +============ ============== +BLOB_TINY TINYBLOB +BLOB_REGULAR BLOB +BLOB_MEDIUM MEDIUMBLOB +BLOB_LONG LONGBLOB +TEXT_TINY TINYTEXT +TEXT_REGULAR TEXT +TEXT_MEDIUM MEDIUMTEXT +TEXT_LONG LONGTEXT +INT_TINY TINYINT +INT_SMALL SMALLINT +INT_MEDIUM MEDIUMINT +INT_REGULAR INT +INT_BIG BIGINT +============ ============== + +For ``binary`` or ``varbinary`` types, if limit is set greater than allowed 255 bytes, the type will be changed to the best matching blob type given the length. + +.. code-block:: php + + table('cart_items'); + $table->addColumn('user_id', 'integer') + ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) + ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) + ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) + ->create(); + +Custom Column Types & Default Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some DBMS systems provide additional column types and default values that are specific to them. +If you don't want to keep your migrations DBMS-agnostic you can use those custom types in your migrations +through the ``\Phinx\Util\Literal::from`` method, which takes a string as its only argument, and returns an +instance of ``\Phinx\Util\Literal``. When Phinx encounters this value as a column's type it knows not to +run any validation on it and to use it exactly as supplied without escaping. This also works for ``default`` +values. + +You can see an example below showing how to add a ``citext`` column as well as a column whose default value +is a function, in PostgreSQL. This method of preventing the built-in escaping is supported in all adapters. + +.. code-block:: php + + table('users') + ->addColumn('username', Literal::from('citext')) + ->addColumn('uniqid', 'uuid', [ + 'default' => Literal::from('uuid_generate_v4()') + ]) + ->addColumn('creation', 'timestamp', [ + 'timezone' => true, + 'default' => Literal::from('now()') + ]) + ->create(); + } + } + +User Defined Types (Custom Data Domain) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Building upon the base types and column options you can define your custom +user defined types. Custom user defined types are configured in the +``data_domain`` root config option. + +.. code-block:: yaml + + data_domain: + phone_number: + type: string + length: 20 + address_line: + type: string + length: 150 + +Each user defined type can hold any valid type and column option, they are just +used as "macros" and replaced at the time of migration. + +.. code-block:: php + + table('user_data'); + $table->addColumn('user_phone_number', 'phone_number') + ->addColumn('user_address_line_1', 'address_line') + ->addColumn('user_address_line_2', 'address_line', ['null' => true]) + ->create(); + +Specifying a data domain at the beginning of your project is crucial to have a +homogeneous data model. It avoids mistakes like having many ``contact_name`` +columns with different lengths, mismatched integer types (long vs. bigint, etc). + +.. note:: + + For ``integer``, ``text`` and ``blob`` columns you can use the special + constants from MySQL and Postgress adapter classes. + + You can even customize some internal types to add your own default options, + but some column options can't be overriden in the data model (some options + are fixed like ``limit`` for the ``uuid`` special data type). + +.. code-block:: yaml + + # Some examples of custom data types + data_domain: + file: + type: blob + limit: BLOB_LONG # For MySQL DB. Uses MysqlAdapter::BLOB_LONG + boolean: + type: boolean # Customization of the boolean to be unsigned + signed: false + image_type: + type: enum # Enums can use YAML lists or a comma separated string + values: + - gif + - jpg + - png + +Get a column list +~~~~~~~~~~~~~~~~~ + +To retrieve all table columns, simply create a `table` object and call `getColumns()` +method. This method will return an array of Column classes with basic info. Example below: + +.. code-block:: php + + table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down() + { + ... + } + } + +Get a column by name +~~~~~~~~~~~~~~~~~~~~ + +To retrieve one table column, simply create a `table` object and call the `getColumn()` +method. This method will return a Column class with basic info or NULL when the column doesn't exist. Example below: + +.. code-block:: php + + table('users')->getColumn('email'); + ... + } + + /** + * Migrate Down. + */ + public function down() + { + ... + } + } + +Checking whether a column exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can check if a table already has a certain column by using the +``hasColumn()`` method. + +.. code-block:: php + + table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // do something + } + + } + } + +Renaming a Column +~~~~~~~~~~~~~~~~~ + +To rename a column, access an instance of the Table object then call the +``renameColumn()`` method. + +.. code-block:: php + + table('users'); + $table->renameColumn('bio', 'biography'); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('users'); + $table->renameColumn('biography', 'bio'); + } + } + +Adding a Column After Another Column +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When adding a column you can dictate its position using the ``after`` option. + +.. code-block:: php + + table('users'); + $table->addColumn('city', 'string', ['after' => 'email']) + ->update(); + } + } + +Dropping a Column +~~~~~~~~~~~~~~~~~ + +To drop a column, use the ``removeColumn()`` method. + +.. code-block:: php + + table('users'); + $table->removeColumn('short_name') + ->save(); + } + } + + +Specifying a Column Limit +~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can limit the maximum length of a column by using the ``limit`` option. + +.. code-block:: php + + table('tags'); + $table->addColumn('short_name', 'string', ['limit' => 30]) + ->update(); + } + } + +Changing Column Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To change column type or options on an existing column, use the ``changeColumn()`` method. +See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values. + +.. code-block:: php + + table('users'); + $users->changeColumn('email', 'string', ['limit' => 255]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Working With Indexes +-------------------- + +To add an index to a table you can simply call the ``addIndex()`` method on the +table object. + +.. code-block:: php + + table('users'); + $table->addColumn('city', 'string') + ->addIndex(['city']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +By default Phinx instructs the database adapter to create a normal index. We +can pass an additional parameter ``unique`` to the ``addIndex()`` method to +specify a unique index. We can also explicitly specify a name for the index +using the ``name`` parameter, the index columns sort order can also be specified using +the ``order`` parameter. The order parameter takes an array of column names and sort order key/value pairs. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addIndex(['email', 'username'], [ + 'unique' => true, + 'name' => 'idx_users_email', + 'order' => ['email' => 'DESC', 'username' => 'ASC']] + ) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +The MySQL adapter also supports ``fulltext`` indexes. If you are using a version before 5.6 you must +ensure the table uses the ``MyISAM`` engine. + +.. code-block:: php + + table('users', ['engine' => 'MyISAM']); + $table->addColumn('email', 'string') + ->addIndex('email', ['type' => 'fulltext']) + ->create(); + } + } + +In addition, MySQL adapter also supports setting the index length defined by limit option. +When you are using a multi-column index, you are able to define each column index length. +The single column index can define its index length with or without defining column name in limit option. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addColumn('user_guid', 'string', ['limit' => 36]) + ->addIndex(['email','username'], ['limit' => ['email' => 5, 'username' => 2]]) + ->addIndex('user_guid', ['limit' => 6]) + ->create(); + } + } + +The SQL Server and PostgreSQL adapters also supports ``include`` (non-key) columns on indexes. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('firstname','string') + ->addColumn('lastname','string') + ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) + ->create(); + } + } + + +Removing indexes is as easy as calling the ``removeIndex()`` method. You must +call this method for each index. + +.. code-block:: php + + table('users'); + $table->removeIndex(['email']) + ->save(); + + // alternatively, you can delete an index by its name, ie: + $table->removeIndexByName('idx_users_email') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + + +Working With Foreign Keys +------------------------- + +Phinx has support for creating foreign key constraints on your database tables. +Let's add a foreign key to an example table: + +.. code-block:: php + + table('tags'); + $table->addColumn('tag_name', 'string') + ->save(); + + $refTable = $this->table('tag_relationships'); + $refTable->addColumn('tag_id', 'integer', ['null' => true]) + ->addForeignKey('tag_id', 'tags', 'id', ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION']) + ->save(); + + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +"On delete" and "On update" actions are defined with a 'delete' and 'update' options array. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and 'RESTRICT'. If 'SET_NULL' is used then the column must be created as nullable with the option ``['null' => true]``. +Constraint name can be changed with the 'constraint' option. + +It is also possible to pass ``addForeignKey()`` an array of columns. +This allows us to establish a foreign key relationship to a table which uses a combined key. + +.. code-block:: php + + table('follower_events'); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('event_id', 'integer') + ->addForeignKey(['user_id', 'follower_id'], + 'followers', + ['user_id', 'follower_id'], + ['delete'=> 'NO_ACTION', 'update'=> 'NO_ACTION', 'constraint' => 'user_follower_id']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +We can add named foreign keys using the ``constraint`` parameter. This feature is supported as of Phinx version 0.6.5 + +.. code-block:: php + + table('your_table'); + $table->addForeignKey('foreign_id', 'reference_table', ['id'], + ['constraint' => 'your_foreign_key_name']); + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +We can also easily check if a foreign key exists: + +.. code-block:: php + + table('tag_relationships'); + $exists = $table->hasForeignKey('tag_id'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Finally, to delete a foreign key, use the ``dropForeignKey`` method. + +Note that like other methods in the ``Table`` class, ``dropForeignKey`` also needs ``save()`` +to be called at the end in order to be executed. This allows phinx to intelligently +plan migrations when more than one table is involved. + +.. code-block:: php + + table('tag_relationships'); + $table->dropForeignKey('tag_id')->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + + + +Using the Query Builder +----------------------- + +It is not uncommon to pair database structure changes with data changes. For example, you may want to +migrate the data in a couple columns from the users to a newly created table. For this type of scenarios, +Phinx provides access to a Query builder object, that you may use to execute complex ``SELECT``, ``UPDATE``, +``INSERT`` or ``DELETE`` statements. + +The Query builder is provided by the `cakephp/database `_ project, and should +be easy to work with as it resembles very closely plain SQL. Accesing the query builder is done by calling the +``getQueryBuilder()`` function: + + +.. code-block:: php + + getQueryBuilder(); + $statement = $builder->select('*')->from('users')->execute(); + var_dump($statement->fetchAll()); + } + } + +Selecting Fields +~~~~~~~~~~~~~~~~ + +Adding fields to the SELECT clause: + + +.. code-block:: php + + select(['id', 'title', 'body']); + + // Results in SELECT id AS pk, title AS aliased_title, body ... + $builder->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); + + // Use a closure + $builder->select(function ($builder) { + return ['id', 'title', 'body']; + }); + + +Where Conditions +~~~~~~~~~~~~~~~~ + +Generating conditions: + +.. code-block:: php + + // WHERE id = 1 + $builder->where(['id' => 1]); + + // WHERE id > 1 + $builder->where(['id >' => 1]); + + +As you can see you can use any operator by placing it with a space after the field name. Adding multiple conditions is easy as well: + + +.. code-block:: php + + where(['id >' => 1])->andWhere(['title' => 'My Title']); + + // Equivalent to + $builder->where(['id >' => 1, 'title' => 'My title']); + + // WHERE id > 1 OR title = 'My title' + $builder->where(['OR' => ['id >' => 1, 'title' => 'My title']]); + + +For even more complex conditions you can use closures and expression objects: + +.. code-block:: php + + select('*') + ->from('articles') + ->where(function ($exp) { + return $exp + ->eq('author_id', 2) + ->eq('published', true) + ->notEq('spam', true) + ->gt('view_count', 10); + }); + + +Which results in: + +.. code-block:: sql + + SELECT * FROM articles + WHERE + author_id = 2 + AND published = 1 + AND spam != 1 + AND view_count > 10 + + +Combining expressions is also possible: + + +.. code-block:: php + + select('*') + ->from('articles') + ->where(function ($exp) { + $orConditions = $exp->or_(['author_id' => 2]) + ->eq('author_id', 5); + return $exp + ->not($orConditions) + ->lte('view_count', 10); + }); + +It generates: + +.. code-block:: sql + + SELECT * + FROM articles + WHERE + NOT (author_id = 2 OR author_id = 5) + AND view_count <= 10 + + +When using the expression objects you can use the following methods to create conditions: + +* ``eq()`` Creates an equality condition. +* ``notEq()`` Create an inequality condition +* ``like()`` Create a condition using the ``LIKE`` operator. +* ``notLike()`` Create a negated ``LIKE`` condition. +* ``in()`` Create a condition using ``IN``. +* ``notIn()`` Create a negated condition using ``IN``. +* ``gt()`` Create a ``>`` condition. +* ``gte()`` Create a ``>=`` condition. +* ``lt()`` Create a ``<`` condition. +* ``lte()`` Create a ``<=`` condition. +* ``isNull()`` Create an ``IS NULL`` condition. +* ``isNotNull()`` Create a negated ``IS NULL`` condition. + + +Aggregates and SQL Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +.. code-block:: php + + select(['count' => $builder->func()->count('*')]); + +A number of commonly used functions can be created with the func() method: + +* ``sum()`` Calculate a sum. The arguments will be treated as literal values. +* ``avg()`` Calculate an average. The arguments will be treated as literal values. +* ``min()`` Calculate the min of a column. The arguments will be treated as literal values. +* ``max()`` Calculate the max of a column. The arguments will be treated as literal values. +* ``count()`` Calculate the count. The arguments will be treated as literal values. +* ``concat()`` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. +* ``coalesce()`` Coalesce values. The arguments are treated as bound parameters unless marked as literal. +* ``dateDiff()`` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. +* ``now()`` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. + +When providing arguments for SQL functions, there are two kinds of parameters you can use, +literal arguments and bound parameters. Literal parameters allow you to reference columns or +other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example: + + +.. code-block:: php + + func()->concat([ + 'title' => 'literal', + ' NEW' + ]); + $query->select(['title' => $concat]); + + +Getting Results out of a Query +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: + + +.. code-block:: php + + execute()->fetchAll('assoc'); + + +Creating an Insert Query +~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating insert queries is also possible: + + +.. code-block:: php + + getQueryBuilder(); + $builder + ->insert(['first_name', 'last_name']) + ->into('users') + ->values(['first_name' => 'Steve', 'last_name' => 'Jobs']) + ->values(['first_name' => 'Jon', 'last_name' => 'Snow']) + ->execute() + + +For increased performance, you can use another builder object as the values for an insert query: + +.. code-block:: php + + getQueryBuilder(); + $namesQuery + ->select(['fname', 'lname']) + ->from('users') + ->where(['is_active' => true]) + + $builder = $this->getQueryBuilder(); + $st = $builder + ->insert(['first_name', 'last_name']) + ->into('names') + ->values($namesQuery) + ->execute() + + var_dump($st->lastInsertId('names', 'id')); + + +The above code will generate: + +.. code-block:: sql + + INSERT INTO names (first_name, last_name) + (SELECT fname, lname FROM USERS where is_active = 1) + + +Creating an update Query +~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating update queries is similar to both inserting and selecting: + +.. code-block:: php + + getQueryBuilder(); + $builder + ->update('users') + ->set('fname', 'Snow') + ->where(['fname' => 'Jon']) + ->execute() + + +Creating a Delete Query +~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, delete queries: + +.. code-block:: php + + getQueryBuilder(); + $builder + ->delete('users') + ->where(['accepted_gdpr' => false]) + ->execute() diff --git a/vendor/robmorgan/phinx/docs/en/namespaces.rst b/vendor/robmorgan/phinx/docs/en/namespaces.rst new file mode 100644 index 0000000..c679687 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/namespaces.rst @@ -0,0 +1,152 @@ +.. index:: + single: Supporting namespaces + +PSR-4 compliance +================== + +Phinx allows the use of namespaces in Migrations and Seeders. +Migrations require a timestamp in the filename, and therefore won't be fully PSR-4 compliant. Seeders do not need a timestamp and will be fully PSR-4 compliant. + +Using namespaces +------------------------ +1) locate your Phinx config file, the config file may be in one of following three formats: PHP, YAML or JSON. +2) Locate the "paths" key inside the config file, it should look something like one of the below examples. + - (NB. the "migrations" and "seeds" keys may be both an array or a string, so don't be alarmed if yours looks different) + +PHP: + +.. code-block:: php + + 'paths' => [ + 'migrations' => 'database/migrations', + 'seeds' => 'database/seeds', + ], + + +YAML: + +.. code-block:: yaml + + paths: + migrations: ./database/migrations + seeds: ./database/seeds + +JSON: + +.. code-block:: json + + { + "paths": { + "migrations": "database/migrations", + "seeds": "database/seeds" + } + } + +3) Enabling namespaces is a fairly simple task, we're going to turn the "migrations" and "seeds" keys into arrays. + - Any value without a key is a global-non-namespaced path + - Any keyed value will use the key as namespace + +.. code-block:: php + + 'paths' => [ + 'migrations' => [ + '/path/to/migration/without/namespace', // Non-namespaced migrations + 'Foo' => '/path/to/migration/Foo', // Migrations in the Foo namespace + ], + 'seeds' => [ + '/path/to/seeds/without/namespace', // Non-namespaced seeders + 'Baz' => '/path/to/seeds/Baz', // Seeders in the Baz namespace + ] + ], + +PHP is a bit special in this case, as it allows keyless and keyed values in the same array. To make this configuration work in YAML and JSON, we have to key the non-namespaced path with "0". + +.. code-block:: json + + { + "paths": { + "migrations": { + "0": "./db/migrations", + "Foo\\Bar": "./src/FooBar/db/migrations" + } + } + } + +.. code-block:: yaml + + paths: + migrations: + 0: ./db/migrations + Foo\\Bar: ./src/FooBar/db/migrations + +Path resolving +^^^^^^^^^^^^^^ + +Let's take a closer look on how the paths are resolved, let's start with the non-namespaced path. + +"./" refers to the project-root, therefore "./db/migrations" would resolve to /db/migrations. +This is the directory where Phinx will look for migrations when migrating. +NB. these migrations must not have a namespace. + +.. image:: http://i.imgur.com/l84308Q.jpg + +This image shows the path for "./db/migrations" where "Phinx" is the project root. + +And the namespaced path would be resolved as shown below. + +"./src/FooBar/db/migrations" would resolve to /src/FooBar/db/migrations, which is where Phinx will look for migrations in the Foo\\Bar namespace. + +.. image:: http://i.imgur.com/2mg0V8V.jpg + +The file path would look like this, if the project-root was "Phinx" + +File examples +^^^^^^^^^^^^^ + +The non-namespaced file in /db/migrations may look like the following example. + +.. code-block:: php + + table('users'); + $table->addColumn('name', 'string')->create(); + } + } + +Whereas the namespaced file will be found in /src/FoorBar/db/migrations and can look like this: +(Notice the namespace is the same as defined in the paths config). + +.. code-block:: php + + table('users'); + $table->addColumn('name', 'string')->create(); + } + } + + +4) That's it, you're ready to go, to create a migration simply run: *$ phinx create CreateUsersTable [--path ./src/FoorBar/db/migrations]* + + - If multiple paths are configured, but none provided with the --path flag, you will be prompted for which path to use. + + +Did you run into an issue? +-------------------------- + +- Due to the way the migrations are created, it is impossible to generate a migration in the *global* namespace with a class-name that is the same as a migration in a user-defined namespace. diff --git a/vendor/robmorgan/phinx/docs/en/seeding.rst b/vendor/robmorgan/phinx/docs/en/seeding.rst new file mode 100644 index 0000000..9569b9c --- /dev/null +++ b/vendor/robmorgan/phinx/docs/en/seeding.rst @@ -0,0 +1,267 @@ +.. index:: + single: Database Seeding + +Database Seeding +================ + +In version 0.5.0 Phinx introduced support for seeding your database with test +data. Seed classes are a great way to easily fill your database with data after +it's created. By default they are stored in the `seeds` directory; however, this +path can be changed in your configuration file. + +.. note:: + + Database seeding is entirely optional, and Phinx does not create a `seeds` + directory by default. + +Creating a New Seed Class +------------------------- + +Phinx includes a command to easily generate a new seed class: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:create UserSeeder + +If you have specified multiple seed paths, you will be asked to select which +path to create the new seed class in. + +It is based on a skeleton template: + +.. code-block:: php + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ],[ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + } + } + +.. note:: + + You must call the `saveData()` method to commit your data to the table. Phinx + will buffer data until you do so. + +Integrating with the Faker library +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's trivial to use the awesome +`Faker library `_ in your seed classes. +Simply install it using Composer: + +.. code-block:: bash + + $ composer require fzaninotto/faker + +Then use it in your seed classes: + +.. code-block:: php + + $faker->userName, + 'password' => sha1($faker->password), + 'password_salt' => sha1('foo'), + 'email' => $faker->email, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'created' => date('Y-m-d H:i:s'), + ]; + } + + $this->table('users')->insert($data)->saveData(); + } + } + +Truncating Tables +----------------- + +In addition to inserting data Phinx makes it trivial to empty your tables using the +SQL `TRUNCATE` command: + +.. code-block:: php + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + + // empty the table + $posts->truncate(); + } + } + +.. note:: + + SQLite doesn't natively support the `TRUNCATE` command so behind the scenes + `DELETE FROM` is used. It is recommended to call the `VACUUM` command + after truncating a table. Phinx does not do this automatically. + +Executing Seed Classes +---------------------- + +This is the easy part. To seed your database, simply use the `seed:run` command: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run + +By default, Phinx will execute all available seed classes. If you would like to +run a specific class, simply pass in the name of it using the `-s` parameter: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -s UserSeeder + +You can also run multiple seeders: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -s UserSeeder -s PermissionSeeder -s LogSeeder + +You can also use the `-v` parameter for more output verbosity: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -v + +The Phinx seed functionality provides a simple mechanism to easily and repeatably +insert test data into your database. diff --git a/vendor/robmorgan/phinx/docs/es/commands.rst b/vendor/robmorgan/phinx/docs/es/commands.rst new file mode 100644 index 0000000..685c229 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/es/commands.rst @@ -0,0 +1,292 @@ +. index:: + single: Commands + +Comandos +######## + +Phinx se corre usando un numero de comandos. + +Migracion de Comandos +===================== + +Comando de Inicio + +El comanado de inicio (manera corta de inicialización) se usa para preparar el proyecto Phinix. Este comando genera el archivo phinx.yml en el directorio raiz del proyecto: + +.. code-block:: bash + + $ cd yourapp + $ phinx init . + +Abre este archivo en tu editor de texto para configurar tu projecto. Por favot mira el :doc:`Configuration ` para mas información. + +Comando de creación. +-------------------- + +El comando de creación se usa para crear un nuevo archivo de migración. Requiere un argumento: el nombre de la migración. El nombre de la migracion debera especificarse en el formato CamelCase: + +.. code-block:: bash + + $ phinx create MyNewMigration + +Abre el nuevo archivo de migración en tu editor de texto para agregar las transformaciones de tu base de datos. Phinx crea archivos de migracion usando direcciones especificas en tu archivo phinx.yml . Por favor mire :doc:`Configuration ` para mas información. + +Usted puede sobreescribir el archivo modelo usado por Phinx suministrando una alternativa del modelo de archivo: + +.. code-block:: bash + + $ phinx create MyNewMigration --template="" + +Tambien puedes suministrar un modelo de clase general. Esta clase deberá implementar la interfaz ``Phinx\Migration\CreationInterface``. + +.. code-block:: bash + + $ phinx create MyNewMigration --class="" + +Además de proveer el modelo para la migracion, la clase tambien puede definir una "callback" que sera llamada una vez que el archivo de migración haya sido generado por el modelo. + +No puede usar ``--template`` y ``--class`` juntos. + +El comando de migración +----------------------- + +El comando de migración corre todas las migraciones que se encuentren disponibles, opcionalmente hasta una version especificada. + +.. code-block:: bash + + $ phinx migrate -e development + +Para migrar a una version especifica se utiliza el parametro ``--targe`` o ``-t`` resumido. + +.. code-block:: bash + + $ phinx migrate -e development -t 20110103081132 + +Use ``--dry-run`` para imprimir las consultas a la salida sin ejecutarlas + +.. code-block:: bash + + $ phinx migrate --dry-run + + +El comando para revertir +------------------------ + +El comando para revertir se utiliza para deshacer las migraciones anteriores ejecutadas por Phinx. Es lo opuesto al comando Migrar. + +Puede revertir a la migración anterior usando el comando rollback sin argumentos. + +.. code-block:: bash + + $ phinx rollback -e development + +Para revertir todas las migraciones a una versión específica, use el parámetro ``--target`` o ``-t`` para abreviar + +.. code-block:: bash + + $ phinx rollback -e development -t 20120103083322 + +Especificar 0 como la versión de destino revertirá todas las migraciones. + +.. code-block:: bash + + $ phinx rollback -e development -t 0 + +Para revertir todas las migraciones a una fecha específica, use el parámetro ``--date`` o -d para abreviar. + +.. code-block:: bash + + $ phinx rollback -e development -d 2012 + $ phinx rollback -e development -d 201201 + $ phinx rollback -e development -d 20120103 + $ phinx rollback -e development -d 2012010312 + $ phinx rollback -e development -d 201201031205 + $ phinx rollback -e development -d 20120103120530 + +Si se establece un punto de interrupción, bloqueando más reversiones, puede anular el punto de interrupción utilizando el parámetro ``--force`` o ``-f`` para abreviar + +.. code-block:: bash + + $ phinx rollback -e development -t 0 -f + +Utilice ``--dry-run`` para imprimir las consultas a la salida estándar sin ejecutarlas + +.. code-block:: bash + + $ phinx rollback --dry-run + +.. note:: + + When rolling back, Phinx orders the executed migrations using the order specified in the version_order option of your phinx.yml file. Please see the :doc:`Configuration ` chapter for more information. + +El comando de estado +-------------------- + +El comando Estado imprime una lista de todas las migraciones, junto con su estado actual. Puede usar este comando para determinar qué migraciones se han ejecutado. + +.. code-block:: bash + + $ phinx status -e development + +La salida de este comando es 0 si la base de datos está actualizada (es decir, todas las migraciones están activas) o uno de los siguientes códigos de lo contrario: + +#. Queda por lo menos una migración por ejecutar. +#: se ejecutó una migración y se registró en la base de datos, pero ahora falta +el archivo de migración + +El comando de interrupción +--------------------------- + +El comando Punto de interrupción se usa para establecer puntos de interrupción, lo que le permite limitar los retrotracción. Puede alternar el punto de interrupción de la migración más reciente al no proporcionar ningún parámetro + +.. code-block:: bash + + $ phinx breakpoint -e development + +Para alternar un punto de interrupción en una versión específica, use el parámetro ``--target`` o ``-t`` para abreviar + +.. code-block:: bash + + $ phinx breakpoint -e development -t 20120103083322 + +Puede eliminar todos los puntos de interrupción utilizando el parámetro ``--remove-all`` o ``-r`` para abreviar. + +.. code-block:: bash + + $ phinx breakpoint -e development -r + +Los puntos de interrupción son visibles cuando ejecuta el comando de estado. + +"Database Seeding" +------------------ + +El comando Crear semilla se puede usar para crear nuevas clases de base de datos. Requiere un argumento, el nombre de la clase. El nombre de la clase debe especificarse en formato CamelCase. + +.. code-block:: bash + + $ phinx seed:create MyNewSeeder + +Abra el nuevo archivo semilla en su editor de texto para agregar los comandos semilla de su base de datos. Phinx crea archivos semilla utilizando la ruta especificada en su archivo phinx.yml. Consulte el capítulo: doc: `Configuration ` para obtener más información. + +El comando de ejecución de semillas +----------------------------------- + +El comando Seed Run ejecuta todas las clases semilla disponibles u opcionalmente solo una. + +.. code-block:: bash + + $ phinx seed:run -e development + +Para ejecutar solo una clase semilla, use el parámetro ``--seed`` o ``-s`` para abreviar. + +.. code-block:: bash + + $ phinx seed:run -e development -s MyNewSeeder + + +Parámetro del archivo de configuración +-------------------------------------- + +Al ejecutar Phinx desde la línea de comandos, puede especificar un archivo de configuración usando el parámetro ``--configuration`` o ``-c``. Además de YAML, el archivo de configuración puede ser la salida calculada de un archivo PHP como una matriz de PHP:: + + [ + "migrations" => "application/migrations" + ], + "environments" => [ + "default_migration_table" => "phinxlog", + "default_environment" => "dev", + "dev" => [ + "adapter" => "mysql", + "host" => $_ENV['DB_HOST'], + "name" => $_ENV['DB_NAME'], + "user" => $_ENV['DB_USER'], + "pass" => $_ENV['DB_PASS'], + "port" => $_ENV['DB_PORT'], + ] + ] + ]; + +Phinx detecta automáticamente qué analizador de idioma usar para los archivos con las extensiones ``.yml`` y ``.php``. El analizador apropiado también se puede especificar a través de los parámetros ``--parser`` y ``-p``. Cualquier otra cosa que no sea "php" se trata como YAML. + +Al usar una matriz de PHP, puede proporcionar una clave de conexión con una instancia de PDO existente. También es importante pasar el nombre de la base de datos, ya que Phinx lo requiere para ciertos métodos como ``hasTable()``:: + + [ + "migrations" => "application/migrations" + ], + "environments" => [ + "default_migration_table" => "phinxlog", + "default_environment" => "dev", + "dev" => [ + "name" => "dev_db", + "connection" => $pdo_instance + ] + ] + ]; + +Ejecutando Phinx en una aplicación web +-------------------------------------- + +Phinx también se puede ejecutar dentro de una aplicación web utilizando la clase ``Phinx\Wrapper\TextWrapper``. Un ejemplo de esto se proporciona en **app/web.php**, que se puede ejecutar como un servidor independiente: + +.. code-block:: bash + + $ php -S localhost:8000 vendor/robmorgan/phinx/app/web.php + +Esto creará un servidor web local en http://localhost:8000 que mostrará el estado actual de la migración de forma predeterminada. Para ejecutar las migraciones, use http://localhost:8000/migrate y para revertir use http: // localhost: 8000 / rollback. + +La aplicación web incluida es solo un ejemplo y no debe utilizarse en producción + + +.. note:: + Para modificar las variables de configuración en tiempo de ejecución + e invalidar ``%%PHINX_DBNAME%%`` u otra opción dinámica, establezca ``$ + _SERVER['PHINX_DBNAME']`` antes de ejecutar los comandos. Las opciones + disponibles están documentadas en la página de Configuración. + +Usando Phinx con PHPUnit +------------------------ + +Phinx puede usarse dentro de las pruebas de su unidad para preparar o sembrar la base de datos. Puedes usarlo programáticamente:: + + public function setUp () + { + $app = new PhinxApplication(); + $app->setAutoExit(false); + $app->run(new StringInput('migrate'), new NullOutput()); + } + +Si usa una base de datos de memoria, deberá darle a Phinx una instancia de PDO específica. Puedes interactuar con Phinx directamente usando la clase Manager:: + + use PDO; + use Phinx\Config\Config; + use Phinx\Migration\Manager; + use PHPUnit\Framework\TestCase; + use Symfony\Component\Console\Input\StringInput; + use Symfony\Component\Console\Output\NullOutput; + + class DatabaseTestCase extends TestCase { + + public function setUp () + { + $pdo = new PDO('sqlite::memory:', null, null, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]); + $configArray = require('phinx.php'); + $configArray['environments']['test'] = [ + 'adapter' => 'sqlite', + 'connection' => $pdo + ]; + $config = new Config($configArray); + $manager = new Manager($config, new StringInput(' '), new NullOutput()); + $manager->migrate('test'); + $manager->seed('test'); + // You can change default fetch mode after the seeding + $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + $this->pdo = $pdo; + } + + } diff --git a/vendor/robmorgan/phinx/docs/es/conf.py b/vendor/robmorgan/phinx/docs/es/conf.py new file mode 100644 index 0000000..4691ece --- /dev/null +++ b/vendor/robmorgan/phinx/docs/es/conf.py @@ -0,0 +1,9 @@ +import sys, os + +# Append the top level directory of the docs, so we can import from the config dir. +sys.path.insert(0, os.path.abspath('..')) + +# Pull in all the configuration options defined in the global config file.. +from config.all import * + +language = 'es' diff --git a/vendor/robmorgan/phinx/docs/es/contents.rst b/vendor/robmorgan/phinx/docs/es/contents.rst new file mode 100644 index 0000000..9bbcc9c --- /dev/null +++ b/vendor/robmorgan/phinx/docs/es/contents.rst @@ -0,0 +1,8 @@ +Contents +######## + +.. toctree:: + :maxdepth: 2 + :caption: Phinx + + commands diff --git a/vendor/robmorgan/phinx/docs/fr/commands.rst b/vendor/robmorgan/phinx/docs/fr/commands.rst new file mode 100644 index 0000000..c9c8334 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/fr/commands.rst @@ -0,0 +1,339 @@ +.. index:: + single: Commands + +Commands +######## + +Phinx is run using a number of commands. + +Migration Commands +================== + +The Init Command + +The Init command (short for initialize) is used to prepare your project for +Phinx. This command generates the ``phinx.yml`` file in the root of your +project directory. + +.. code-block:: bash + + $ cd yourapp + $ phinx init . + +Open this file in your text editor to setup your project configuration. Please +see the :doc:`Configuration ` chapter for more information. + +The Create Command +------------------ + +The Create command is used to create a new migration file. It requires one +argument: the name of the migration. The migration name should be specified in +CamelCase format. + +.. code-block:: bash + + $ phinx create MyNewMigration + +Open the new migration file in your text editor to add your database +transformations. Phinx creates migration files using the path specified in your +``phinx.yml`` file. Please see the :doc:`Configuration ` chapter +for more information. + +You are able to override the template file used by Phinx by supplying an +alternative template filename. + +.. code-block:: bash + + $ phinx create MyNewMigration --template="" + +You can also supply a template generating class. This class must implement the +interface ``Phinx\Migration\CreationInterface``. + +.. code-block:: bash + + $ phinx create MyNewMigration --class="" + +In addition to providing the template for the migration, the class can also define +a callback that will be called once the migration file has been generated from the +template. + +You cannot use ``--template`` and ``--class`` together. + +The Migrate Command +------------------- + +The Migrate command runs all of the available migrations, optionally up to a +specific version. + +.. code-block:: bash + + $ phinx migrate -e development + +To migrate to a specific version then use the ``--target`` parameter or ``-t`` +for short. + +.. code-block:: bash + + $ phinx migrate -e development -t 20110103081132 + +Use ``--dry-run`` to print the queries to standard output without executing them + +.. code-block:: bash + + $ phinx migrate --dry-run + +The Rollback Command +-------------------- + +The Rollback command is used to undo previous migrations executed by Phinx. It +is the opposite of the Migrate command. + +You can rollback to the previous migration by using the ``rollback`` command +with no arguments. + +.. code-block:: bash + + $ phinx rollback -e development + +To rollback all migrations to a specific version then use the ``--target`` +parameter or ``-t`` for short. + +.. code-block:: bash + + $ phinx rollback -e development -t 20120103083322 + +Specifying 0 as the target version will revert all migrations. + +.. code-block:: bash + + $ phinx rollback -e development -t 0 + +To rollback all migrations to a specific date then use the ``--date`` +parameter or ``-d`` for short. + +.. code-block:: bash + + $ phinx rollback -e development -d 2012 + $ phinx rollback -e development -d 201201 + $ phinx rollback -e development -d 20120103 + $ phinx rollback -e development -d 2012010312 + $ phinx rollback -e development -d 201201031205 + $ phinx rollback -e development -d 20120103120530 + +If a breakpoint is set, blocking further rollbacks, you can override the +breakpoint using the ``--force`` parameter or ``-f`` for short. + +.. code-block:: bash + + $ phinx rollback -e development -t 0 -f + +Use ``--dry-run`` to print the queries to standard output without executing them + +.. code-block:: bash + + $ phinx rollback --dry-run + +.. note:: + + When rolling back, Phinx orders the executed migrations using + the order specified in the ``version_order`` option of your + ``phinx.yml`` file. + Please see the :doc:`Configuration ` chapter for more information. + +The Status Command +------------------ + +The Status command prints a list of all migrations, along with their current +status. You can use this command to determine which migrations have been run. + +.. code-block:: bash + + $ phinx status -e development + +This command exits with code 0 if the database is up-to-date (ie. all migrations are up) or one of the following codes otherwise: + +* 1: There is at least one migration left to be executed. +* 2: There was a migration run and recorded in the database, but the migration file is now missing. + +The Breakpoint Command +---------------------- + +The Breakpoint command is used to set breakpoints, allowing you to limit +rollbacks. You can toggle the breakpoint of the most recent migration by +not supplying any parameters. + +.. code-block:: bash + + $ phinx breakpoint -e development + +To toggle a breakpoint on a specific version then use the ``--target`` +parameter or ``-t`` for short. + +.. code-block:: bash + + $ phinx breakpoint -e development -t 20120103083322 + +You can remove all the breakpoints by using the ``--remove-all`` parameter +or ``-r`` for short. + +.. code-block:: bash + + $ phinx breakpoint -e development -r + +Breakpoints are visible when you run the ``status`` command. + +Database Seeding +================ + +The Seed Create Command +----------------------- + +The Seed Create command can be used to create new database seed classes. It +requires one argument, the name of the class. The class name should be specified +in CamelCase format. + +.. code-block:: bash + + $ phinx seed:create MyNewSeeder + +Open the new seed file in your text editor to add your database seed commands. +Phinx creates seed files using the path specified in your ``phinx.yml`` file. +Please see the :doc:`Configuration ` chapter for more information. + +The Seed Run Command +-------------------- + +The Seed Run command runs all of the available seed classes or optionally just +one. + +.. code-block:: bash + + $ phinx seed:run -e development + +To run only one seed class use the ``--seed`` parameter or ``-s`` for short. + +.. code-block:: bash + + $ phinx seed:run -e development -s MyNewSeeder + +Configuration File Parameter +---------------------------- + +When running Phinx from the command line, you may specify a configuration file +using the ``--configuration`` or ``-c`` parameter. In addition to YAML, the +configuration file may be the computed output of a PHP file as a PHP array: + +.. code-block:: php + + [ + "migrations" => "application/migrations" + ], + "environments" => [ + "default_migration_table" => "phinxlog", + "default_environment" => "dev", + "dev" => [ + "adapter" => "mysql", + "host" => $_ENV['DB_HOST'], + "name" => $_ENV['DB_NAME'], + "user" => $_ENV['DB_USER'], + "pass" => $_ENV['DB_PASS'], + "port" => $_ENV['DB_PORT'], + ] + ] + ]; + +Phinx auto-detects which language parser to use for files with ``*.yml`` and ``*.php`` extensions. The appropriate +parser may also be specified via the ``--parser`` and ``-p`` parameters. Anything other than ``"php"`` is treated as YAML. + +When using a PHP array, you can provide a ``connection`` key with an existing PDO instance. It is also important to pass +the database name too, as Phinx requires this for certain methods such as ``hasTable()``: + +.. code-block:: php + + [ + "migrations" => "application/migrations" + ], + "environments" => [ + "default_migration_table" => "phinxlog", + "default_environment" => "dev", + "dev" => [ + "name" => "dev_db", + "connection" => $pdo_instance + ] + ] + ]; + +Running Phinx in a Web App +-------------------------- + +Phinx can also be run inside of a web application by using the ``Phinx\Wrapper\TextWrapper`` +class. An example of this is provided in ``app/web.php``, which can be run as a +standalone server: + +.. code-block:: bash + + $ php -S localhost:8000 vendor/robmorgan/phinx/app/web.php + +This will create local web server at ``__ which will show current +migration status by default. To run migrations up, use ``__ +and to rollback use ``__. + +**The included web app is only an example and should not be used in production!** + +.. note:: + + To modify configuration variables at runtime and override ``%%PHINX_DBNAME%%`` + or other another dynamic option, set ``$_SERVER['PHINX_DBNAME']`` before + running commands. Available options are documented in the Configuration page. + +Using Phinx with PHPUnit +-------------------------- + +Phinx can be used within your unit tests to prepare or seed the database. You can use it programatically : + +.. code-block:: php + + public function setUp () + { + $app = new PhinxApplication(); + $app->setAutoExit(false); + $app->run(new StringInput('migrate'), new NullOutput()); + } + +If you use a memory database, you'll need to give Phinx a specific PDO instance. You can interact with Phinx directly using the Manager class : + +.. code-block:: php + + use PDO; + use Phinx\Config\Config; + use Phinx\Migration\Manager; + use PHPUnit\Framework\TestCase; + use Symfony\Component\Console\Input\StringInput; + use Symfony\Component\Console\Output\NullOutput; + + class DatabaseTestCase extends TestCase { + + public function setUp () + { + $pdo = new PDO('sqlite::memory:', null, null, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]); + $configArray = require('phinx.php'); + $configArray['environments']['test'] = [ + 'adapter' => 'sqlite', + 'connection' => $pdo + ]; + $config = new Config($configArray); + $manager = new Manager($config, new StringInput(' '), new NullOutput()); + $manager->migrate('test'); + $manager->seed('test'); + // You can change default fetch mode after the seeding + $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + $this->pdo = $pdo; + } + + } diff --git a/vendor/robmorgan/phinx/docs/fr/conf.py b/vendor/robmorgan/phinx/docs/fr/conf.py new file mode 100644 index 0000000..b02032e --- /dev/null +++ b/vendor/robmorgan/phinx/docs/fr/conf.py @@ -0,0 +1,9 @@ +import sys, os + +# Append the top level directory of the docs, so we can import from the config dir. +sys.path.insert(0, os.path.abspath('..')) + +# Pull in all the configuration options defined in the global config file.. +from config.all import * + +language = 'fr' diff --git a/vendor/robmorgan/phinx/docs/fr/configuration.rst b/vendor/robmorgan/phinx/docs/fr/configuration.rst new file mode 100644 index 0000000..bf9142a --- /dev/null +++ b/vendor/robmorgan/phinx/docs/fr/configuration.rst @@ -0,0 +1,308 @@ +.. index:: + single: Configuration + +Configuration +============= + +When you initialize your project using the :doc:`Init Command`, Phinx +creates a default file called ``phinx.yml`` in the root of your project directory. +This file uses the YAML data serialization format. + +If a ``--configuration`` command line option is given, Phinx will load the +specified file. Otherwise, it will attempt to find ``phinx.php``, ``phinx.json``, +``phinx.yml`` or ``phinx.yaml`` and load the first file found. See the +:doc:`Commands ` chapter for more information. + +.. warning:: + + Remember to store the configuration file outside of a publicly accessible + directory on your webserver. This file contains your database credentials + and may be accidentally served as plain text. + +Note that while JSON and YAML files are *parsed*, the PHP file is *included*. +This means that: + +* It must `return` an array of configuration items. +* The variable scope is local, i.e. you would need to explicitly declare + any global variables your initialization file reads or modifies. +* Its standard output is suppressed. +* Unlike with JSON and YAML, it is possible to omit environment connection details + and instead specify ``connection`` which must contain an initialized PDO instance. + This is useful when you want your migrations to interact with your application + and/or share the same connection. However remember to also pass the database name + as Phinx cannot infer this from the PDO connection. + +:: + + getDatabase()->getPdo(); + + return ['environments' => + [ + 'default_environment' => 'development', + 'development' => [ + 'name' => 'devdb', + 'connection' => $pdo, + ] + ] + ]; + +Migration Paths +--------------- + +The first option specifies the path to your migration directory. Phinx uses +``%%PHINX_CONFIG_DIR%%/db/migrations`` by default. + +.. note:: + + ``%%PHINX_CONFIG_DIR%%`` is a special token and is automatically replaced + with the root directory where your ``phinx.yml`` file is stored. + +In order to overwrite the default ``%%PHINX_CONFIG_DIR%%/db/migrations``, you +need to add the following to the yaml configuration. + +.. code-block:: yaml + + paths: + migrations: /your/full/path + +You can also provide multiple migration paths by using an array in your configuration: + +.. code-block:: yaml + + paths: + migrations: + - application/module1/migrations + - application/module2/migrations + +You can also use the ``%%PHINX_CONFIG_DIR%%`` token in your path. + +.. code-block:: yaml + + paths: + migrations: '%%PHINX_CONFIG_DIR%%/your/relative/path' + +Migrations are captured with ``glob``, so you can define a pattern for multiple +directories. + +.. code-block:: yaml + + paths: + migrations: '%%PHINX_CONFIG_DIR%%/module/*/{data,scripts}/migrations' + +Custom Migration Base +--------------------- + +By default all migrations will extend from Phinx's ``AbstractMigration`` class. +This can be set to a custom class that extends from ``AbstractMigration`` by +setting ``migration_base_class`` in your config: + +.. code-block:: yaml + + migration_base_class: MyMagicalMigration + +Seed Paths +---------- + +The second option specifies the path to your seed directory. Phinx uses +``%%PHINX_CONFIG_DIR%%/db/seeds`` by default. + +.. note:: + + ``%%PHINX_CONFIG_DIR%%`` is a special token and is automatically replaced + with the root directory where your ``phinx.yml`` file is stored. + +In order to overwrite the default ``%%PHINX_CONFIG_DIR%%/db/seeds``, you +need to add the following to the yaml configuration. + +.. code-block:: yaml + + paths: + seeds: /your/full/path + +You can also provide multiple seed paths by using an array in your configuration: + +.. code-block:: yaml + + paths: + seeds: + - /your/full/path1 + - /your/full/path2 + +You can also use the ``%%PHINX_CONFIG_DIR%%`` token in your path. + +.. code-block:: yaml + + paths: + seeds: '%%PHINX_CONFIG_DIR%%/your/relative/path' + +Environments +------------ + +One of the key features of Phinx is support for multiple database environments. +You can use Phinx to create migrations on your development environment, then +run the same migrations on your production environment. Environments are +specified under the ``environments`` nested collection. For example: + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + host: localhost + name: production_db + user: root + pass: '' + port: 3306 + charset: utf8 + collation: utf8_unicode_ci + +would define a new environment called ``production``. + +In a situation when multiple developers work on the same project and each has +a different environment (e.g. a convention such as ``--``), or when you need to have separate +environments for separate purposes (branches, testing, etc) use environment +variable `PHINX_ENVIRONMENT` to override the default environment in the yaml +file: + +.. code-block:: bash + + export PHINX_ENVIRONMENT=dev-`whoami`-`hostname` + +Table Prefix and Suffix +----------------------- + +You can define a table prefix and table suffix: + +.. code-block:: yaml + + environments: + development: + .... + table_prefix: dev_ + table_suffix: _v1 + testing: + .... + table_prefix: test_ + table_suffix: _v2 + +Socket Connections +------------------ + +When using the MySQL adapter, it is also possible to use sockets instead of +network connections. The socket path is configured with ``unix_socket``: + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + name: production_db + user: root + pass: '' + unix_socket: /var/run/mysql/mysql.sock + charset: utf8 + +External Variables +------------------ + +Phinx will automatically grab any environment variable prefixed with ``PHINX_`` +and make it available as a token in the config file. The token will have +exactly the same name as the variable but you must access it by wrapping two +``%%`` symbols on either side. e.g: ``%%PHINX_DBUSER%%``. This is especially +useful if you wish to store your secret database credentials directly on the +server and not in a version control system. This feature can be easily +demonstrated by the following example: + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + host: '%%PHINX_DBHOST%%' + name: '%%PHINX_DBNAME%%' + user: '%%PHINX_DBUSER%%' + pass: '%%PHINX_DBPASS%%' + port: 3306 + charset: utf8 + +Supported Adapters +------------------ + +Phinx currently supports the following database adapters natively: + +* `MySQL `_: specify the ``mysql`` adapter. +* `PostgreSQL `_: specify the ``pgsql`` adapter. +* `SQLite `_: specify the ``sqlite`` adapter. +* `SQL Server `_: specify the ``sqlsrv`` adapter. + +SQLite +~~~~~~ + +Declaring an SQLite database uses a simplified structure: + +.. code-block:: yaml + + environments: + development: + adapter: sqlite + name: ./data/derby + testing: + adapter: sqlite + memory: true # Setting memory to *any* value overrides name + +SQL Server +~~~~~~~~~~ + +When using the ``sqlsrv`` adapter and connecting to a named instance you should +omit the ``port`` setting as SQL Server will negotiate the port automatically. +Additionally, omit the ``charset: utf8`` or change to ``charset: 65001`` which +corresponds to UTF8 for SQL Server. + +Custom Adapters +~~~~~~~~~~~~~~~ + +You can provide a custom adapter by registering an implementation of the +``Phinx\\Db\\Adapter\\AdapterInterface`` with ``AdapterFactory``: + +.. code-block:: php + + $name = 'fizz'; + $class = 'Acme\Adapter\FizzAdapter'; + + AdapterFactory::instance()->registerAdapter($name, $class); + +Adapters can be registered any time before `$app->run()` is called, which normally +called by `bin/phinx`. + +Aliases +------- + +Template creation class names can be aliased and used with the ``--class`` command line option for the :doc:`Create Command `. + +The aliased classes will still be required to implement the ``Phinx\Migration\CreationInterface`` interface. + +.. code-block:: yaml + + aliases: + permission: \Namespace\Migrations\PermissionMigrationTemplateGenerator + view: \Namespace\Migrations\ViewMigrationTemplateGenerator + +Version Order +------------- + +When rolling back or printing the status of migrations, Phinx orders the executed migrations according to the +``version_order`` option, which can have the following values: + +* ``creation`` (the default): migrations are ordered by their creation time, which is also part of their filename. +* ``execution``: migrations are ordered by their execution time, also known as start time. diff --git a/vendor/robmorgan/phinx/docs/fr/contents.rst b/vendor/robmorgan/phinx/docs/fr/contents.rst new file mode 100644 index 0000000..e5dae97 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/fr/contents.rst @@ -0,0 +1,11 @@ +Contenus +======== + +.. toctree:: + :maxdepth: 2 + + index + migrations + seeding + commands + configuration diff --git a/vendor/robmorgan/phinx/docs/fr/index.rst b/vendor/robmorgan/phinx/docs/fr/index.rst new file mode 100644 index 0000000..ac9d0a0 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/fr/index.rst @@ -0,0 +1,57 @@ +Phinx Migrations +################ + +Phinx est un utilitaire autonome en ligne de commande pour gérer les Migrations de bases de données. Le plugin officiel de Migrations de CakePHP est basé sur cet outil. + +Phinx rend ridiculement simple la gestion des migrations de bases de données pour vos applications PHP. En moins de 5 minutes, +vous pouvez installer Composer et créer votre première migration de base de données. Phinx ne s'occupe que de la migration des +bases de données, il laisse de côté les aspects ORM de la base de données et le cadre applicatif. + +Introduction +============ + +Les bons développeurs gèrent toujours leurs codes sources avec un outil de gestion de versions, +alors pourquoi ne feraient-ils pas la même chose avec leurs schémas de bases de données. + +Phinx permet aux développeurs de modifier et de manipuler leurs bases de données d'une façon claire et concise. Il évite +d'avoir à écrire du SQL à la main et offre à la place une API puissante pour écrire des scripts de migration en utilisant PHP. +Les développeurs peuvent alors versionner ces fichiers de migration en utilisant leur outil de versionnement préféré. Cela rend +les migrations Phinx indépendantes des moteurs de base de données. Phinx conserve la trace des migrations précédentes, cela +permet de se concentrer un peu plus sur l'amélioration de votre application et un peu moins sur l'état de votre base de +données. + +Objectifs +========= + +Phinx a été développé avec les objectifs suivants en tête : + +* Être portable entre les principaux moteurs de base de données. +* Être indépendant de tout framework PHP. +* Être aisement installable. +* Être utilisable facilement en ligne de commande. +* Être intégrable avec d'autres outils PHP (Phing, PHPUnit) et des frameworks web. + +Installation +============ + +Phinx devrait être installé en utilisant Composer, qui est un outil pour la gestion des dépendances en PHP. Visiter le site +internet de `Composer `_ pour avoir plus d’informations. + +.. note:: + + Phinx a besoin au minimum de PHP 5.4 (ou supérieur) + +Pour installer Phinx, il suffit simplement de l'appeler via Composer + +.. code-block:: bash + + php composer.phar require robmorgan/phinx + +Créez les dossiers ``db/migrations`` dans votre projet en vous assurant que les droits sont bien configurés. +C’est à cet endroit que vos fichiers de migrations devraient être créés et laissés. + +Phinx peut maintenant être exécuté depuis la racine de votre projet. + +.. code-block:: bash + + vendor/bin/phinx init diff --git a/vendor/robmorgan/phinx/docs/fr/migrations.rst b/vendor/robmorgan/phinx/docs/fr/migrations.rst new file mode 100644 index 0000000..46a9bf9 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/fr/migrations.rst @@ -0,0 +1,1305 @@ +.. index:: + single: Writing Migrations + +Writing Migrations +================== + +Phinx relies on migrations in order to transform your database. Each migration +is represented by a PHP class in a unique file. It is preferred that you write +your migrations using the Phinx PHP API, but raw SQL is also supported. + +Creating a New Migration +------------------------ + +Generating a skeleton migration file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's start by creating a new Phinx migration. Run Phinx using the ``create`` +command: + +.. code-block:: bash + + $ php vendor/bin/phinx create MyNewMigration + +This will create a new migration in the format +``YYYYMMDDHHMMSS_my_new_migration.php``, where the first 14 characters are +replaced with the current timestamp down to the second. + +If you have specified multiple migration paths, you will be asked to select +which path to create the new migration in. + +Phinx automatically creates a skeleton migration file with a single method:: + + table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } + + /** + * Migrate Up. + */ + public function up() + { + + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +When executing this migration, Phinx will create the ``user_logins`` table on +the way up and automatically figure out how to drop the table on the way down. +Please be aware that when a ``change`` method exists, Phinx will automatically +ignore the ``up`` and ``down`` methods. If you need to use these methods it is +recommended to create a separate migration file. + +.. note:: + + When creating or updating tables inside a ``change()`` method you must use + the Table ``create()`` and ``update()`` methods. Phinx cannot automatically + determine whether a ``save()`` call is creating a new table or modifying an + existing one. + +Phinx can only reverse the following commands: + +- createTable +- renameTable +- addColumn +- renameColumn +- addIndex +- addForeignKey + +If a command cannot be reversed then Phinx will throw a +``IrreversibleMigrationException`` exception when it's migrating down. + +The Up Method +~~~~~~~~~~~~~ + +The up method is automatically run by Phinx when you are migrating up and it +detects the given migration hasn't been executed previously. You should use the +up method to transform the database with your intended changes. + +The Down Method +~~~~~~~~~~~~~~~ + +The down method is automatically run by Phinx when you are migrating down and +it detects the given migration has been executed in the past. You should use +the down method to reverse/undo the transformations described in the up method. + +Executing Queries +----------------- + +Queries can be executed with the ``execute()`` and ``query()`` methods. The +``execute()`` method returns the number of affected rows whereas the +``query()`` method returns the result as a +`PDOStatement `_:: + + execute('DELETE FROM users'); // returns the number of affected rows + + // query() + $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement + $rows = $stmt->fetchAll(); // returns the result as an array + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +.. note:: + + These commands run using the PHP Data Objects (PDO) extension which + defines a lightweight, consistent interface for accessing databases + in PHP. Always make sure your queries abide with PDOs before using + the ``execute()`` command. This is especially important when using + DELIMITERs during insertion of stored procedures or triggers which + don't support DELIMITERs. + +.. warning:: + + When using ``execute()`` or ``query()`` with a batch of queries, PDO doesn't + throw an exception if there is an issue with one or more of the queries + in the batch. + + As such, the entire batch is assumed to have passed without issue. + + If Phinx was to iterate any potential result sets, looking to see if one + had an error, then Phinx would be denying access to all the results as there + is no facility in PDO to get a previous result set + `nextRowset() `_ - + but no ``previousSet()``). + + So, as a consequence, due to the design decision in PDO to not throw + an exception for batched queries, Phinx is unable to provide the fullest + support for error handling when batches of queries are supplied. + + Fortunately though, all the features of PDO are available, so multiple batches + can be controlled within the migration by calling upon + `nextRowset() `_ + and examining `errorInfo `_. + +Fetching Rows +------------- + +There are two methods available to fetch rows. The ``fetchRow()`` method will +fetch a single row, whilst the ``fetchAll()`` method will return multiple rows. +Both methods accept raw SQL as their only parameter:: + + fetchRow('SELECT * FROM users'); + + // fetch an array of messages + $rows = $this->fetchAll('SELECT * FROM messages'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Inserting Data +-------------- + +Phinx makes it easy to insert data into your tables. Whilst this feature is +intended for the :doc:`seed feature `, you are also free to use the +insert methods in your migrations:: + + 1, + 'name' => 'In Progress' + ]; + + $table = $this->table('status'); + $table->insert($singleRow); + $table->saveData(); + + // inserting multiple rows + $rows = [ + [ + 'id' => 2, + 'name' => 'Stopped' + ], + [ + 'id' => 3, + 'name' => 'Queued' + ] + ]; + + // this is a handy shortcut + $this->insert('status', $rows); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->execute('DELETE FROM status'); + } + } + +.. note:: + + You cannot use the insert methods inside a ``change()`` method. Please use the + ``up()`` and ``down()`` methods. + +Working With Tables +------------------- + +The Table Object +~~~~~~~~~~~~~~~~ + +The Table object is one of the most useful APIs provided by Phinx. It allows +you to easily manipulate database tables using PHP code. You can retrieve an +instance of the Table object by calling the ``table()`` method from within +your database migration:: + + table('tableName'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +You can then manipulate this table using the methods provided by the Table +object. + +The Save Method +~~~~~~~~~~~~~~~ + +When working with the Table object, Phinx stores certain operations in a +pending changes cache. + +When in doubt, it is recommended you call this method. It will commit any +pending changes to the database. + +Creating a Table +~~~~~~~~~~~~~~~~ + +Creating a table is really easy using the Table object. Let's create a table to +store a collection of users:: + + table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Columns are added using the ``addColumn()`` method. We create a unique index +for both the username and email columns using the ``addIndex()`` method. +Finally calling ``save()`` commits the changes to the database. + +.. note:: + + Phinx automatically creates an auto-incrementing primary key column called ``id`` for every + table. + +The ``id`` option sets the name of the automatically created identity field, while the ``primary_key`` +option selects the field or fields used for primary key. ``id`` will always override the ``primary_key`` +option unless it's set to false. If you don't need a primary key set ``id`` to false without +specifying a ``primary_key``, and no primary key will be created. + +To specify an alternate primary key, you can specify the ``primary_key`` option +when accessing the Table object. Let's disable the automatic ``id`` column and +create a primary key using two columns instead:: + + table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('created', 'datetime') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Setting a single ``primary_key`` doesn't enable the ``AUTO_INCREMENT`` option. +To simply change the name of the primary key, we need to override the default +``id`` field name:: + + table('followers', ['id' => 'user_id']); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +In addition, the MySQL adapter supports following options: + +========= =========== +Option Description +========= =========== +comment set a text comment on the table +engine define table engine *(defaults to `InnoDB`)* +collation define table collation *(defaults to `utf8_general_ci`)* +signed whether the primary key is ``signed`` +========= =========== + +By default the primary key is ``signed``. +To simply set it to unsigned just pass ``signed`` option with a ``false`` +value:: + + table('followers', ['signed' => false]); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Valid Column Types +~~~~~~~~~~~~~~~~~~ + +Column types are specified as strings and can be one of: + +- biginteger +- binary +- boolean +- date +- datetime +- decimal +- float +- integer +- string +- text +- time +- timestamp +- uuid + +In addition, the MySQL adapter supports ``enum``, ``set``, ``blob`` and ``json`` +column types. (``json`` in MySQL 5.7 and above) + +In addition, the Postgres adapter supports ``smallint``, ``json``, ``jsonb``, +``uuid``, ``cidr``, ``inet`` and ``macaddr`` column types (PostgreSQL 9.3 and +above). + +For valid options, see the ref:`Valid Column Options`_ below. + +Determining Whether a Table Exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can determine whether or not a table exists by using the ``hasTable()`` +method:: + + hasTable('users'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Dropping a Table +~~~~~~~~~~~~~~~~ + +Tables can be dropped quite easily using the ``dropTable()`` method. It is a +good idea to recreate the table again in the ``down()`` method:: + + dropTable('users'); + } + + /** + * Migrate Down. + */ + public function down() + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } + } + +Renaming a Table +~~~~~~~~~~~~~~~~ + +To rename a table access an instance of the Table object then call the +``rename()`` method:: + + table('users'); + $table->rename('legacy_users'); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('legacy_users'); + $table->rename('users'); + } + } + +Working With Columns +-------------------- + +.. _valid-column-types: + +Valid Column Types +~~~~~~~~~~~~~~~~~~ + +Column types are specified as strings and can be one of: + +- biginteger +- binary +- boolean +- char +- date +- datetime +- decimal +- float +- integer +- string +- text +- time +- timestamp +- uuid + +In addition, the MySQL adapter supports ``enum``, ``set`` and ``blob`` column types. + +In addition, the Postgres adapter supports ``smallint``, ``json``, ``jsonb``, ``uuid``, ``cidr``, ``inet`` and ``macaddr`` column types +(PostgreSQL 9.3 and above). + +Valid Column Options +~~~~~~~~~~~~~~~~~~~~ + +The following are valid column options: + +For any column type: + +======= =========== +Option Description +======= =========== +limit set maximum length for strings, also hints column types in adapters (see note below) +length alias for ``limit`` +default set default value or action +null allow ``NULL`` values (should not be used with primary keys!) +after specify the column that a new column should be placed after +comment set a text comment on the column +======= =========== + +For ``decimal`` columns: + +========= =========== +Option Description +========= =========== +precision combine with ``scale`` set to set decimal accuracy +scale combine with ``precision`` to set decimal accuracy +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +========= =========== + +For ``enum`` and ``set`` columns: + +========= =========== +Option Description +========= =========== +values Can be a comma separated list or an array of values +========= =========== + +For ``integer`` and ``biginteger`` columns: + +======== =========== +Option Description +======== =========== +identity enable or disable automatic incrementing +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +======== =========== + +For ``timestamp`` columns: + +======== =========== +Option Description +======== =========== +default set default value (use with ``CURRENT_TIMESTAMP``) +update set an action to be triggered when the row is updated (use with ``CURRENT_TIMESTAMP``) +timezone enable or disable the ``with time zone`` option for ``time`` and ``timestamp`` columns *(only applies to Postgres)* +======== =========== + +You can add ``created_at`` and ``updated_at`` timestamps to a table using the +``addTimestamps()`` method. This method also allows you to supply alternative +names:: + + table('users')->addTimestamps(null, 'amended_at')->create(); + } + } + +For ``boolean`` columns: + +======== =========== +Option Description +======== =========== +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +======== =========== + +For ``string`` and ``text`` columns: + +========= =========== +Option Description +========= =========== +collation set collation that differs from table defaults *(only applies to MySQL)* +encoding set character set that differs from table defaults *(only applies to MySQL)* +========= =========== + +For foreign key definitions: + +====== =========== +Option Description +====== =========== +update set an action to be triggered when the row is updated +delete set an action to be triggered when the row is deleted +====== =========== + +You can pass one or more of these options to any column with the optional +third argument array. + +Limit Option and PostgreSQL +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using the PostgreSQL adapter, additional hinting of database column type can be +made for ``integer`` columns. Using ``limit`` with one the following options will +modify the column type accordingly: + +============ ============== +Limit Column Type +============ ============== +INT_SMALL SMALLINT +============ ============== + +.. code-block:: php + + use Phinx\Db\Adapter\PostgresAdapter; + + //... + + $table = $this->table('cart_items'); + $table->addColumn('user_id', 'integer') + ->addColumn('subtype_id', 'integer', ['limit' => PostgresAdapter::INT_SMALL]) + ->create(); + +Limit Option and MySQL +~~~~~~~~~~~~~~~~~~~~~~ + +When using the MySQL adapter, additional hinting of database column type can be +made for ``integer``, ``text`` and ``blob`` columns. Using ``limit`` with +one the following options will modify the column type accordingly: + +============ ============== +Limit Column Type +============ ============== +BLOB_TINY TINYBLOB +BLOB_REGULAR BLOB +BLOB_MEDIUM MEDIUMBLOB +BLOB_LONG LONGBLOB +TEXT_TINY TINYTEXT +TEXT_REGULAR TEXT +TEXT_MEDIUM MEDIUMTEXT +TEXT_LONG LONGTEXT +INT_TINY TINYINT +INT_SMALL SMALLINT +INT_MEDIUM MEDIUMINT +INT_REGULAR INT +INT_BIG BIGINT +============ ============== + +.. code-block:: php + + use Phinx\Db\Adapter\MysqlAdapter; + + //... + + $table = $this->table('cart_items'); + $table->addColumn('user_id', 'integer') + ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) + ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) + ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) + ->create(); + +Get a column list +~~~~~~~~~~~~~~~~~ + +To retrieve all table columns, simply create a `table` object and call `getColumns()` +method. This method will return an array of Column classes with basic info. Example below:: + + table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down() + { + ... + } + } + +Checking whether a column exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can check if a table already has a certain column by using the +``hasColumn()`` method:: + + table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // do something + } + + } + } + +Renaming a Column +~~~~~~~~~~~~~~~~~ + +To rename a column, access an instance of the Table object then call the +``renameColumn()`` method:: + + table('users'); + $table->renameColumn('bio', 'biography'); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('users'); + $table->renameColumn('biography', 'bio'); + } + } + +Adding a Column After Another Column +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When adding a column you can dictate its position using the ``after`` option:: + + table('users'); + $table->addColumn('city', 'string', ['after' => 'email']) + ->update(); + } + } + +Dropping a Column +~~~~~~~~~~~~~~~~~ + +To drop a column, use the ``removeColumn()`` method:: + + table('users'); + $table->removeColumn('short_name') + ->save(); + } + } + +Specifying a Column Limit +~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can limit the maximum length of a column by using the ``limit`` option:: + + table('tags'); + $table->addColumn('short_name', 'string', ['limit' => 30]) + ->update(); + } + } + +Changing Column Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To change column type or options on an existing column, use the +``changeColumn()`` method. See :ref:`valid-column-types` and `Valid Column +Options`_ for allowed values:: + + table('users'); + $users->changeColumn('email', 'string', ['limit' => 255]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Working With Indexes +-------------------- + +To add an index to a table you can simply call the ``addIndex()`` method on the +table object:: + + table('users'); + $table->addColumn('city', 'string') + ->addIndex(['city']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +By default Phinx instructs the database adapter to create a normal index. We +can pass an additional parameter ``unique`` to the ``addIndex()`` method to +specify a unique index. We can also explicitly specify a name for the index +using the ``name`` parameter:: + + table('users'); + $table->addColumn('email', 'string') + ->addIndex(['email'], ['unique' => true, 'name' => 'idx_users_email']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +The MySQL adapter also supports ``fulltext`` indexes. If you are using a version +before 5.6 you must ensure the table uses the ``MyISAM`` engine:: + + table('users', ['engine' => 'MyISAM']); + $table->addColumn('email', 'string') + ->addIndex('email', ['type' => 'fulltext']) + ->create(); + } + } + +Removing indexes is as easy as calling the ``removeIndex()`` method. You must +call this method for each index:: + + table('users'); + $table->removeIndex(['email']); + + // alternatively, you can delete an index by its name, ie: + $table->removeIndexByName('idx_users_email'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +.. note:: + + There is no need to call the ``save()`` method when using + ``removeIndex()``. The index will be removed immediately. + +Working With Foreign Keys +------------------------- + +Phinx has support for creating foreign key constraints on your database tables. +Let's add a foreign key to an example table:: + + table('tags'); + $table->addColumn('tag_name', 'string') + ->save(); + + $refTable = $this->table('tag_relationships'); + $refTable->addColumn('tag_id', 'integer') + ->addForeignKey('tag_id', 'tags', 'id', ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION']) + ->save(); + + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +"On delete" and "On update" actions are defined with a 'delete' and 'update' +options array. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and +'RESTRICT'. Constraint name can be changed with the 'constraint' option. + +It is also possible to pass ``addForeignKey()`` an array of columns. This +allows us to establish a foreign key relationship to a table which uses +a combined key:: + + table('follower_events'); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('event_id', 'integer') + ->addForeignKey(['user_id', 'follower_id'], + 'followers', + ['user_id', 'follower_id'], + ['delete'=> 'NO_ACTION', 'update'=> 'NO_ACTION', 'constraint' => 'user_follower_id']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +We can add named foreign keys using the ``constraint`` parameter. This feature +is supported as of Phinx version 0.6.5:: + + table('your_table'); + $table->addForeignKey('foreign_id', 'reference_table', ['id'], + ['constraint'=>'your_foreign_key_name']); + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +We can also easily check if a foreign key exists:: + + table('tag_relationships'); + $exists = $table->hasForeignKey('tag_id'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Finally, to delete a foreign key, use the ``dropForeignKey`` method:: + + table('tag_relationships'); + $table->dropForeignKey('tag_id'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } diff --git a/vendor/robmorgan/phinx/docs/fr/seeding.rst b/vendor/robmorgan/phinx/docs/fr/seeding.rst new file mode 100644 index 0000000..2352285 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/fr/seeding.rst @@ -0,0 +1,217 @@ +.. index:: + single: Database Seeding + +Database Seeding +================ + +In version 0.5.0 Phinx introduced support for seeding your database with test +data. Seed classes are a great way to easily fill your database with data after +it's created. By default they are stored in the ``seeds`` directory; however, +this path can be changed in your configuration file. + +.. note:: + + Database seeding is entirely optional, and Phinx does not create a ``seeds`` + directory by default. + +Creating a New Seed Class +------------------------- + +Phinx includes a command to easily generate a new seed class: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:create UserSeeder + +If you have specified multiple seed paths, you will be asked to select which +path to create the new seed class in. + +It is based on a skeleton template:: + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->save(); + } + } + +.. note:: + + You must call the ``save()`` method to commit your data to the table. Phinx + will buffer data until you do so. + +Integrating with the Faker library +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's trivial to use the awesome +`Faker library `_ in your seed classes. +Simply install it using Composer: + +.. code-block:: bash + + $ composer require fzaninotto/faker + +Then use it in your seed classes:: + + $faker->userName, + 'password' => sha1($faker->password), + 'password_salt' => sha1('foo'), + 'email' => $faker->email, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'created' => date('Y-m-d H:i:s'), + ]; + } + + $this->insert('users', $data); + } + } + +Truncating Tables +----------------- + +In addition to inserting data Phinx makes it trivial to empty your tables using +the SQL ``TRUNCATE`` command:: + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->save(); + + // empty the table + $posts->truncate(); + } + } + +.. note:: + + SQLite doesn't natively support the ``TRUNCATE`` command so behind the scenes + ``DELETE FROM`` is used. It is recommended to call the ``VACUUM`` command + after truncating a table. Phinx does not do this automatically. + +Executing Seed Classes +---------------------- + +This is the easy part. To seed your database, simply use the ``seed:run`` command: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run + +By default, Phinx will execute all available seed classes. If you would like to +run a specific class, simply pass in the name of it using the ``-s`` parameter: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -s UserSeeder + +You can also run multiple seeders: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -s UserSeeder -s PermissionSeeder -s LogSeeder + +You can also use the ``-v`` parameter for more output verbosity: + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -v + +The Phinx seed functionality provides a simple mechanism to easily and repeatably +insert test data into your database. diff --git a/vendor/robmorgan/phinx/docs/ja/commands.rst b/vendor/robmorgan/phinx/docs/ja/commands.rst new file mode 100644 index 0000000..51c7bf2 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/ja/commands.rst @@ -0,0 +1,338 @@ +.. index:: + single: Commands + +コマンド +######## + +Phinx は多くのコマンドを使用して実行されます。 + +マイグレーションコマンド +======================== + +init コマンド +---------------- + +init コマンド (initialize の略) は、Phinx のプロジェクトを準備するために使用されます。 +このコマンドはプロジェクトディレクトリーのルートに ``phinx.yml`` ファイルを生成します。 + +.. code-block:: bash + + $ cd yourapp + $ phinx init . + +このファイルをテキストエディターで開き、プロジェクトの設定を行います。 +詳しくは :doc:`設定 ` の章をご覧下さい。 + +create コマンド +--------------- + +create コマンドは、新しいマイグレーションファイルを作成するために使用されます。 +1つの引数、つまりマイグレーション名が必要です。 +マイグレーション名は、キャメルケース形式で指定する必要があります。 + +.. code-block:: bash + + $ phinx create MyNewMigration + +新しいマイグレーションファイルをテキストエディターで開き、データベースの変更を追加します。 +Phinx は ``phinx.yml`` ファイルで指定されたパスを使ってマイグレーションファイルを作成します。 +詳しくは :doc:`設定 ` の章をご覧下さい。 + +代替のテンプレートファイル名を指定することで、 +Phinx が使用するテンプレートファイルを上書きすることができます。 + +.. code-block:: bash + + $ phinx create MyNewMigration --template="" + +テンプレート生成クラスを提供することもできます。このクラスはインタフェース +``Phinx\Migration\CreationInterface`` を実装しなければなりません。 + +.. code-block:: bash + + $ phinx create MyNewMigration --class="" + +マイグレーションのテンプレートを提供するだけでなく、クラスはマイグレーションファイルが +テンプレートから生成されると呼び出されるコールバックを定義することもできます。 + +``--template`` と ``--class`` の両方を使用することはできません。 + +migrate コマンド +---------------- + +migrate コマンドは、すべての使用可能なマイグレーションを実行します。 +オプションで特定のバージョンまで実行できます。 + +.. code-block:: bash + + $ phinx migrate -e development + +特定のバージョンにマイグレーションするには、 ``--target`` パラメーターまたは省略して +``-t`` を使用します。 + +.. code-block:: bash + + $ phinx migrate -e development -t 20110103081132 + +``--dry-run`` を使って、クエリーを実行せずに標準出力に出力します。 + +.. code-block:: bash + + $ phinx migrate --dry-run + +rollback コマンド +----------------- + +rollback コマンドは、Phinx によって実行された以前のマイグレーションを取り消すために使用されます。 +これは、migrate コマンドの反対です。 + +引数を指定せずに ``rollback`` コマンドを使用すると、以前の移行にロールバックすることができます。 + +.. code-block:: bash + + $ phinx rollback -e development + +すべてのマイグレーションを特定のバージョンにロールバックするには、 ``--target`` パラメーターまたは +省略して ``-t`` を使用します。 + +.. code-block:: bash + + $ phinx rollback -e development -t 20120103083322 + +ターゲットバージョンとして 0 を指定すると、すべてのマイグレーションが元に戻ります。 + +.. code-block:: bash + + $ phinx rollback -e development -t 0 + +すべてのマイグレーションを特定の日付にロールバックするには、 ``--date`` パラメーターまたは省略して +``-d`` を省略して使用します。 + +.. code-block:: bash + + $ phinx rollback -e development -d 2012 + $ phinx rollback -e development -d 201201 + $ phinx rollback -e development -d 20120103 + $ phinx rollback -e development -d 2012010312 + $ phinx rollback -e development -d 201201031205 + $ phinx rollback -e development -d 20120103120530 + +ブレークポイントが設定され、さらにロールバックをブロックしている場合は、 ``--force`` パラメーターまたは +``-f`` を使ってブレークポイントをオーバーライドすることができます。 + +.. code-block:: bash + + $ phinx rollback -e development -t 0 -f + +``--dry-run`` を使って、クエリーを実行せずに標準出力に出力します。 + +.. code-block:: bash + + $ phinx rollback --dry-run + +.. note:: + + ロールバックすると、Phinx は ``phinx.yml`` ファイルの ``version_order`` オプションで + 指定された順序で実行されたマイグレーションを処理します。 + 詳しくは :doc:`設定 ` の章をご覧下さい。 + +status コマンド +--------------- + +status コマンドは、すべてのマイグレーションのリストを現在のステータスとともに表示します。 +このコマンドを使用して、実行されたマイグレーションを確認できます。 + +.. code-block:: bash + + $ phinx status -e development + +このコマンドは、データベースが最新の場合(つまり、すべてのマイグレーションが稼働している場合) +コード0で終了します。またはそれ以外の場合は、次のコードのいずれかで終了します。 + +* 1: 実行されるマイグレーションが少なくとも1つ残っています。 +* 2: マイグレーションが実行され、データベースに記録されましたが、マイグレーションファイルが有りません。 + +breakpoint コマンド +------------------- + +breakpoint コマンドは、ブレークポイントを設定するために使用され、ロールバックを制限することができます。 +最新のマイグレーションのブレークポイントは、パラメーターを指定しないで切り替えることができます。 + +.. code-block:: bash + + $ phinx breakpoint -e development + +特定のバージョンでブレークポイントを切り替えるには、 ``--target`` パラメーターまたは省略して +``-t`` を使用します。 + +.. code-block:: bash + + $ phinx breakpoint -e development -t 20120103083322 + +全てのブレークポイントを削除するには、 ``--remove-all`` パラメーターまたは省略して +``-r`` を使用します。 + +.. code-block:: bash + + $ phinx breakpoint -e development -r + +ブレークポイントは、 ``status`` コマンドを実行すると表示されます。 + +データベースの初期データ投入 +============================ + +seed:create コマンド +-------------------- + +seed:create コマンドを使用して、新しいデータベースシードクラスを作成できます。 +1つの引数、クラスの名前が必要です。クラス名はキャメルケース形式で指定する必要があります。 + +.. code-block:: bash + + $ phinx seed:create MyNewSeeder + +テキストエディターで新しいシードファイルを開き、データベースシードコマンドを追加します。 +Phinx は ``phinx.yml`` ファイルで指定されたパスを使ってシードファイルを作成します。 +詳しくは :doc:`設定 ` の章をご覧下さい。 + +seed:run コマンド +----------------- + +seed:run コマンドは、使用可能なすべてのシードクラスを実行するか、オプションで1つだけを実行します。 + +.. code-block:: bash + + $ phinx seed:run -e development + +シードクラスを1つだけ実行するには、 ``--seed`` パラメーターまたは省略して ``-s`` を使用します。 + +.. code-block:: bash + + $ phinx seed:run -e development -s MyNewSeeder + +設定ファイルパラメーター +------------------------ + +コマンドラインから Phinx を実行するときは、 ``--configuration`` または +``-c`` パラメーターを使って設定ファイルを指定することができます。 +YAML に加えて、設定ファイルは PHP 配列として PHP ファイルの計算された出力でもよいです。 + +.. code-block:: php + + [ + "migrations" => "application/migrations" + ], + "environments" => [ + "default_migration_table" => "phinxlog", + "default_environment" => "dev", + "dev" => [ + "adapter" => "mysql", + "host" => $_ENV['DB_HOST'], + "name" => $_ENV['DB_NAME'], + "user" => $_ENV['DB_USER'], + "pass" => $_ENV['DB_PASS'], + "port" => $_ENV['DB_PORT'], + ] + ] + ]; + +Phinx は ``*.yml`` と ``*.php`` 拡張子を持つファイルにどの言語パーサーを使うかを自動的に検出します。 +適切なパーサーは、 ``--parser`` と ``-p`` パラメーターで指定することもできます。 +``"php"`` 以外は YAML として扱われます。 + +PHP 配列を使用する場合、既存の PDO インスタンスに ``connection`` キーを提供することができます。 +Phinx は ``hasTable()`` のような特定のメソッドに対してデータベース名を必要とするため、 +データベース名も渡すことも重要です。 + +.. code-block:: php + + [ + "migrations" => "application/migrations" + ], + "environments" => [ + "default_migration_table" => "phinxlog", + "default_environment" => "dev", + "dev" => [ + "name" => "dev_db", + "connection" => $pdo_instance + ] + ] + ]; + +ウェブアプリ内で Phinx を実行 +----------------------------- + +Phinx は ``Phinx\Wrapper\TextWrapper`` クラスを使ってウェブアプリケーションの内部で +実行することもできます。この例は ``app/web.php`` で提供されています。 +これはスタンドアロンサーバーとして実行できます。 + +.. code-block:: bash + + $ php -S localhost:8000 vendor/robmorgan/phinx/app/web.php + +これはデフォルトで現在のマイグレーションの状態を表示する ``__ +にローカルウェブサーバーを作成します。マイグレーションを実行するには、 +``__ を使用し、ロールバックには +``__ を使用します。 + +**付属のウェブアプリは一例に過ぎません、本番環境では使用しないでください!** + +.. note:: + + 実行時に設定変数を変更し、 ``%%PHINX_DBNAME%%`` やその他の動的オプションを変更するには、 + コマンドを実行する前に ``$_SERVER['PHINX_DBNAME']`` を設定します。 + 使用可能なオプションは、設定ページに記載されています。 + +PHPUnit で Phinx を使用 +----------------------- + +Phinx は、ユニットテスト内でデータベースを準備またはシードするために使用できます。 +プログラムによって使用することができます。 + +.. code-block:: php + + public function setUp () + { + $app = new PhinxApplication(); + $app->setAutoExit(false); + $app->run(new StringInput('migrate'), new NullOutput()); + } + +メモリーデータベースを使用する場合は、Phinx に特定の PDO インスタンスを提供する必要があります。 +Manager クラスを使用して Phinx と直接対話することができます。 + +.. code-block:: php + + use PDO; + use Phinx\Config\Config; + use Phinx\Migration\Manager; + use PHPUnit\Framework\TestCase; + use Symfony\Component\Console\Input\StringInput; + use Symfony\Component\Console\Output\NullOutput; + + class DatabaseTestCase extends TestCase { + + public function setUp () + { + $pdo = new PDO('sqlite::memory:', null, null, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]); + $configArray = require('phinx.php'); + $configArray['environments']['test'] = [ + 'adapter' => 'sqlite', + 'connection' => $pdo + ]; + $config = new Config($configArray); + $manager = new Manager($config, new StringInput(' '), new NullOutput()); + $manager->migrate('test'); + $manager->seed('test'); + // シード後にデフォルトのフェッチモードを変更することができます + $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + $this->pdo = $pdo; + } + + } diff --git a/vendor/robmorgan/phinx/docs/ja/conf.py b/vendor/robmorgan/phinx/docs/ja/conf.py new file mode 100644 index 0000000..5871da6 --- /dev/null +++ b/vendor/robmorgan/phinx/docs/ja/conf.py @@ -0,0 +1,9 @@ +import sys, os + +# Append the top level directory of the docs, so we can import from the config dir. +sys.path.insert(0, os.path.abspath('..')) + +# Pull in all the configuration options defined in the global config file.. +from config.all import * + +language = 'ja' diff --git a/vendor/robmorgan/phinx/docs/ja/configuration.rst b/vendor/robmorgan/phinx/docs/ja/configuration.rst new file mode 100644 index 0000000..169c78e --- /dev/null +++ b/vendor/robmorgan/phinx/docs/ja/configuration.rst @@ -0,0 +1,305 @@ +.. index:: + single: Configuration + +構成設定 +============= + +:doc:`init コマンド ` を使ってプロジェクトを初期化すると、 +Phinx はプロジェクトディレクトリーのルートに ``phinx.yml`` というデフォルトファイルを作成します。 +このファイルは、YAML データのシリアル化形式を使用します。 + +``--configuration`` コマンドラインオプションが与えられた場合、Phinx は指定されたファイルを +ロードします。それ以外の場合は、 ``phinx.php`` 、 ``phinx.json`` , ``phinx.yml``, +または ``phinx.yaml`` を見つけて、最初に見つかったファイルを読み込みます。詳しくは、 +:doc:`コマンド ` の章をご覧下さい。 + +.. warning:: + + 設定ファイルは、ウェブサーバー上の一般公開されているディレクトリーの外に保存してください。 + このファイルにはデータベースの信用情報が含まれており、 + 誤ってプレーンテキストとして提供される可能性があります。 + +JSON ファイルと YAML ファイルは *パース* されますが、PHP ファイルは *インクルード* されています。 +つまり、こういうことです。 + +* 設定項目の配列を `return` する必要があります。 +* 変数スコープはローカルです。つまり、初期化ファイルが読み取ったり変更したりするグローバル変数を + 明示的に宣言する必要があります。 +* その標準出力は抑制されます。 +* JSON や YAML とは異なり、環境接続の詳細を省略し、代わりに初期化された PDO インスタンスを含む + ``connection`` を指定することができます。 + これは、マイグレーションがアプリケーションとやり取りしたり、同じ接続を共有したりする場合に便利です。 + ただし、Phinx は PDO 接続からデータベース名を推測できないため、データベース名を渡すことを + 忘れないでください。 + +:: + + getDatabase()->getPdo(); + + return ['environments' => + [ + 'default_environment' => 'development', + 'development' => [ + 'name' => 'devdb', + 'connection' => $pdo, + ] + ] + ]; + +マイグレーションのパス +---------------------- + +最初のオプションは、マイグレーションのディレクトリーへのパスを指定します。 +デフォルトでは Phinx は ``%%PHINX_CONFIG_DIR%%/db/migrations`` を使用します。 + +.. note:: + + ``%%PHINX_CONFIG_DIR%%`` は特別なトークンで、 + ``phinx.yml`` ファイルが保存されているルートディレクトリーに自動的に置き換えられます。 + +デフォルトの ``%%PHINX_CONFIG_DIR%%/db/migrations`` を上書きするには、 +yaml 設定に次の行を追加する必要があります。 + +.. code-block:: yaml + + paths: + migrations: /your/full/path + +また、設定内の配列を使用して複数のマイグレーションパスを提供することもできます。 + +.. code-block:: yaml + + paths: + migrations: + - application/module1/migrations + - application/module2/migrations + +パスに ``%%PHINX_CONFIG_DIR%%`` トークンを使うこともできます。 + +.. code-block:: yaml + + paths: + migrations: '%%PHINX_CONFIG_DIR%%/your/relative/path' + +マイグレーションは ``glob`` で取り込まれるので、複数のディレクトリーのパターンを定義することができます。 + +.. code-block:: yaml + + paths: + migrations: '%%PHINX_CONFIG_DIR%%/module/*/{data,scripts}/migrations' + +カスタムベースクラス +--------------------- + +デフォルトでは、すべてのマイグレーションは Phinx の ``AbstractMigration`` クラスを継承します。 +これは、設定の中で ``migration_base_class`` を設定することによって、 ``AbstractMigration`` +を継承したカスタムクラスに設定することができます。 + +.. code-block:: yaml + + migration_base_class: MyMagicalMigration + +シードのパス +------------ + +2番目のオプションは、シードディレクトリーへのパスを指定します。 +デフォルトでは Phinx は ``%%PHINX_CONFIG_DIR%%/db/seeds`` を使用します。 + +.. note:: + + ``%%PHINX_CONFIG_DIR%%`` は特別なトークンで、 + ``phinx.yml`` ファイルが保存されているルートディレクトリーに自動的に置き換えられます。 + +デフォルトの ``%%PHINX_CONFIG_DIR%%/db/seeds`` を上書きするには、 +yaml 設定に以下を追加する必要があります。 + +.. code-block:: yaml + + paths: + seeds: /your/full/path + +また、設定内で配列を使用して複数のシードパスを指定することもできます。 + +.. code-block:: yaml + + paths: + seeds: + - /your/full/path1 + - /your/full/path2 + +パスに ``%%PHINX_CONFIG_DIR%%`` トークンを使うこともできます。 + +.. code-block:: yaml + + paths: + seeds: '%%PHINX_CONFIG_DIR%%/your/relative/path' + +環境 +---- + +Phinx の主な機能の1つは、複数のデータベース環境をサポートすることです。Phinx を使用して、 +開発環境でマイグレーションを作成した後、本番環境で同じマイグレーションを実行することができます。 +環境は ``environments`` 以下にネストされたコレクションで指定されます。例: + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + host: localhost + name: production_db + user: root + pass: '' + port: 3306 + charset: utf8 + collation: utf8_unicode_ci + +上記は ``production`` と呼ばれる新しい環境を定義します。 + +複数の開発者が同じプロジェクトで作業し、それぞれが異なる環境を持つ状況 +(例えば、 ``--`` のような規約)、 +または、別々の目的(ブランチ、テストなど)のために別々の環境を持つ必要がある場合には、 +環境変数 `PHINX_ENVIRONMENT` を使用して yaml ファイルのデフォルト環境を上書きします。 + +.. code-block:: bash + + export PHINX_ENVIRONMENT=dev-`whoami`-`hostname` + +テーブルのプレフィクスとサフィックス +------------------------------------ + +テーブルのプレフィックスとサフィックスを定義することができます。 + +.. code-block:: yaml + + environments: + development: + .... + table_prefix: dev_ + table_suffix: _v1 + testing: + .... + table_prefix: test_ + table_suffix: _v2 + +ソケット接続 +------------ + +MySQL アダプターを使用する場合、ネットワーク接続の代わりにソケットを使用することもできます。 +ソケットのパスは ``unix_socket`` で設定されます。 + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + name: production_db + user: root + pass: '' + unix_socket: /var/run/mysql/mysql.sock + charset: utf8 + +外部変数 +-------- + +Phinx は ``PHINX_`` というプレフィックスが付いた環境変数を自動的に取得し、 +設定ファイルのトークンとして利用できるようにします。 +トークンは変数とまったく同じ名前になりますが、どちらの側にも2つの +``%%`` のシンボルをラップすることによってアクセスする必要があります。 +例: ``%%PHINX_DBUSER%%`` 。これは、秘密のデータベース資格情報をバージョン管理システムではなく +サーバーに直接格納する場合に特に便利です。この機能は、次の例で簡単に実証できます。 + +.. code-block:: yaml + + environments: + default_migration_table: phinxlog + default_environment: development + production: + adapter: mysql + host: '%%PHINX_DBHOST%%' + name: '%%PHINX_DBNAME%%' + user: '%%PHINX_DBUSER%%' + pass: '%%PHINX_DBPASS%%' + port: 3306 + charset: utf8 + +サポートするアダプター +---------------------- + +Phinx は現在、次のデータベースアダプターをネイティブにサポートしています。 + +* `MySQL `_: ``mysql`` アダプターを指定。 +* `PostgreSQL `_: ``pgsql`` アダプターを指定。 +* `SQLite `_: ``sqlite`` アダプターを指定。 +* `SQL Server `_: ``sqlsrv`` アダプターを指定。 + +SQLite +~~~~~~ + +SQLite データベースを宣言すると、単純化された構造が使用されます。 + +.. code-block:: yaml + + environments: + development: + adapter: sqlite + name: ./data/derby + testing: + adapter: sqlite + memory: true # *任意* の値で memory を設定すると、 name が上書きされます + +SQL Server +~~~~~~~~~~ + +``sqlsrv`` アダプターを使用して名前付きインスタンスに接続するときは、 +SQL Server が自動的にポートをネゴシエートするので、 ``port`` 設定を省略してください。 +さらに、 ``charset: utf8`` を省略するか、SQL Server の UTF8 に対応する +``charset: 65001`` に変更してください。 + +カスタムアダプター +~~~~~~~~~~~~~~~~~~ + +``Phinx\\Db\\Adapter\\AdapterInterface`` の実装を ``AdapterFactory`` で登録することで +カスタムアダプターを提供できます。 + +.. code-block:: php + + $name = 'fizz'; + $class = 'Acme\Adapter\FizzAdapter'; + + AdapterFactory::instance()->registerAdapter($name, $class); + +アダプターは `$app->run()` が呼び出される前にいつでも登録することができます。 +通常は `bin/phinx` によって呼び出されます。 + +エイリアス +---------- + +テンプレート作成クラス名は、別名をつけて :doc:`create コマンド` の +``--class`` コマンドラインオプションで使うことができます。 + +エイリアス化されたクラスは ``Phinx\Migration\CreationInterface`` インタフェースを実装する +必要があります。 + +.. code-block:: yaml + + aliases: + permission: \Namespace\Migrations\PermissionMigrationTemplateGenerator + view: \Namespace\Migrations\ViewMigrationTemplateGenerator + +バージョンの順序 +---------------- + +マイグレーションの状態をロールバックまたは表示するとき、Phinx は ``version_order`` +オプションに従って実行されたマイグレーションを処理します。これは次の値をとります。 + +* ``creation`` (デフォルト): マイグレーションはファイル名の一部でもある作成時間順に並べ替えられます。 +* ``execution``: マイグレーションは実行時間(開始時間とも呼ばれます)によって順序付けられます。 diff --git a/vendor/robmorgan/phinx/docs/ja/contents.rst b/vendor/robmorgan/phinx/docs/ja/contents.rst new file mode 100644 index 0000000..abb198f --- /dev/null +++ b/vendor/robmorgan/phinx/docs/ja/contents.rst @@ -0,0 +1,12 @@ +コンテンツ +=========== + +.. toctree:: + :maxdepth: 2 + :caption: Phinx + + index + migrations + seeding + commands + configuration diff --git a/vendor/robmorgan/phinx/docs/ja/index.rst b/vendor/robmorgan/phinx/docs/ja/index.rst new file mode 100644 index 0000000..65bd83c --- /dev/null +++ b/vendor/robmorgan/phinx/docs/ja/index.rst @@ -0,0 +1,60 @@ +Phinx マイグレーション +###################### + +Phinx は、データベースのマイグレーションを管理するためのスタンドアロンのコマンドラインツールです。 +CakePHP の公式 Migrations プラグインはこのツールに基づいています。 + +Phinx は、あなたの PHP アプリケーションのデータベースマイグレーションを超簡単に管理できます。 +5分以内に、Composer を使用して Phinx をインストールし、最初のデータベースマイグレーションを作成できます。 +Phinx は、データベース ORM システムやアプリケーションフレームワークを使わずに、マイグレーションします。 + +はじめに +============ + +良い開発者は常に SCM システムを使ってコードをバージョン管理します。それでは、なぜ、 +データベーススキーマについても同じことをしないのでしょうか? + +Phinx は、開発者がデータベースを明確かつ簡潔な方法で変更および操作できるようにします。 +これは手作業で SQL を書くことを避け、代わりに PHP コードを使用してマイグレーションを作成するための +強力な API を提供します。開発者は、好みの SCM システムを使用して、これらのマイグレーションを +バージョン管理することができます。これにより、異なるデータベースシステム間で Phinx のマイグレーションを +移植可能にします。Phinx はどのマイグレーションが実行されたかを把握しているので、 +データベースの状態については心配する必要はなく、より良いソフトウェアを構築することに +集中することができます。 + +ゴール +======= + +Phinx は以下の目標を念頭に置いて開発されました。 + +* 最も普及しているデータベースベンダー間の可搬性。 +* PHP フレームワークに依存しない。 +* シンプルなインストール手順。 +* 使いやすいコマンドライン操作。 +* 他のさまざまな PHP ツール(Phing、PHPUnit)やウェブフレームワークとの統合。 + +インストール +============= + +Phinx は、PHP の依存関係管理のためのツールである Composer を使用してインストールする必要があります。 +詳しくは `Composer `_ のウェブサイトを見てください。 + +.. note:: + + Phinx には少なくともPHP 5.4(またはそれ以降)が必要です。 + +Phinx をインストールするには、Composer を使用する必要があります。 + +.. code-block:: bash + + php composer.phar require robmorgan/phinx + +適切なパーミッションを持つ ``db/migrations`` 構造に従って、プロジェクト内にフォルダーを作成します。 +これは、マイグレーションファイルが存在し、書き込み可能である場所です。 + +今、プロジェクト内から Phinx を実行できるようになりました。 + +.. code-block:: bash + + vendor/bin/phinx init + diff --git a/vendor/robmorgan/phinx/docs/ja/migrations.rst b/vendor/robmorgan/phinx/docs/ja/migrations.rst new file mode 100644 index 0000000..3d9b83f --- /dev/null +++ b/vendor/robmorgan/phinx/docs/ja/migrations.rst @@ -0,0 +1,1293 @@ +.. index:: + single: Writing Migrations + +マイグレーションを書く +====================== + +Phinx は、データベースを変換するためにマイグレーションに依存しています。 +各マイグレーションは、一意のファイル内の PHP クラスによって表されます。 +Phinx の PHP API を使用してマイグレーションを記述することをお勧めしますが、 +生の SQL もサポートされています。 + +新しいマイグレーションの作成 +---------------------------- + +マイグレーションファイルのスケルトンを生成 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +新しい Phinx マイグレーションを作成することから始めましょう。 +``create`` コマンドを使って Phinx を実行してください。 + +.. code-block:: bash + + $ php vendor/bin/phinx create MyNewMigration + +これにより、 ``YYYYMMDDHHMMSS_my_new_migration.php`` という形式で +新しいマイグレーションが作成されます。最初の14文字は現在のタイムスタンプで置き換えられます。 + +複数のマイグレーションのパスを指定した場合は、新しいマイグレーションを作成するパスを +選択するよう求められます。 + +Phinx は、単一の方法でスケルトンのマイグレーションファイルを自動的に作成します。 :: + + table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } + + /** + * Migrate Up. + */ + public function up() + { + + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +このマイグレーションを実行すると、Phinx は up すると ``user_logins`` テーブルを作成し、 +down するとテーブルを削除する方法を自動的に見つけ出します。 ``change`` メソッドが存在する場合、 +Phinx は自動的に ``up`` メソッドと ``down`` メソッドを無視することに注意してください。 +これらのメソッドを使う必要がある場合、別のマイグレーションファイルを作成することをお勧めします。 + +.. note:: + + ``change()`` メソッドの中でテーブルを作成または更新する場合は、Table の ``create()`` + と ``update()`` メソッドを使用する必要があります。Phinx は、 ``save()`` の呼び出しが + 新しいテーブルを作成しているのか、既存のテーブルを変更しているのかを自動的に判断することはできません。 + +Phinx は、次のコマンドでのみ、逆にすることができます。 + +- createTable +- renameTable +- addColumn +- renameColumn +- addIndex +- addForeignKey + +コマンドを元に戻せない場合、Phinx はマイグレーション中に ``IrreversibleMigrationException`` +例外をスローします。 + +up メソッド +~~~~~~~~~~~ + +up メソッドは、マイグレーション中に Phinx によって自動的に実行され、 +指定されたマイグレーションが以前に実行されていないことを検出します。 +データベースを目的の変更に変換するには、up メソッドを使用する必要があります。 + +down メソッド +~~~~~~~~~~~~~ + +down メソッドは、マイグレーション中に Phinx によって自動的に実行され、 +指定されたマイグレーションが過去に実行されたことを検出します。 +up メソッドで記述された変換を元に戻すには、down メソッドを使用する必要があります。 + +クエリーの実行 +-------------- + +クエリーは、 ``execute()`` と ``query()`` メソッドで実行できます。 +``execute()`` メソッドは影響を受ける行の数を返しますが、 ``query()`` メソッドは結果を +`PDOStatement `_ +として返します。 :: + + execute('DELETE FROM users'); // 影響を受ける行の数を返します + + // query() + $stmt = $this->query('SELECT * FROM users'); // PDOStatement を返します + $rows = $stmt->fetchAll(); // 配列として結果を返します + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +.. note:: + + これらのコマンドは、PHP のデータベースにアクセスするための軽量で一貫した + インターフェースを定義する PHP Data Objects(PDO)拡張を使用して実行されます。 + ``execute()`` コマンドを使う前に、必ずクエリーが PDO に従っていることを確認してください。 + これは、DELIMITER をサポートしていないストアードプロシージャーまたはトリガーの挿入時に + DELIMITER を使用する場合に特に重要です。 + +.. warning:: + + クエリーのバッチで ``execute()`` や ``query()`` を使用すると、 + PDO はバッチに1つ以上のクエリーに問題があったとしても、例外をスローしません。 + + したがって、バッチ全体が問題なく通過したものとみなされます。 + + Phinx が潜在的な結果セットを反復して、1つのエラーがあることを発見した場合、 + 以前の結果セットを得る機能が PDO にはないため、Phinx はすべての結果へのアクセスを拒否します。 + (``previousSet()`` ではなく + `nextRowset() `_) 。 + + その結果、バッチ処理されたクエリーの例外を投げないようにする PDO の設計上の決定により、 + Phinx はクエリーのバッチが提供されたときにエラー処理を最大限にサポートすることができません。 + + 幸いにも、PDO のすべての機能が利用可能であるため、 + `nextRowset() `_ + を呼び出して `errorInfo `_ + を調べることで、複数のバッチをマイグレーション中に制御することができます。 + +行の取得 +-------- + +行を取得するには2つのメソッドがあります。 ``fetchRow()`` メソッドは単一の行を取得し、 +``fetchAll()`` メソッドは複数の行を返します。どちらのメソッドも、唯一のパラメーターとして +生の SQL を受け取ります。 :: + + fetchRow('SELECT * FROM users'); + + // メッセージの配列を取得 + $rows = $this->fetchAll('SELECT * FROM messages'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +データの挿入 +------------ + +Phinx ではテーブルにデータを簡単に挿入できます。この機能は :doc:`シード機能 ` +を対象としていますが、マイグレーションでも insert メソッドを自由に使うこともできます。 :: + + 1, + 'name' => 'In Progress' + ]; + + $table = $this->table('status'); + $table->insert($singleRow); + $table->saveData(); + + // 複数行の追加 + $rows = [ + [ + 'id' => 2, + 'name' => 'Stopped' + ], + [ + 'id' => 3, + 'name' => 'Queued' + ] + ]; + + // これは便利なショートカットです + $this->insert('status', $rows); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->execute('DELETE FROM status'); + } + } + +.. note:: + + insert メソッドは、 ``change()`` メソッドの中で使うことはできません。 + ``up()`` と ``down()`` メソッドを使用してください。 + +テーブルの操作 +------------------- + +Table オブジェクト +~~~~~~~~~~~~~~~~~~ + +Table オブジェクトは、 Phinx が提供する最も便利な API の一つです。 +これにより、PHP コードを使用して簡単にデータベーステーブルを操作できます。 +データベースのマイグレーションの中から ``table()`` メソッドを呼び出すことによって、 +Table オブジェクトのインスタンスを取得することができます。 :: + + table('tableName'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +次に、Table オブジェクトによって提供されるメソッドを使用して、このテーブルを操作できます。 + +save メソッド +~~~~~~~~~~~~~ + +Table オブジェクトを操作する場合、Phinx は特定の操作を保留中の変更キャッシュに保存します。 + +疑わしいときは、このメソッドを呼び出すことをお勧めします。 +これは、データベースに対する保留中の変更をコミットします。 + +テーブルの作成 +~~~~~~~~~~~~~~ + +テーブルの作成は、Table オブジェクトを使用するととても簡単です。 +ユーザーのコレクションを格納するテーブルを作成しましょう。 :: + + table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +カラムは ``addColumn()`` メソッドを使って追加されます。 ``addIndex()`` メソッドを使用して、 +username と email カラムの両方に一意なインデックスを作成します。 +最後に ``save()`` を呼び出すと、データベースへの変更がコミットされます。 + +.. note:: + + Phinx は、すべてのテーブルに対して ``id`` という名前のオートインクリメントの主キーを + 自動的に作成します。 + +``id`` オプションは自動的に作成された識別フィールドの名前を設定し、 +``primary_key`` オプションは主キーに使われるフィールドを選択します。 +``id`` は、 false に設定されていない限り、 ``primary_key`` オプションを上書きします。 +主キーが必要ない場合は、 ``primary_key`` を指定せずに ``id`` を false に設定してください。 +主キーは作成されません。 + +別の主キーを指定するには、Table オブジェクトにアクセスする際に ``primary_key`` +オプションを指定します。自動的な ``id`` カラムを無効にし、 +代わりに2つのカラムを使って主キーを作成しましょう。 :: + + table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('created', 'datetime') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +1つの ``primary_key`` を設定しても、 ``AUTO_INCREMENT`` オプションは有効になりません。 +単純に主キーの名前を変更するには、デフォルトの ``id`` フィールド名を上書きする必要があります。 :: + + table('followers', ['id' => 'user_id']); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +さらに、MySQL アダプターは次のオプションをサポートしています。 + +========== =========== +オプション 説明 +========== =========== +comment テーブルにテキストコメントを設定 +engine テーブルエンジンの定義 *(デフォルトは `InnoDB`)* +collation テーブル照合順序の定義 *(デフォルトは `utf8_general_ci`)* +signed 主キーが ``符号付き`` かどうか +========== =========== + +デフォルトでは、主キーは ``符号付き`` です。 +単純に unsigned に設定するには ``signed`` オプションに ``false`` の値を渡します。 :: + + table('followers', ['signed' => false]); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +有効なカラムの型 +~~~~~~~~~~~~~~~~~~ + +カラム型は文字列として指定され、次のいずれかになります。 + +- biginteger +- binary +- boolean +- date +- datetime +- decimal +- float +- integer +- string +- text +- time +- timestamp +- uuid + +さらに、MySQL アダプターは、 ``enum`` 、 ``set`` 、 ``blob`` 、 ``json`` +カラム型をサポートしています。 (``json`` は MySQL 5.7 以降) + +さらに、Postgres アダプターは、 ``smallint`` 、 ``json`` 、 ``jsonb`` 、 ``uuid`` 、 +``cidr`` 、 ``inet`` 、 ``macaddr`` カラム型をサポートしています。(PostgreSQL 9.3 以降) + +有効なオプションについては、以下の `有効なカラムのオプション`_ を参照してください。 + +テーブルが存在するかどうかの判断 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``hasTable()`` メソッドを使うことによって、テーブルが存在するかどうかを判断することができます。 :: + + hasTable('users'); + if ($exists) { + // 何かします + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +テーブルの削除 +~~~~~~~~~~~~~~~~ + +Table は ``dropTable()`` メソッドを使って非常に簡単に削除することができます。 +テーブルを ``down()`` メソッドで再作成することは良い考えです。 :: + + dropTable('users'); + } + + /** + * Migrate Down. + */ + public function down() + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } + } + +テーブル名の変更 +~~~~~~~~~~~~~~~~ + +テーブルの名前を変更するには、Table オブジェクトのインスタンスにアクセスし、 +``rename()`` メソッドを呼び出します。 :: + + table('users'); + $table->rename('legacy_users') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('legacy_users'); + $table->rename('users') + ->save(); + } + } + +カラムの操作 +-------------------- + +.. _valid-column-types: + +有効なカラムの型 +~~~~~~~~~~~~~~~~~~ + +カラムの型は文字列として指定され、次のいずれかになります。 + +- biginteger +- binary +- boolean +- char +- date +- datetime +- decimal +- float +- integer +- string +- text +- time +- timestamp +- uuid + +さらに、MySQL アダプターは、 ``enum`` 、 ``set`` 、 ``blob`` 、 ``json`` +カラム型をサポートしています。 (``json`` は MySQL 5.7 以降) + +さらに、Postgres アダプターは、 ``smallint`` 、 ``json`` 、 ``jsonb`` 、 ``uuid`` 、 +``cidr`` 、 ``inet`` 、 ``macaddr`` カラム型をサポートしています。(PostgreSQL 9.3 以降) + + +有効なカラムのオプション +~~~~~~~~~~~~~~~~~~~~~~~~ + +有効なカラムのオプションは次のとおりです。 + +全てのカラム型: + +========== =========== +オプション 説明 +========== =========== +limit 文字列の最大長を設定します。また、アダプターのカラムの種類を示します(下記の注を参照) +length ``limit`` の別名 +default デフォルトの値やアクションを設定 +null ``NULL`` 値の許可 (主キーで使用してはいけません!) +after 新しいカラムの前に配置するカラムを指定 +comment カラムのテキストコメントを設定 +========== =========== + +``decimal`` カラム: + +========== =========== +オプション 説明 +========== =========== +precision ``scale`` と組み合わせ、数値全体の桁数を設定 +scale ``precision`` と組み合わせ、少数点以下の桁数を設定 +signed ``unsigned`` オプションを有効または無効にする *(MySQL のみ適用)* +========== =========== + +``enum`` と ``set`` カラム: + +========== =========== +オプション 説明 +========== =========== +values カンマ区切りリストまたは値の配列 +========== =========== + +``integer`` と ``biginteger`` カラム: + +========== =========== +オプション 説明 +========== =========== +identity 自動インクリメントを有効または無効にする +signed ``unsigned`` オプションを有効または無効にする *(MySQL のみ適用)* +========== =========== + +``timestamp`` カラム: + +========== =========== +オプション 説明 +========== =========== +default デフォルト値を設定 (``CURRENT_TIMESTAMP`` を使用) +update 行が更新されたときにトリガーされるアクションを設定 (``CURRENT_TIMESTAMP`` を使用) +timezone ``time`` と ``timestamp`` カラムの ``with time zone`` オプションを有効または無効にする *(Postgres のみ適用)* +========== =========== + +``addTimestamps()`` を使うことで、テーブルに ``created_at`` と ``updated_at`` +タイムスタンプを追加できます。このメソッドでは、代わりの名を指定することもできます。 :: + + table('users')->addTimestamps(null, 'amended_at')->create(); + } + } + +``boolean`` カラム: + +========== =========== +オプション 説明 +========== =========== +signed ``unsigned`` オプションを有効または無効にする *(MySQL のみ適用)* +========== =========== + +``string`` と ``text`` カラム: + +========== =========== +オプション 説明 +========== =========== +collation テーブルのデフォルトとは異なる照合順序を設定 *(MySQL のみ適用)* +encoding テーブルのデフォルトとは異なる文字セットを設定 *(MySQL のみ適用)* +========== =========== + +外部キーの定義: + +========== =========== +オプション 説明 +========== =========== +update 行が更新されたときにトリガーされるアクションを設定 +delete 行が削除されたときにトリガーされるアクションを設定 +========== =========== + +オプションの第3引数配列を使用して、これらのオプションの1つ以上を任意のカラムに渡すことができます。 + +PostgreSQL の Limit オプション +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PostgreSQL アダプターを使用する場合、 ``integer`` カラムに対してデータベースのカラム型のヒントを +追加することができます。次のいずれかのオプションで ``limit`` を使うと、 +それに応じてカラムの型が変更されます。 + +============ ============== +Limit カラム型 +============ ============== +INT_SMALL SMALLINT +============ ============== + +.. code-block:: php + + use Phinx\Db\Adapter\PostgresAdapter; + + //... + + $table = $this->table('cart_items'); + $table->addColumn('user_id', 'integer') + ->addColumn('subtype_id', 'integer', ['limit' => PostgresAdapter::INT_SMALL]) + ->create(); + +MySQL の Limit オプション +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +MySQL アダプターを使用する場合、 ``integer`` 、 ``text`` 、および ``blob`` カラムに対して、 +データベースのカラム型のヒントを追加することができます。次のいずれかのオプションで ``limit`` を使うと、 +それに応じてカラムの型が変更されます。 + +============ ============== +Limit カラム型 +============ ============== +BLOB_TINY TINYBLOB +BLOB_REGULAR BLOB +BLOB_MEDIUM MEDIUMBLOB +BLOB_LONG LONGBLOB +TEXT_TINY TINYTEXT +TEXT_REGULAR TEXT +TEXT_MEDIUM MEDIUMTEXT +TEXT_LONG LONGTEXT +INT_TINY TINYINT +INT_SMALL SMALLINT +INT_MEDIUM MEDIUMINT +INT_REGULAR INT +INT_BIG BIGINT +============ ============== + +.. code-block:: php + + use Phinx\Db\Adapter\MysqlAdapter; + + //... + + $table = $this->table('cart_items'); + $table->addColumn('user_id', 'integer') + ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) + ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) + ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) + ->create(); + +カラム一覧の取得 +~~~~~~~~~~~~~~~~~ + +すべてのテーブルのカラムを取得するには、 `table` オブジェクトを作成し、 +`getColumns()` メソッドを呼び出します。 +このメソッドは、基本情報を持つ Column クラスの配列を返します。 例:: + + table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down() + { + ... + } + } + +カラムが存在するかどうかの確認 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``hasColumn()`` メソッドを使ってテーブルに特定のカラムがすでにあるかどうかを調べることができます。 :: + + table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // 何かします + } + + } + } + +カラム名の変更 +~~~~~~~~~~~~~~~~~ + +カラムの名前を変更するには、Table オブジェクトのインスタンスにアクセスし、 +``renameColumn()`` メソッドを呼び出します。 :: + + table('users'); + $table->renameColumn('bio', 'biography') + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('users'); + $table->renameColumn('biography', 'bio') + ->update(); + } + } + +別のカラムの後にカラムの追加 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +列を追加するときに、 ``after`` オプションを使用してその位置を指定することができます。 :: + + table('users'); + $table->addColumn('city', 'string', ['after' => 'email']) + ->update(); + } + } + +カラムの削除 +~~~~~~~~~~~~~~~~~ + +カラムを削除するには、 ``removeColumn()`` メソッドを使用してください。 :: + + table('users'); + $table->removeColumn('short_name') + ->save(); + } + } + +カラムの制限を指定 +~~~~~~~~~~~~~~~~~~~~~~~~~ + +``limit`` オプションを使ってカラムの最大長を制限できます。 :: + + table('tags'); + $table->addColumn('short_name', 'string', ['limit' => 30]) + ->update(); + } + } + +カラムの属性を変更 +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +既存のカラムのカラム型またはオプションを変更するには、 ``changeColumn()`` メソッドを使用します。 +使用可能な値に関しては、 :ref:`valid-column-types` や `有効なカラムのオプション`_ をご覧ください。 :: + + table('users'); + $users->changeColumn('email', 'string', ['limit' => 255]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +インデックスの操作 +-------------------- + +テーブルにインデックスを追加するには、テーブルオブジェクトに対して +``addIndex()`` メソッドを呼び出すことができます。 :: + + table('users'); + $table->addColumn('city', 'string') + ->addIndex(['city']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +デフォルトでは、Phinx はデータベースアダプターに通常のインデックスを作成するよう指示します。 +一意のインデックスを指定するために、 ``unique`` を ``addIndex()`` メソッドに渡すことができます。 +また、 ``name`` パラメーターを使ってインデックスの名前を明示的に指定することもできます。 :: + + table('users'); + $table->addColumn('email', 'string') + ->addIndex(['email'], ['unique' => true, 'name' => 'idx_users_email']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +MySQL アダプターは、 ``fulltext`` インデックスもサポートしています。 +5.6 より前のバージョンを使用している場合は、 +テーブルが ``MyISAM`` エンジンを使用していることを確認する必要があります。 :: + + table('users', ['engine' => 'MyISAM']); + $table->addColumn('email', 'string') + ->addIndex('email', ['type' => 'fulltext']) + ->create(); + } + } + +``removeIndex()`` メソッドを呼び出すと簡単にインデックスが削除できます。 +各インデックスに対してこのメソッドを呼び出す必要があります。 :: + + table('users'); + $table->removeIndex(['email']) + ->save(); + + // あるいは、インデックスの名前で削除することもできます。例: + $table->removeIndexByName('idx_users_email') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +外部キーの操作 +-------------- + +Phinx は、データベーステーブルに外部キー制約を作成する機能をサポートしています。 +例のテーブルに外部キーを追加しましょう。 :: + + table('tags'); + $table->addColumn('tag_name', 'string') + ->save(); + + $refTable = $this->table('tag_relationships'); + $refTable->addColumn('tag_id', 'integer') + ->addForeignKey('tag_id', 'tags', 'id', ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION']) + ->save(); + + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +「削除時」および「更新時」アクションは、 'delete' および 'update' オプション配列で定義されます。 +使用可能な値は、 'SET_NULL'、 'NO_ACTION'、 'CASCADE' および 'RESTRICT' です。 +制約名は 'constraint' オプションで変更できます。 + +``addForeignKey()`` にカラムの配列を渡すこともできます。 +これにより、複合キーを使用するテーブルとの外部キー関係を確立することができます。 :: + + table('follower_events'); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('event_id', 'integer') + ->addForeignKey(['user_id', 'follower_id'], + 'followers', + ['user_id', 'follower_id'], + ['delete'=> 'NO_ACTION', 'update'=> 'NO_ACTION', 'constraint' => 'user_follower_id']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +``constraint`` パラメーターを使って名前付きの外部キーを追加することができます。 +この機能は Phinx バージョン 0.6.5 でサポートされます。 :: + + table('your_table'); + $table->addForeignKey('foreign_id', 'reference_table', ['id'], + ['constraint'=>'your_foreign_key_name']); + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +外部キーが存在するかどうかも簡単に確認できます。 :: + + table('tag_relationships'); + $exists = $table->hasForeignKey('tag_id'); + if ($exists) { + // 何かします + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +最後に、外部キーを削除するには、 ``dropForeignKey`` メソッドを使用します。 :: + + table('tag_relationships'); + $table->dropForeignKey('tag_id'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } diff --git a/vendor/robmorgan/phinx/docs/ja/seeding.rst b/vendor/robmorgan/phinx/docs/ja/seeding.rst new file mode 100644 index 0000000..92680be --- /dev/null +++ b/vendor/robmorgan/phinx/docs/ja/seeding.rst @@ -0,0 +1,214 @@ +.. index:: + single: Database Seeding + +データベースの初期データ投入 +============================ + +バージョン0.5.0 では、Phinx はデータベースにテストデータをシードするためのサポートを導入しました。 +シードクラスは、作成済みのデータをデータベースに簡単に投入するための優れた方法です。 +デフォルトでは ``seeds`` ディレクトリーに保存されます。 ただし、このパスは設定ファイルで変更できます。 + +.. note:: + + データベースのシードは完全にオプションで、 + Phinx はデフォルトで ``seeds`` ディレクトリーを作成しません。 + +シードクラスの新規作成 +---------------------- + +Phinx には、新しいシードクラスを簡単に生成するコマンドが含まれています。 + +.. code-block:: bash + + $ php vendor/bin/phinx seed:create UserSeeder + +複数のシードのパスを指定した場合は、新しいシードクラスを作成するパスを選択するように求められます。 + +以下は、スケルトンテンプレートを元にしています。 :: + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->save(); + } + } + +.. note:: + + ``save()`` メソッドを呼び出して、データをテーブルにコミットする必要があります。 + Phinx はデータをバッファリングします。 + +Faker ライブラリーとの統合 +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +シードクラスですばらしい `Faker ライブラリー `_ +を使うのは簡単です。Composer を使用してインストールするだけです。 + +.. code-block:: bash + + $ composer require fzaninotto/faker + +そして、シードクラスの中で、それを使用してください。 :: + + $faker->userName, + 'password' => sha1($faker->password), + 'password_salt' => sha1('foo'), + 'email' => $faker->email, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'created' => date('Y-m-d H:i:s'), + ]; + } + + $this->insert('users', $data); + } + } + +テーブルのデータ消去 +-------------------- + +データを挿入することに加えて、Phinx は SQL の ``TRUNCATE`` コマンドを使って +テーブルを空にすることを容易にします。 :: + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->save(); + + // テーブルを空にします + $posts->truncate(); + } + } + +.. note:: + + SQLite は ``TRUNCATE`` コマンドをネイティブにサポートしていないので、 ``DELETE FROM`` + が使用されています。テーブルのデータ消去後、 ``VACUUM`` コマンドを呼び出すことをお勧めします。 + Phinx はこれを自動的には行いません。 + +シードクラスの実行 +------------------ + +これは簡単な部分です。データベースをシードするには、 ``seed:run`` コマンドを使います。 + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run + +デフォルトでは、Phinx は利用可能なすべてのシードクラスを実行します。 +特定のクラスを実行したい場合は、 ``-s`` パラメーターを使ってそのクラスの名前を渡します。 + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -s UserSeeder + +複数のシーダーを実行することもできます。 + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -s UserSeeder -s PermissionSeeder -s LogSeeder + +``-v`` パラメーターを使用して、より詳細な出力を表示することもできます。 + +.. code-block:: bash + + $ php vendor/bin/phinx seed:run -v + +Phinx のシード機能は、テストデータをデータベースに簡単かつ繰り返し挿入するための +簡単なメカニズムを提供します。 diff --git a/vendor/robmorgan/phinx/phpstan-baseline.neon b/vendor/robmorgan/phinx/phpstan-baseline.neon new file mode 100644 index 0000000..dd036a9 --- /dev/null +++ b/vendor/robmorgan/phinx/phpstan-baseline.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: "#^Call to function is_subclass_of\\(\\) with non\\-empty\\-string and 'Phinx\\\\\\\\Migration…' will always evaluate to false\\.$#" + count: 2 + path: src/Phinx/Console/Command/Create.php + diff --git a/vendor/robmorgan/phinx/src/Phinx/Config/Config.php b/vendor/robmorgan/phinx/src/Phinx/Config/Config.php new file mode 100644 index 0000000..0b5e909 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Config/Config.php @@ -0,0 +1,540 @@ +configFilePath = $configFilePath; + $this->values = $this->replaceTokens($configArray); + } + + /** + * Create a new instance of the config class using a Yaml file path. + * + * @param string $configFilePath Path to the Yaml File + * @throws \RuntimeException + * @return \Phinx\Config\Config + */ + public static function fromYaml($configFilePath) + { + if (!class_exists('Symfony\\Component\\Yaml\\Yaml', true)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('Missing yaml parser, symfony/yaml package is not installed.'); + // @codeCoverageIgnoreEnd + } + + $configFile = file_get_contents($configFilePath); + $configArray = Yaml::parse($configFile); + + if (!is_array($configArray)) { + throw new RuntimeException(sprintf( + 'File \'%s\' must be valid YAML', + $configFilePath + )); + } + + return new static($configArray, $configFilePath); + } + + /** + * Create a new instance of the config class using a JSON file path. + * + * @param string $configFilePath Path to the JSON File + * @throws \RuntimeException + * @return \Phinx\Config\Config + */ + public static function fromJson($configFilePath) + { + if (!function_exists('json_decode')) { + // @codeCoverageIgnoreStart + throw new RuntimeException('Need to install JSON PHP extension to use JSON config'); + // @codeCoverageIgnoreEnd + } + + $configArray = json_decode(file_get_contents($configFilePath), true); + if (!is_array($configArray)) { + throw new RuntimeException(sprintf( + 'File \'%s\' must be valid JSON', + $configFilePath + )); + } + + return new static($configArray, $configFilePath); + } + + /** + * Create a new instance of the config class using a PHP file path. + * + * @param string $configFilePath Path to the PHP File + * @throws \RuntimeException + * @return \Phinx\Config\Config + */ + public static function fromPhp($configFilePath) + { + ob_start(); + /** @noinspection PhpIncludeInspection */ + $configArray = include $configFilePath; + + // Hide console output + ob_end_clean(); + + if (!is_array($configArray)) { + throw new RuntimeException(sprintf( + 'PHP file \'%s\' must return an array', + $configFilePath + )); + } + + return new static($configArray, $configFilePath); + } + + /** + * @inheritDoc + */ + public function getEnvironments() + { + if (isset($this->values) && isset($this->values['environments'])) { + $environments = []; + foreach ($this->values['environments'] as $key => $value) { + if (is_array($value)) { + $environments[$key] = $value; + } + } + + return $environments; + } + + return null; + } + + /** + * @inheritDoc + */ + public function getEnvironment($name) + { + $environments = $this->getEnvironments(); + + if (isset($environments[$name])) { + if (isset($this->values['environments']['default_migration_table'])) { + $environments[$name]['default_migration_table'] = + $this->values['environments']['default_migration_table']; + } + + if ( + isset($environments[$name]['adapter']) + && $environments[$name]['adapter'] === 'sqlite' + && !empty($environments[$name]['memory']) + ) { + $environments[$name]['name'] = SQLiteAdapter::MEMORY; + } + + return $this->parseAgnosticDsn($environments[$name]); + } + + return null; + } + + /** + * @inheritDoc + */ + public function hasEnvironment($name) + { + return $this->getEnvironment($name) !== null; + } + + /** + * @inheritDoc + */ + public function getDefaultEnvironment() + { + // The $PHINX_ENVIRONMENT variable overrides all other default settings + $env = getenv('PHINX_ENVIRONMENT'); + if (!empty($env)) { + if ($this->hasEnvironment($env)) { + return $env; + } + + throw new RuntimeException(sprintf( + 'The environment configuration (read from $PHINX_ENVIRONMENT) for \'%s\' is missing', + $env + )); + } + + // deprecated: to be removed 0.13 + if (isset($this->values['environments']['default_database'])) { + $this->values['environments']['default_environment'] = $this->values['environments']['default_database']; + } + + // if the user has configured a default environment then use it, + // providing it actually exists! + if (isset($this->values['environments']['default_environment'])) { + if ($this->hasEnvironment($this->values['environments']['default_environment'])) { + return $this->values['environments']['default_environment']; + } + + throw new RuntimeException(sprintf( + 'The environment configuration for \'%s\' is missing', + $this->values['environments']['default_environment'] + )); + } + + // else default to the first available one + if (is_array($this->getEnvironments()) && count($this->getEnvironments()) > 0) { + $names = array_keys($this->getEnvironments()); + + return $names[0]; + } + + throw new RuntimeException('Could not find a default environment'); + } + + /** + * @inheritDoc + */ + public function getAlias($alias) + { + return !empty($this->values['aliases'][$alias]) ? $this->values['aliases'][$alias] : null; + } + + /** + * @inheritDoc + */ + public function getAliases() + { + return !empty($this->values['aliases']) ? $this->values['aliases'] : []; + } + + /** + * @inheritDoc + */ + public function getConfigFilePath() + { + return $this->configFilePath; + } + + /** + * @inheritDoc + * @throws \UnexpectedValueException + */ + public function getMigrationPaths() + { + if (!isset($this->values['paths']['migrations'])) { + throw new UnexpectedValueException('Migrations path missing from config file'); + } + + if (is_string($this->values['paths']['migrations'])) { + $this->values['paths']['migrations'] = [$this->values['paths']['migrations']]; + } + + return $this->values['paths']['migrations']; + } + + /** + * Gets the base class name for migrations. + * + * @param bool $dropNamespace Return the base migration class name without the namespace. + * @return string + */ + public function getMigrationBaseClassName($dropNamespace = true) + { + $className = !isset($this->values['migration_base_class']) ? 'Phinx\Migration\AbstractMigration' : $this->values['migration_base_class']; + + return $dropNamespace ? (substr(strrchr($className, '\\'), 1) ?: $className) : $className; + } + + /** + * @inheritDoc + * @throws \UnexpectedValueException + */ + public function getSeedPaths() + { + if (!isset($this->values['paths']['seeds'])) { + throw new UnexpectedValueException('Seeds path missing from config file'); + } + + if (is_string($this->values['paths']['seeds'])) { + $this->values['paths']['seeds'] = [$this->values['paths']['seeds']]; + } + + return $this->values['paths']['seeds']; + } + + /** + * Gets the base class name for seeders. + * + * @param bool $dropNamespace Return the base seeder class name without the namespace. + * @return string + */ + public function getSeedBaseClassName($dropNamespace = true) + { + $className = !isset($this->values['seed_base_class']) ? 'Phinx\Seed\AbstractSeed' : $this->values['seed_base_class']; + + return $dropNamespace ? substr(strrchr($className, '\\'), 1) : $className; + } + + /** + * Get the template file name. + * + * @return string|false + */ + public function getTemplateFile() + { + if (!isset($this->values['templates']['file'])) { + return false; + } + + return $this->values['templates']['file']; + } + + /** + * Get the template class name. + * + * @return string|false + */ + public function getTemplateClass() + { + if (!isset($this->values['templates']['class'])) { + return false; + } + + return $this->values['templates']['class']; + } + + /** + * {@inheritdoc} + */ + public function getDataDomain() + { + if (!isset($this->values['data_domain'])) { + return []; + } + + return $this->values['data_domain']; + } + + /** + * @inheritDoc + */ + public function getContainer() + { + if (!isset($this->values['container'])) { + return null; + } + + return $this->values['container']; + } + + /** + * Get the version order. + * + * @return string + */ + public function getVersionOrder() + { + if (!isset($this->values['version_order'])) { + return self::VERSION_ORDER_CREATION_TIME; + } + + return $this->values['version_order']; + } + + /** + * Is version order creation time? + * + * @return bool + */ + public function isVersionOrderCreationTime() + { + $versionOrder = $this->getVersionOrder(); + + return $versionOrder == self::VERSION_ORDER_CREATION_TIME; + } + + /** + * Get the bootstrap file path + * + * @return string|false + */ + public function getBootstrapFile() + { + if (!isset($this->values['paths']['bootstrap'])) { + return false; + } + + return $this->values['paths']['bootstrap']; + } + + /** + * Replace tokens in the specified array. + * + * @param array $arr Array to replace + * @return array + */ + protected function replaceTokens(array $arr) + { + // Get environment variables + // Depending on configuration of server / OS and variables_order directive, + // environment variables either end up in $_SERVER (most likely) or $_ENV, + // so we search through both + $tokens = []; + foreach (array_merge($_ENV, $_SERVER) as $varname => $varvalue) { + if (strpos($varname, 'PHINX_') === 0) { + $tokens['%%' . $varname . '%%'] = $varvalue; + } + } + + // Phinx defined tokens (override env tokens) + $tokens['%%PHINX_CONFIG_PATH%%'] = $this->getConfigFilePath(); + $tokens['%%PHINX_CONFIG_DIR%%'] = $this->getConfigFilePath() !== null ? dirname($this->getConfigFilePath()) : ''; + + // Recurse the array and replace tokens + return $this->recurseArrayForTokens($arr, $tokens); + } + + /** + * Recurse an array for the specified tokens and replace them. + * + * @param array $arr Array to recurse + * @param string[] $tokens Array of tokens to search for + * @return array + */ + protected function recurseArrayForTokens($arr, $tokens) + { + $out = []; + foreach ($arr as $name => $value) { + if (is_array($value)) { + $out[$name] = $this->recurseArrayForTokens($value, $tokens); + continue; + } + if (is_string($value)) { + foreach ($tokens as $token => $tval) { + $value = str_replace($token, $tval ?? '', $value); + } + $out[$name] = $value; + continue; + } + $out[$name] = $value; + } + + return $out; + } + + /** + * Parse a database-agnostic DSN into individual options. + * + * @param array $options Options + * @return array + */ + protected function parseAgnosticDsn(array $options) + { + $parsed = Util::parseDsn($options['dsn'] ?? ''); + if ($parsed) { + unset($options['dsn']); + } + + $options = array_merge($parsed, $options); + + return $options; + } + + /** + * {@inheritDoc} + * + * @param mixed $id ID + * @param mixed $value Value + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($id, $value) + { + $this->values[$id] = $value; + } + + /** + * {@inheritDoc} + * + * @param mixed $id ID + * @throws \InvalidArgumentException + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($id) + { + if (!array_key_exists($id, $this->values)) { + throw new InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); + } + + return $this->values[$id] instanceof Closure ? $this->values[$id]($this) : $this->values[$id]; + } + + /** + * {@inheritDoc} + * + * @param mixed $id ID + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($id) + { + return isset($this->values[$id]); + } + + /** + * {@inheritDoc} + * + * @param mixed $id ID + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($id) + { + unset($this->values[$id]); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Config/ConfigInterface.php b/vendor/robmorgan/phinx/src/Phinx/Config/ConfigInterface.php new file mode 100644 index 0000000..15f09f0 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Config/ConfigInterface.php @@ -0,0 +1,156 @@ +null if no environments exist. + * + * @return array|null + */ + public function getEnvironments(); + + /** + * Returns the configuration for a given environment. + * + * This method returns null if the specified environment + * doesn't exist. + * + * @param string $name Environment Name + * @return array|null + */ + public function getEnvironment($name); + + /** + * Does the specified environment exist in the configuration file? + * + * @param string $name Environment Name + * @return bool + */ + public function hasEnvironment($name); + + /** + * Gets the default environment name. + * + * @throws \RuntimeException + * @return string + */ + public function getDefaultEnvironment(); + + /** + * Get the aliased value from a supplied alias. + * + * @param string $alias Alias + * @return string|null + */ + public function getAlias($alias); + + /** + * Get all the aliased values. + * + * @return string[] + */ + public function getAliases(); + + /** + * Gets the config file path. + * + * @return string|null + */ + public function getConfigFilePath(); + + /** + * Gets the paths to search for migration files. + * + * @return string[] + */ + public function getMigrationPaths(); + + /** + * Gets the paths to search for seed files. + * + * @return string[] + */ + public function getSeedPaths(); + + /** + * Get the template file name. + * + * @return string|false + */ + public function getTemplateFile(); + + /** + * Get the template class name. + * + * @return string|false + */ + public function getTemplateClass(); + + /** + * Get the user-provided container for instantiating seeds + * + * @return \Psr\Container\ContainerInterface|null + */ + public function getContainer(); + + /** + * Get the data domain array. + * + * @return array + */ + public function getDataDomain(); + + /** + * Get the version order. + * + * @return string + */ + public function getVersionOrder(); + + /** + * Is version order creation time? + * + * @return bool + */ + public function isVersionOrderCreationTime(); + + /** + * Get the bootstrap file path + * + * @return string|false + */ + public function getBootstrapFile(); + + /** + * Gets the base class name for migrations. + * + * @param bool $dropNamespace Return the base migration class name without the namespace. + * @return string + */ + public function getMigrationBaseClassName($dropNamespace = true); + + /** + * Gets the base class name for seeders. + * + * @param bool $dropNamespace Return the base seeder class name without the namespace. + * @return string + */ + public function getSeedBaseClassName($dropNamespace = true); +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Config/NamespaceAwareInterface.php b/vendor/robmorgan/phinx/src/Phinx/Config/NamespaceAwareInterface.php new file mode 100644 index 0000000..b6f5e08 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Config/NamespaceAwareInterface.php @@ -0,0 +1,33 @@ +getMigrationPaths(); + + return $this->searchNamespace($path, $paths); + } + + /** + * Get Seed Namespace associated with path. + * + * @param string $path Path + * @return string|null + */ + public function getSeedNamespaceByPath($path) + { + $paths = $this->getSeedPaths(); + + return $this->searchNamespace($path, $paths); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/AbstractCommand.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/AbstractCommand.php new file mode 100644 index 0000000..5169658 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/AbstractCommand.php @@ -0,0 +1,399 @@ + + */ +abstract class AbstractCommand extends Command +{ + public const FORMAT_JSON = 'json'; + public const FORMAT_YML_ALIAS = 'yaml'; + public const FORMAT_YML = 'yml'; + public const FORMAT_PHP = 'php'; + public const FORMAT_DEFAULT = 'php'; + + /** + * The location of the default migration template. + */ + protected const DEFAULT_MIGRATION_TEMPLATE = '/../../Migration/Migration.template.php.dist'; + + /** + * The location of the default seed template. + */ + protected const DEFAULT_SEED_TEMPLATE = '/../../Seed/Seed.template.php.dist'; + + /** + * @var \Phinx\Config\ConfigInterface + */ + protected $config; + + /** + * @var \Phinx\Db\Adapter\AdapterInterface + */ + protected $adapter; + + /** + * @var \Phinx\Migration\Manager + */ + protected $manager; + + /** + * Exit code for when command executes successfully + * + * @var int + */ + public const CODE_SUCCESS = 0; + + /** + * Exit code for when command hits a non-recoverable error during execution + * + * @var int + */ + public const CODE_ERROR = 1; + + /** + * Exit code for when status command is run and there are missing migrations + * + * @var int + */ + public const CODE_STATUS_MISSING = 2; + + /** + * Exit code for when status command is run and there are no missing migations, + * but does have down migrations + * + * @var int + */ + public const CODE_STATUS_DOWN = 3; + + /** + * {@inheritDoc} + * + * @return void + */ + protected function configure() + { + $this->addOption('--configuration', '-c', InputOption::VALUE_REQUIRED, 'The configuration file to load'); + $this->addOption('--parser', '-p', InputOption::VALUE_REQUIRED, 'Parser used to read the config file. Defaults to YAML'); + } + + /** + * Bootstrap Phinx. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return void + */ + public function bootstrap(InputInterface $input, OutputInterface $output) + { + /** @var \Phinx\Config\ConfigInterface|null $config */ + $config = $this->getConfig(); + if (!$config) { + $this->loadConfig($input, $output); + } + + $this->loadManager($input, $output); + + $bootstrap = $this->getConfig()->getBootstrapFile(); + if ($bootstrap) { + $output->writeln('using bootstrap ' . Util::relativePath($bootstrap) . ' '); + Util::loadPhpFile($bootstrap, $input, $output, $this); + } + + // report the paths + $paths = $this->getConfig()->getMigrationPaths(); + + $output->writeln('using migration paths '); + + foreach (Util::globAll($paths) as $path) { + $output->writeln(' - ' . realpath($path) . ''); + } + + try { + $paths = $this->getConfig()->getSeedPaths(); + + $output->writeln('using seed paths '); + + foreach (Util::globAll($paths) as $path) { + $output->writeln(' - ' . realpath($path) . ''); + } + } catch (UnexpectedValueException $e) { + // do nothing as seeds are optional + } + } + + /** + * Sets the config. + * + * @param \Phinx\Config\ConfigInterface $config Config + * @return $this + */ + public function setConfig(ConfigInterface $config) + { + $this->config = $config; + + return $this; + } + + /** + * Gets the config. + * + * @return \Phinx\Config\ConfigInterface + */ + public function getConfig() + { + return $this->config; + } + + /** + * Sets the database adapter. + * + * @param \Phinx\Db\Adapter\AdapterInterface $adapter Adapter + * @return $this + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * Gets the database adapter. + * + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * Sets the migration manager. + * + * @param \Phinx\Migration\Manager $manager Manager + * @return $this + */ + public function setManager(Manager $manager) + { + $this->manager = $manager; + + return $this; + } + + /** + * Gets the migration manager. + * + * @return \Phinx\Migration\Manager|null + */ + public function getManager() + { + return $this->manager; + } + + /** + * Returns config file path + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @return string + */ + protected function locateConfigFile(InputInterface $input) + { + $configFile = $input->getOption('configuration'); + + $useDefault = false; + + if ($configFile === null || $configFile === false) { + $useDefault = true; + } + + $cwd = getcwd(); + + // locate the phinx config file + // In future walk the tree in reverse (max 10 levels) + $locator = new FileLocator([ + $cwd . DIRECTORY_SEPARATOR, + ]); + + if (!$useDefault) { + // Locate() throws an exception if the file does not exist + return $locator->locate($configFile, $cwd, true); + } + + $possibleConfigFiles = ['phinx.php', 'phinx.json', 'phinx.yaml', 'phinx.yml']; + foreach ($possibleConfigFiles as $configFile) { + try { + return $locator->locate($configFile, $cwd, true); + } catch (InvalidArgumentException $exception) { + $lastException = $exception; + } + } + throw $lastException; + } + + /** + * Parse the config file and load it into the config object + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @throws \InvalidArgumentException + * @return void + */ + protected function loadConfig(InputInterface $input, OutputInterface $output) + { + $configFilePath = $this->locateConfigFile($input); + $output->writeln('using config file ' . Util::relativePath($configFilePath)); + + $parser = $input->getOption('parser'); + + // If no parser is specified try to determine the correct one from the file extension. Defaults to YAML + if ($parser === null) { + $extension = pathinfo($configFilePath, PATHINFO_EXTENSION); + + switch (strtolower($extension)) { + case self::FORMAT_JSON: + $parser = self::FORMAT_JSON; + break; + case self::FORMAT_YML_ALIAS: + case self::FORMAT_YML: + $parser = self::FORMAT_YML; + break; + case self::FORMAT_PHP: + default: + $parser = self::FORMAT_DEFAULT; + break; + } + } + + switch (strtolower($parser)) { + case self::FORMAT_JSON: + $config = Config::fromJson($configFilePath); + break; + case self::FORMAT_PHP: + $config = Config::fromPhp($configFilePath); + break; + case self::FORMAT_YML_ALIAS: + case self::FORMAT_YML: + $config = Config::fromYaml($configFilePath); + break; + default: + throw new InvalidArgumentException(sprintf('\'%s\' is not a valid parser.', $parser)); + } + + $output->writeln('using config parser ' . $parser); + + $this->setConfig($config); + } + + /** + * Load the migrations manager and inject the config + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return void + */ + protected function loadManager(InputInterface $input, OutputInterface $output) + { + if ($this->getManager() === null) { + $manager = new Manager($this->getConfig(), $input, $output); + $container = $this->getConfig()->getContainer(); + if ($container !== null) { + $manager->setContainer($container); + } + $this->setManager($manager); + } else { + $manager = $this->getManager(); + $manager->setInput($input); + $manager->setOutput($output); + } + } + + /** + * Verify that the migration directory exists and is writable. + * + * @param string $path Path + * @throws \InvalidArgumentException + * @return void + */ + protected function verifyMigrationDirectory($path) + { + if (!is_dir($path)) { + throw new InvalidArgumentException(sprintf( + 'Migration directory "%s" does not exist', + $path + )); + } + + if (!is_writable($path)) { + throw new InvalidArgumentException(sprintf( + 'Migration directory "%s" is not writable', + $path + )); + } + } + + /** + * Verify that the seed directory exists and is writable. + * + * @param string $path Path + * @throws \InvalidArgumentException + * @return void + */ + protected function verifySeedDirectory($path) + { + if (!is_dir($path)) { + throw new InvalidArgumentException(sprintf( + 'Seed directory "%s" does not exist', + $path + )); + } + + if (!is_writable($path)) { + throw new InvalidArgumentException(sprintf( + 'Seed directory "%s" is not writable', + $path + )); + } + } + + /** + * Returns the migration template filename. + * + * @return string + */ + protected function getMigrationTemplateFilename() + { + return __DIR__ . self::DEFAULT_MIGRATION_TEMPLATE; + } + + /** + * Returns the seed template filename. + * + * @return string + */ + protected function getSeedTemplateFilename() + { + return __DIR__ . self::DEFAULT_SEED_TEMPLATE; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/Breakpoint.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Breakpoint.php new file mode 100644 index 0000000..7230d72 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Breakpoint.php @@ -0,0 +1,106 @@ +addOption('--environment', '-e', InputOption::VALUE_REQUIRED, 'The target environment.'); + + $this->setDescription('Manage breakpoints') + ->addOption('--target', '-t', InputOption::VALUE_REQUIRED, 'The version number to target for the breakpoint') + ->addOption('--set', '-s', InputOption::VALUE_NONE, 'Set the breakpoint') + ->addOption('--unset', '-u', InputOption::VALUE_NONE, 'Unset the breakpoint') + ->addOption('--remove-all', '-r', InputOption::VALUE_NONE, 'Remove all breakpoints') + ->setHelp( + <<breakpoint command allows you to toggle, set, or unset a breakpoint against a specific target to inhibit rollbacks beyond a certain target. +If no target is supplied then the most recent migration will be used. +You cannot specify un-migrated targets + +phinx breakpoint -e development +phinx breakpoint -e development -t 20110103081132 +phinx breakpoint -e development -r +EOT + ); + } + + /** + * Toggle the breakpoint. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @throws \InvalidArgumentException + * @return int integer 0 on success, or an error code. + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->bootstrap($input, $output); + + $environment = $input->getOption('environment'); + $version = (int)$input->getOption('target') ?: null; + $removeAll = $input->getOption('remove-all'); + $set = $input->getOption('set'); + $unset = $input->getOption('unset'); + + if ($environment === null) { + $environment = $this->getConfig()->getDefaultEnvironment(); + $output->writeln('warning no environment specified, defaulting to: ' . $environment); + } else { + $output->writeln('using environment ' . $environment); + } + + if (!$this->getConfig()->hasEnvironment($environment)) { + $output->writeln(sprintf('The environment "%s" does not exist', $environment)); + + return self::CODE_ERROR; + } + + if ($version && $removeAll) { + throw new InvalidArgumentException('Cannot toggle a breakpoint and remove all breakpoints at the same time.'); + } + + if (($set && $unset) || ($set && $removeAll) || ($unset && $removeAll)) { + throw new InvalidArgumentException('Cannot use more than one of --set, --unset, or --remove-all at the same time.'); + } + + if ($removeAll) { + // Remove all breakpoints. + $this->getManager()->removeBreakpoints($environment); + } elseif ($set) { + // Set the breakpoint. + $this->getManager()->setBreakpoint($environment, $version); + } elseif ($unset) { + // Unset the breakpoint. + $this->getManager()->unsetBreakpoint($environment, $version); + } else { + // Toggle the breakpoint. + $this->getManager()->toggleBreakpoint($environment, $version); + } + + return self::CODE_SUCCESS; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/Create.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Create.php new file mode 100644 index 0000000..75f805d --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Create.php @@ -0,0 +1,314 @@ +setDescription('Create a new migration') + ->addArgument('name', InputArgument::OPTIONAL, 'Class name of the migration (in CamelCase)') + ->setHelp(sprintf( + '%sCreates a new database migration%s', + PHP_EOL, + PHP_EOL + )); + + // An alternative template. + $this->addOption('template', 't', InputOption::VALUE_REQUIRED, 'Use an alternative template'); + + // A classname to be used to gain access to the template content as well as the ability to + // have a callback once the migration file has been created. + $this->addOption('class', 'l', InputOption::VALUE_REQUIRED, 'Use a class implementing "' . self::CREATION_INTERFACE . '" to generate the template'); + + // Allow the migration path to be chosen non-interactively. + $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'Specify the path in which to create this migration'); + } + + /** + * Get the confirmation question asking if the user wants to create the + * migrations directory. + * + * @return \Symfony\Component\Console\Question\ConfirmationQuestion + */ + protected function getCreateMigrationDirectoryQuestion() + { + return new ConfirmationQuestion('Create migrations directory? [y]/n ', true); + } + + /** + * Get the question that allows the user to select which migration path to use. + * + * @param string[] $paths Paths + * @return \Symfony\Component\Console\Question\ChoiceQuestion + */ + protected function getSelectMigrationPathQuestion(array $paths) + { + return new ChoiceQuestion('Which migrations path would you like to use?', $paths, 0); + } + + /** + * Returns the migration path to create the migration in. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @throws \Exception + * @return string + */ + protected function getMigrationPath(InputInterface $input, OutputInterface $output) + { + // First, try the non-interactive option: + $path = $input->getOption('path'); + + if (!empty($path)) { + return $path; + } + + $paths = $this->getConfig()->getMigrationPaths(); + + // No paths? That's a problem. + if (empty($paths)) { + throw new Exception('No migration paths set in your Phinx configuration file.'); + } + + $paths = Util::globAll($paths); + + if (empty($paths)) { + throw new Exception( + 'You probably used curly braces to define migration path in your Phinx configuration file, ' . + 'but no directories have been matched using this pattern. ' . + 'You need to create a migration directory manually.' + ); + } + + // Only one path set, so select that: + if (count($paths) === 1) { + return array_shift($paths); + } + + /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = $this->getSelectMigrationPathQuestion($paths); + + return $helper->ask($input, $output, $question); + } + + /** + * Create the new migration. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return int 0 on success + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->bootstrap($input, $output); + + // get the migration path from the config + $path = $this->getMigrationPath($input, $output); + + if (!file_exists($path)) { + /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = $this->getCreateMigrationDirectoryQuestion(); + + if ($helper->ask($input, $output, $question)) { + mkdir($path, 0755, true); + } + } + + $this->verifyMigrationDirectory($path); + + $config = $this->getConfig(); + $namespace = $config instanceof NamespaceAwareInterface ? $config->getMigrationNamespaceByPath($path) : null; + + $path = realpath($path); + $className = $input->getArgument('name'); + if ($className === null) { + $currentTimestamp = Util::getCurrentTimestamp(); + $className = 'V' . $currentTimestamp; + $fileName = $currentTimestamp . '.php'; + } else { + if (!Util::isValidPhinxClassName($className)) { + throw new InvalidArgumentException(sprintf( + 'The migration class name "%s" is invalid. Please use CamelCase format.', + $className + )); + } + + // Compute the file path + $fileName = Util::mapClassNameToFileName($className); + } + + if (!Util::isUniqueMigrationClassName($className, $path)) { + throw new InvalidArgumentException(sprintf( + 'The migration class name "%s%s" already exists', + $namespace ? $namespace . '\\' : '', + $className + )); + } + + $filePath = $path . DIRECTORY_SEPARATOR . $fileName; + + if (is_file($filePath)) { + throw new InvalidArgumentException(sprintf( + 'The file "%s" already exists', + $filePath + )); + } + + // Get the alternative template and static class options from the config, but only allow one of them. + $defaultAltTemplate = $this->getConfig()->getTemplateFile(); + $defaultCreationClassName = $this->getConfig()->getTemplateClass(); + if ($defaultAltTemplate && $defaultCreationClassName) { + throw new InvalidArgumentException('Cannot define template:class and template:file at the same time'); + } + + // Get the alternative template and static class options from the command line, but only allow one of them. + /** @phpstan-var class-string|null $altTemplate */ + $altTemplate = $input->getOption('template'); + /** @phpstan-var class-string|null $creationClassName */ + $creationClassName = $input->getOption('class'); + if ($altTemplate && $creationClassName) { + throw new InvalidArgumentException('Cannot use --template and --class at the same time'); + } + + // If no commandline options then use the defaults. + if (!$altTemplate && !$creationClassName) { + $altTemplate = $defaultAltTemplate; + $creationClassName = $defaultCreationClassName; + } + + // Verify the alternative template file's existence. + if ($altTemplate && !is_file($altTemplate)) { + throw new InvalidArgumentException(sprintf( + 'The alternative template file "%s" does not exist', + $altTemplate + )); + } + + // Verify that the template creation class (or the aliased class) exists and that it implements the required interface. + $aliasedClassName = null; + if ($creationClassName) { + // Supplied class does not exist, is it aliased? + if (!class_exists($creationClassName)) { + $aliasedClassName = $this->getConfig()->getAlias($creationClassName); + if ($aliasedClassName && !class_exists($aliasedClassName)) { + throw new InvalidArgumentException(sprintf( + 'The class "%s" via the alias "%s" does not exist', + $aliasedClassName, + $creationClassName + )); + } elseif (!$aliasedClassName) { + throw new InvalidArgumentException(sprintf( + 'The class "%s" does not exist', + $creationClassName + )); + } + } + + // Does the class implement the required interface? + if (!$aliasedClassName && !is_subclass_of($creationClassName, self::CREATION_INTERFACE)) { + throw new InvalidArgumentException(sprintf( + 'The class "%s" does not implement the required interface "%s"', + $creationClassName, + self::CREATION_INTERFACE + )); + } elseif ($aliasedClassName && !is_subclass_of($aliasedClassName, self::CREATION_INTERFACE)) { + throw new InvalidArgumentException(sprintf( + 'The class "%s" via the alias "%s" does not implement the required interface "%s"', + $aliasedClassName, + $creationClassName, + self::CREATION_INTERFACE + )); + } + } + + // Use the aliased class. + $creationClassName = $aliasedClassName ?: $creationClassName; + + // Determine the appropriate mechanism to get the template + if ($creationClassName) { + // Get the template from the creation class + $creationClass = new $creationClassName($input, $output); + $contents = $creationClass->getMigrationTemplate(); + } else { + // Load the alternative template if it is defined. + $contents = file_get_contents($altTemplate ?: $this->getMigrationTemplateFilename()); + } + + // inject the class names appropriate to this migration + $classes = [ + '$namespaceDefinition' => $namespace !== null ? (PHP_EOL . 'namespace ' . $namespace . ';' . PHP_EOL) : '', + '$namespace' => $namespace, + '$useClassName' => $this->getConfig()->getMigrationBaseClassName(false), + '$className' => $className, + '$version' => Util::getVersionFromFileName($fileName), + '$baseClassName' => $this->getConfig()->getMigrationBaseClassName(true), + ]; + $contents = strtr($contents, $classes); + + if (file_put_contents($filePath, $contents) === false) { + throw new RuntimeException(sprintf( + 'The file "%s" could not be written to', + $path + )); + } + + // Do we need to do the post creation call to the creation class? + if (isset($creationClass)) { + /** @var \Phinx\Migration\CreationInterface $creationClass */ + $creationClass->postMigrationCreation($filePath, $className, $this->getConfig()->getMigrationBaseClassName()); + } + + $output->writeln('using migration base class ' . $classes['$useClassName']); + + if (!empty($altTemplate)) { + $output->writeln('using alternative template ' . $altTemplate); + } elseif (!empty($creationClassName)) { + $output->writeln('using template creation class ' . $creationClassName); + } else { + $output->writeln('using default template'); + } + + $output->writeln('created ' . Util::relativePath($filePath)); + + return self::CODE_SUCCESS; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/Init.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Init.php new file mode 100644 index 0000000..d5cf9a9 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Init.php @@ -0,0 +1,169 @@ +setDescription('Initialize the application for Phinx') + ->addOption( + '--format', + '-f', + InputArgument::OPTIONAL, + 'What format should we use to initialize?', + AbstractCommand::FORMAT_DEFAULT + ) + ->addArgument('path', InputArgument::OPTIONAL, 'Which path should we initialize for Phinx?') + ->setHelp(sprintf( + '%sInitializes the application for Phinx%s', + PHP_EOL, + PHP_EOL + )); + } + + /** + * Initializes the application. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Interface implemented by all input classes. + * @param \Symfony\Component\Console\Output\OutputInterface $output Interface implemented by all output classes. + * @return int 0 on success + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $format = strtolower($input->getOption('format')); + $path = $this->resolvePath($input, $format); + $this->writeConfig($path, $format); + + $output->writeln("created {$path}"); + + return AbstractCommand::CODE_SUCCESS; + } + + /** + * Return valid $path for Phinx's config file. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Interface implemented by all input classes. + * @param string $format Format to resolve for + * @throws \InvalidArgumentException + * @return string + */ + protected function resolvePath(InputInterface $input, $format) + { + // get the migration path from the config + $path = (string)$input->getArgument('path'); + + if (!in_array($format, static::$supportedFormats, true)) { + throw new InvalidArgumentException(sprintf( + 'Invalid format "%s". Format must be either ' . implode(', ', static::$supportedFormats) . '.', + $format + )); + } + + // Fallback + if (!$path) { + $path = getcwd() . DIRECTORY_SEPARATOR . self::FILE_NAME . '.' . $format; + } + + // Adding file name if necessary + if (is_dir($path)) { + $path .= DIRECTORY_SEPARATOR . self::FILE_NAME . '.' . $format; + } + + // Check if path is available + $dirname = dirname($path); + if (is_dir($dirname) && !is_file($path)) { + return $path; + } + + // Path is valid, but file already exists + if (is_file($path)) { + throw new InvalidArgumentException(sprintf( + 'Config file "%s" already exists.', + $path + )); + } + + // Dir is invalid + throw new InvalidArgumentException(sprintf( + 'Invalid path "%s" for config file.', + $path + )); + } + + /** + * Writes Phinx's config in provided $path + * + * @param string $path Config file's path. + * @param string $format Format to use for config file + * @throws \InvalidArgumentException + * @throws \RuntimeException + * @return void + */ + protected function writeConfig($path, $format = AbstractCommand::FORMAT_DEFAULT) + { + // Check if dir is writable + $dirname = dirname($path); + if (!is_writable($dirname)) { + throw new InvalidArgumentException(sprintf( + 'The directory "%s" is not writable', + $dirname + )); + } + + if ($format === AbstractCommand::FORMAT_YML_ALIAS) { + $format = AbstractCommand::FORMAT_YML; + } + + // load the config template + if (is_dir(__DIR__ . '/../../../../data')) { + $contents = file_get_contents(__DIR__ . '/../../../../data/' . self::FILE_NAME . '.' . $format . '.dist'); + } else { + throw new RuntimeException(sprintf( + 'Could not find template for format "%s".', + $format + )); + } + + if (file_put_contents($path, $contents) === false) { + throw new RuntimeException(sprintf( + 'The file "%s" could not be written to', + $path + )); + } + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/ListAliases.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/ListAliases.php new file mode 100644 index 0000000..2c791e7 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/ListAliases.php @@ -0,0 +1,77 @@ +setDescription('List template class aliases') + ->setHelp('The list:aliases command lists the migration template generation class aliases'); + } + + /** + * List migration template creation aliases. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return int 0 on success + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->bootstrap($input, $output); + + $aliases = $this->config->getAliases(); + + if ($aliases) { + $maxAliasLength = max(array_map('strlen', array_keys($aliases))); + $maxClassLength = max(array_map('strlen', $aliases)); + $output->writeln( + array_merge( + [ + '', + sprintf('%s %s', str_pad('Alias', $maxAliasLength), str_pad('Class', $maxClassLength)), + sprintf('%s %s', str_repeat('=', $maxAliasLength), str_repeat('=', $maxClassLength)), + ], + array_map( + function ($alias, $class) use ($maxAliasLength, $maxClassLength) { + return sprintf('%s %s', str_pad($alias, $maxAliasLength), str_pad($class, $maxClassLength)); + }, + array_keys($aliases), + $aliases + ) + ) + ); + } else { + $output->writeln( + sprintf( + 'No aliases defined in %s', + Util::relativePath($this->config->getConfigFilePath()) + ) + ); + } + + return self::CODE_SUCCESS; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/Migrate.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Migrate.php new file mode 100644 index 0000000..684e30a --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Migrate.php @@ -0,0 +1,140 @@ +addOption('--environment', '-e', InputOption::VALUE_REQUIRED, 'The target environment'); + + $this->setDescription('Migrate the database') + ->addOption('--target', '-t', InputOption::VALUE_REQUIRED, 'The version number to migrate to') + ->addOption('--date', '-d', InputOption::VALUE_REQUIRED, 'The date to migrate to') + ->addOption('--dry-run', '-x', InputOption::VALUE_NONE, 'Dump query to standard output instead of executing it') + ->addOption('--fake', null, InputOption::VALUE_NONE, "Mark any migrations selected as run, but don't actually execute them") + ->setHelp( + <<migrate command runs all available migrations, optionally up to a specific version + +phinx migrate -e development +phinx migrate -e development -t 20110103081132 +phinx migrate -e development -d 20110103 +phinx migrate -e development -v + +EOT + ); + } + + /** + * Migrate the database. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return int integer 0 on success, or an error code. + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->bootstrap($input, $output); + + $version = $input->getOption('target'); + $environment = $input->getOption('environment'); + $date = $input->getOption('date'); + $fake = (bool)$input->getOption('fake'); + + if ($environment === null) { + $environment = $this->getConfig()->getDefaultEnvironment(); + $output->writeln('warning no environment specified, defaulting to: ' . $environment); + } else { + $output->writeln('using environment ' . $environment); + } + + if (!$this->getConfig()->hasEnvironment($environment)) { + $output->writeln(sprintf('The environment "%s" does not exist', $environment)); + + return self::CODE_ERROR; + } + + $envOptions = $this->getConfig()->getEnvironment($environment); + if (isset($envOptions['adapter'])) { + $output->writeln('using adapter ' . $envOptions['adapter']); + } + + if (isset($envOptions['wrapper'])) { + $output->writeln('using wrapper ' . $envOptions['wrapper']); + } + + if (isset($envOptions['name'])) { + $output->writeln('using database ' . $envOptions['name']); + } else { + $output->writeln('Could not determine database name! Please specify a database name in your config file.'); + + return self::CODE_ERROR; + } + + if (isset($envOptions['table_prefix'])) { + $output->writeln('using table prefix ' . $envOptions['table_prefix']); + } + if (isset($envOptions['table_suffix'])) { + $output->writeln('using table suffix ' . $envOptions['table_suffix']); + } + + $versionOrder = $this->getConfig()->getVersionOrder(); + $output->writeln('ordering by ' . $versionOrder . ' time'); + + if ($fake) { + $output->writeln('warning performing fake migrations'); + } + + try { + // run the migrations + $start = microtime(true); + if ($date !== null) { + $this->getManager()->migrateToDateTime($environment, new DateTime($date), $fake); + } else { + if ($version) { + $version = (int)$version; + } + $this->getManager()->migrate($environment, $version, $fake); + } + $end = microtime(true); + } catch (Exception $e) { + $output->writeln('' . $e->__toString() . ''); + + return self::CODE_ERROR; + } catch (Throwable $e) { + $output->writeln('' . $e->__toString() . ''); + + return self::CODE_ERROR; + } + + $output->writeln(''); + $output->writeln('All Done. Took ' . sprintf('%.4fs', $end - $start) . ''); + + return self::CODE_SUCCESS; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/Rollback.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Rollback.php new file mode 100644 index 0000000..3be48af --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Rollback.php @@ -0,0 +1,169 @@ +addOption('--environment', '-e', InputOption::VALUE_REQUIRED, 'The target environment'); + + $this->setDescription('Rollback the last or to a specific migration') + ->addOption('--target', '-t', InputOption::VALUE_REQUIRED, 'The version number to rollback to') + ->addOption('--date', '-d', InputOption::VALUE_REQUIRED, 'The date to rollback to') + ->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force rollback to ignore breakpoints') + ->addOption('--dry-run', '-x', InputOption::VALUE_NONE, 'Dump query to standard output instead of executing it') + ->addOption('--fake', null, InputOption::VALUE_NONE, "Mark any rollbacks selected as run, but don't actually execute them") + ->setHelp( + <<rollback command reverts the last migration, or optionally up to a specific version + +phinx rollback -e development +phinx rollback -e development -t 20111018185412 +phinx rollback -e development -d 20111018 +phinx rollback -e development -v +phinx rollback -e development -t 20111018185412 -f + +If you have a breakpoint set, then you can rollback to target 0 and the rollbacks will stop at the breakpoint. +phinx rollback -e development -t 0 + +The version_order configuration option is used to determine the order of the migrations when rolling back. +This can be used to allow the rolling back of the last executed migration instead of the last created one, or combined +with the -d|--date option to rollback to a certain date using the migration start times to order them. + +EOT + ); + } + + /** + * Rollback the migration. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return int integer 0 on success, or an error code. + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->bootstrap($input, $output); + + $environment = $input->getOption('environment'); + $version = $input->getOption('target'); + $date = $input->getOption('date'); + $force = (bool)$input->getOption('force'); + $fake = (bool)$input->getOption('fake'); + + $config = $this->getConfig(); + + if ($environment === null) { + $environment = $config->getDefaultEnvironment(); + $output->writeln('warning no environment specified, defaulting to: ' . $environment); + } else { + $output->writeln('using environment ' . $environment); + } + + if (!$this->getConfig()->hasEnvironment($environment)) { + $output->writeln(sprintf('The environment "%s" does not exist', $environment)); + + return self::CODE_ERROR; + } + + $envOptions = $config->getEnvironment($environment); + if (isset($envOptions['adapter'])) { + $output->writeln('using adapter ' . $envOptions['adapter']); + } + + if (isset($envOptions['wrapper'])) { + $output->writeln('using wrapper ' . $envOptions['wrapper']); + } + + if (isset($envOptions['name'])) { + $output->writeln('using database ' . $envOptions['name']); + } + + $versionOrder = $this->getConfig()->getVersionOrder(); + $output->writeln('ordering by ' . $versionOrder . ' time'); + + if ($fake) { + $output->writeln('warning performing fake rollbacks'); + } + + // rollback the specified environment + if ($date === null) { + $targetMustMatchVersion = true; + $target = $version; + } else { + $targetMustMatchVersion = false; + $target = $this->getTargetFromDate($date); + } + + $start = microtime(true); + $this->getManager()->rollback($environment, $target, $force, $targetMustMatchVersion, $fake); + $end = microtime(true); + + $output->writeln(''); + $output->writeln('All Done. Took ' . sprintf('%.4fs', $end - $start) . ''); + + return self::CODE_SUCCESS; + } + + /** + * Get Target from Date + * + * @param string $date The date to convert to a target. + * @throws \InvalidArgumentException + * @return string The target + */ + public function getTargetFromDate($date) + { + if (!preg_match('/^\d{4,14}$/', $date)) { + throw new InvalidArgumentException('Invalid date. Format is YYYY[MM[DD[HH[II[SS]]]]].'); + } + + // what we need to append to the date according to the possible date string lengths + $dateStrlenToAppend = [ + 14 => '', + 12 => '00', + 10 => '0000', + 8 => '000000', + 6 => '01000000', + 4 => '0101000000', + ]; + + if (!isset($dateStrlenToAppend[strlen($date)])) { + throw new InvalidArgumentException('Invalid date. Format is YYYY[MM[DD[HH[II[SS]]]]].'); + } + + $target = $date . $dateStrlenToAppend[strlen($date)]; + + $dateTime = DateTime::createFromFormat('YmdHis', $target); + + if ($dateTime === false) { + throw new InvalidArgumentException('Invalid date. Format is YYYY[MM[DD[HH[II[SS]]]]].'); + } + + return $dateTime->format('YmdHis'); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/SeedCreate.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/SeedCreate.php new file mode 100644 index 0000000..a910241 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/SeedCreate.php @@ -0,0 +1,205 @@ +setDescription('Create a new database seeder') + ->addArgument('name', InputArgument::REQUIRED, 'What is the name of the seeder?') + ->addOption('path', null, InputOption::VALUE_REQUIRED, 'Specify the path in which to create this seeder') + ->setHelp(sprintf( + '%sCreates a new database seeder%s', + PHP_EOL, + PHP_EOL + )); + + // An alternative template. + $this->addOption('template', 't', InputOption::VALUE_REQUIRED, 'Use an alternative template'); + } + + /** + * Get the confirmation question asking if the user wants to create the + * seeds directory. + * + * @return \Symfony\Component\Console\Question\ConfirmationQuestion + */ + protected function getCreateSeedDirectoryQuestion() + { + return new ConfirmationQuestion('Create seeds directory? [y]/n ', true); + } + + /** + * Get the question that allows the user to select which seed path to use. + * + * @param string[] $paths Paths + * @return \Symfony\Component\Console\Question\ChoiceQuestion + */ + protected function getSelectSeedPathQuestion(array $paths) + { + return new ChoiceQuestion('Which seeds path would you like to use?', $paths, 0); + } + + /** + * Returns the seed path to create the seeder in. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @throws \Exception + * @return string + */ + protected function getSeedPath(InputInterface $input, OutputInterface $output) + { + // First, try the non-interactive option: + $path = $input->getOption('path'); + + if (!empty($path)) { + return $path; + } + + $paths = $this->getConfig()->getSeedPaths(); + + // No paths? That's a problem. + if (empty($paths)) { + throw new Exception('No seed paths set in your Phinx configuration file.'); + } + + $paths = Util::globAll($paths); + + if (empty($paths)) { + throw new Exception( + 'You probably used curly braces to define seed path in your Phinx configuration file, ' . + 'but no directories have been matched using this pattern. ' . + 'You need to create a seed directory manually.' + ); + } + + // Only one path set, so select that: + if (count($paths) === 1) { + return array_shift($paths); + } + + /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = $this->getSelectSeedPathQuestion($paths); + + return $helper->ask($input, $output, $question); + } + + /** + * Create the new seeder. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return int 0 on success + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->bootstrap($input, $output); + + // get the seed path from the config + $path = $this->getSeedPath($input, $output); + + if (!file_exists($path)) { + /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = $this->getCreateSeedDirectoryQuestion(); + + if ($helper->ask($input, $output, $question)) { + mkdir($path, 0755, true); + } + } + + $this->verifySeedDirectory($path); + + $path = realpath($path); + $className = $input->getArgument('name'); + + if (!Util::isValidPhinxClassName($className)) { + throw new InvalidArgumentException(sprintf( + 'The seed class name "%s" is invalid. Please use CamelCase format', + $className + )); + } + + // Compute the file path + $filePath = $path . DIRECTORY_SEPARATOR . $className . '.php'; + + if (is_file($filePath)) { + throw new InvalidArgumentException(sprintf( + 'The file "%s" already exists', + basename($filePath) + )); + } + + // Get the alternative template option from the command line. + $altTemplate = $input->getOption('template'); + + // Verify the alternative template file's existence. + if ($altTemplate && !is_file($altTemplate)) { + throw new InvalidArgumentException(sprintf( + 'The template file "%s" does not exist', + $altTemplate + )); + } + + // Determine the appropriate mechanism to get the template + // Load the alternative template if it is defined. + $contents = file_get_contents($altTemplate ?: $this->getSeedTemplateFilename()); + + $config = $this->getConfig(); + $namespace = $config instanceof NamespaceAwareInterface ? $config->getSeedNamespaceByPath($path) : null; + $classes = [ + '$namespaceDefinition' => $namespace !== null ? ('namespace ' . $namespace . ';') : '', + '$namespace' => $namespace, + '$useClassName' => $config->getSeedBaseClassName(false), + '$className' => $className, + '$baseClassName' => $config->getSeedBaseClassName(true), + ]; + $contents = strtr($contents, $classes); + + if (file_put_contents($filePath, $contents) === false) { + throw new RuntimeException(sprintf( + 'The file "%s" could not be written to', + $path + )); + } + + $output->writeln('using seed base class ' . $classes['$useClassName']); + $output->writeln('created ' . Util::relativePath($filePath)); + + return self::CODE_SUCCESS; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/SeedRun.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/SeedRun.php new file mode 100644 index 0000000..9960098 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/SeedRun.php @@ -0,0 +1,117 @@ +addOption('--environment', '-e', InputOption::VALUE_REQUIRED, 'The target environment'); + + $this->setDescription('Run database seeders') + ->addOption('--seed', '-s', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'What is the name of the seeder?') + ->setHelp( + <<seed:run command runs all available or individual seeders + +phinx seed:run -e development +phinx seed:run -e development -s UserSeeder +phinx seed:run -e development -s UserSeeder -s PermissionSeeder -s LogSeeder +phinx seed:run -e development -v + +EOT + ); + } + + /** + * Run database seeders. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return int integer 0 on success, or an error code. + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->bootstrap($input, $output); + + $seedSet = $input->getOption('seed'); + $environment = $input->getOption('environment'); + + if ($environment === null) { + $environment = $this->getConfig()->getDefaultEnvironment(); + $output->writeln('warning no environment specified, defaulting to: ' . $environment); + } else { + $output->writeln('using environment ' . $environment); + } + + if (!$this->getConfig()->hasEnvironment($environment)) { + $output->writeln(sprintf('The environment "%s" does not exist', $environment)); + + return self::CODE_ERROR; + } + + $envOptions = $this->getConfig()->getEnvironment($environment); + if (isset($envOptions['adapter'])) { + $output->writeln('using adapter ' . $envOptions['adapter']); + } + + if (isset($envOptions['wrapper'])) { + $output->writeln('using wrapper ' . $envOptions['wrapper']); + } + + if (isset($envOptions['name'])) { + $output->writeln('using database ' . $envOptions['name']); + } else { + $output->writeln('Could not determine database name! Please specify a database name in your config file.'); + + return self::CODE_ERROR; + } + + if (isset($envOptions['table_prefix'])) { + $output->writeln('using table prefix ' . $envOptions['table_prefix']); + } + if (isset($envOptions['table_suffix'])) { + $output->writeln('using table suffix ' . $envOptions['table_suffix']); + } + + $start = microtime(true); + + if (empty($seedSet)) { + // run all the seed(ers) + $this->getManager()->seed($environment); + } else { + // run seed(ers) specified in a comma-separated list of classes + foreach ($seedSet as $seed) { + $this->getManager()->seed($environment, trim($seed)); + } + } + + $end = microtime(true); + + $output->writeln(''); + $output->writeln('All Done. Took ' . sprintf('%.4fs', $end - $start) . ''); + + return self::CODE_SUCCESS; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/Status.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Status.php new file mode 100644 index 0000000..9288f78 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Status.php @@ -0,0 +1,90 @@ +addOption('--environment', '-e', InputOption::VALUE_REQUIRED, 'The target environment.'); + + $this->setDescription('Show migration status') + ->addOption('--format', '-f', InputOption::VALUE_REQUIRED, 'The output format: text or json. Defaults to text.') + ->setHelp( + <<status command prints a list of all migrations, along with their current status + +phinx status -e development +phinx status -e development -f json + +The version_order configuration option is used to determine the order of the status migrations. +EOT + ); + } + + /** + * Show the migration status. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return int 0 if all migrations are up, or an error code + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->bootstrap($input, $output); + + $environment = $input->getOption('environment'); + $format = $input->getOption('format'); + + if ($environment === null) { + $environment = $this->getConfig()->getDefaultEnvironment(); + $output->writeln('warning no environment specified, defaulting to: ' . $environment); + } else { + $output->writeln('using environment ' . $environment); + } + + if (!$this->getConfig()->hasEnvironment($environment)) { + $output->writeln(sprintf('The environment "%s" does not exist', $environment)); + + return self::CODE_ERROR; + } + + if ($format !== null) { + $output->writeln('using format ' . $format); + } + + $output->writeln('ordering by ' . $this->getConfig()->getVersionOrder() . ' time'); + + // print the status + $result = $this->getManager()->printStatus($environment, $format); + + if ($result['hasMissingMigration']) { + return self::CODE_STATUS_MISSING; + } elseif ($result['hasDownMigration']) { + return self::CODE_STATUS_DOWN; + } + + return self::CODE_SUCCESS; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/Command/Test.php b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Test.php new file mode 100644 index 0000000..bfd1fc4 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/Command/Test.php @@ -0,0 +1,98 @@ + + */ +class Test extends AbstractCommand +{ + /** + * @var string + */ + protected static $defaultName = 'test'; + + /** + * {@inheritDoc} + * + * @return void + */ + protected function configure() + { + parent::configure(); + + $this->addOption('--environment', '-e', InputOption::VALUE_REQUIRED, 'The target environment'); + + $this->setDescription('Verify the configuration file') + ->setHelp( + <<test command is used to verify the phinx configuration file and optionally an environment + +phinx test +phinx test -e development + +If the environment option is set, it will test that phinx can connect to the DB associated with that environment +EOT + ); + } + + /** + * Verify configuration file + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @throws \InvalidArgumentException + * @return int 0 on success + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->loadConfig($input, $output); + $this->loadManager($input, $output); + + // Verify the migrations path(s) + array_map( + [$this, 'verifyMigrationDirectory'], + Util::globAll($this->getConfig()->getMigrationPaths()) + ); + + // Verify the seed path(s) + array_map( + [$this, 'verifySeedDirectory'], + Util::globAll($this->getConfig()->getSeedPaths()) + ); + + $envName = $input->getOption('environment'); + if ($envName) { + if (!$this->getConfig()->hasEnvironment($envName)) { + throw new InvalidArgumentException(sprintf( + 'The environment "%s" does not exist', + $envName + )); + } + + $output->writeln(sprintf('validating environment %s', $envName)); + $environment = new Environment( + $envName, + $this->getConfig()->getEnvironment($envName) + ); + // validate environment connection + $environment->getAdapter()->connect(); + } + + $output->writeln('success!'); + + return self::CODE_SUCCESS; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Console/PhinxApplication.php b/vendor/robmorgan/phinx/src/Phinx/Console/PhinxApplication.php new file mode 100644 index 0000000..bd72ebf --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Console/PhinxApplication.php @@ -0,0 +1,70 @@ + + */ +class PhinxApplication extends Application +{ + /** + * Initialize the Phinx console application. + */ + public function __construct() + { + parent::__construct('Phinx by CakePHP - https://phinx.org.'); + + $this->addCommands([ + new Init(), + new Create(), + new Migrate(), + new Rollback(), + new Status(), + new Breakpoint(), + new Test(), + new SeedCreate(), + new SeedRun(), + new ListAliases(), + ]); + } + + /** + * Runs the current application. + * + * @param \Symfony\Component\Console\Input\InputInterface $input An Input instance + * @param \Symfony\Component\Console\Output\OutputInterface $output An Output instance + * @return int 0 if everything went fine, or an error code + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + // always show the version information except when the user invokes the help + // command as that already does it + if (($input->hasParameterOption(['--help', '-h']) !== false) || ($input->getFirstArgument() !== null && $input->getFirstArgument() !== 'list')) { + $output->writeln($this->getLongVersion()); + $output->writeln(''); + } + + return parent::doRun($input, $output); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/Action.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/Action.php new file mode 100644 index 0000000..8931011 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/Action.php @@ -0,0 +1,38 @@ +table = $table; + } + + /** + * The table this action will be applied to + * + * @return \Phinx\Db\Table\Table + */ + public function getTable() + { + return $this->table; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddColumn.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddColumn.php new file mode 100644 index 0000000..fadf2dd --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddColumn.php @@ -0,0 +1,62 @@ +column = $column; + } + + /** + * Returns a new AddColumn object after assembling the given commands + * + * @param \Phinx\Db\Table\Table $table The table to add the column to + * @param string $columnName The column name + * @param mixed $type The column type + * @param mixed $options The column options + * @return \Phinx\Db\Action\AddColumn + */ + public static function build(Table $table, $columnName, $type = null, $options = []) + { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); // map options to column methods + + return new static($table, $column); + } + + /** + * Returns the column to be added + * + * @return \Phinx\Db\Table\Column + */ + public function getColumn() + { + return $this->column; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddForeignKey.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddForeignKey.php new file mode 100644 index 0000000..1e0cc77 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddForeignKey.php @@ -0,0 +1,78 @@ +foreignKey = $fk; + } + + /** + * Creates a new AddForeignKey object after building the foreign key with + * the passed attributes + * + * @param \Phinx\Db\Table\Table $table The table object to add the foreign key to + * @param string|string[] $columns The columns for the foreign key + * @param \Phinx\Db\Table\Table|string $referencedTable The table the foreign key references + * @param string|string[] $referencedColumns The columns in the referenced table + * @param array $options Extra options for the foreign key + * @param string|null $name The name of the foreign key + * @return \Phinx\Db\Action\AddForeignKey + */ + public static function build(Table $table, $columns, $referencedTable, $referencedColumns = ['id'], array $options = [], $name = null) + { + if (is_string($referencedColumns)) { + $referencedColumns = [$referencedColumns]; // str to array + } + + if (is_string($referencedTable)) { + $referencedTable = new Table($referencedTable); + } + + $fk = new ForeignKey(); + $fk->setReferencedTable($referencedTable) + ->setColumns($columns) + ->setReferencedColumns($referencedColumns) + ->setOptions($options); + + if ($name !== null) { + $fk->setConstraint($name); + } + + return new static($table, $fk); + } + + /** + * Returns the foreign key to be added + * + * @return \Phinx\Db\Table\ForeignKey + */ + public function getForeignKey() + { + return $this->foreignKey; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddIndex.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddIndex.php new file mode 100644 index 0000000..c68012e --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/AddIndex.php @@ -0,0 +1,71 @@ +index = $index; + } + + /** + * Creates a new AddIndex object after building the index object with the + * provided arguments + * + * @param \Phinx\Db\Table\Table $table The table to add the index to + * @param mixed $columns The columns to index + * @param array $options Additional options for the index creation + * @return \Phinx\Db\Action\AddIndex + */ + public static function build(Table $table, $columns, array $options = []) + { + // create a new index object if strings or an array of strings were supplied + $index = $columns; + + if (!$columns instanceof Index) { + $index = new Index(); + + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + + $index->setColumns($columns); + $index->setOptions($options); + } + + return new static($table, $index); + } + + /** + * Returns the index to be added + * + * @return \Phinx\Db\Table\Index + */ + public function getIndex() + { + return $this->index; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangeColumn.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangeColumn.php new file mode 100644 index 0000000..bdc62a3 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangeColumn.php @@ -0,0 +1,87 @@ +columnName = $columnName; + $this->column = $column; + + // if the name was omitted use the existing column name + if ($column->getName() === null || strlen($column->getName()) === 0) { + $column->setName($columnName); + } + } + + /** + * Creates a new ChangeColumn object after building the column definition + * out of the provided arguments + * + * @param \Phinx\Db\Table\Table $table The table to alter + * @param mixed $columnName The name of the column to change + * @param mixed $type The type of the column + * @param mixed $options Additional options for the column + * @return \Phinx\Db\Action\ChangeColumn + */ + public static function build(Table $table, $columnName, $type = null, $options = []) + { + $column = new Column(); + $column->setName($columnName); + $column->setType($type); + $column->setOptions($options); // map options to column methods + + return new static($table, $columnName, $column); + } + + /** + * Returns the name of the column to change + * + * @return string + */ + public function getColumnName() + { + return $this->columnName; + } + + /** + * Returns the column definition + * + * @return \Phinx\Db\Table\Column + */ + public function getColumn() + { + return $this->column; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangeComment.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangeComment.php new file mode 100644 index 0000000..4842c79 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangeComment.php @@ -0,0 +1,42 @@ +newComment = $newComment; + } + + /** + * Return the new comment for the table + * + * @return string|null + */ + public function getNewComment() + { + return $this->newComment; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangePrimaryKey.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangePrimaryKey.php new file mode 100644 index 0000000..bb220fd --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/ChangePrimaryKey.php @@ -0,0 +1,42 @@ +newColumns = $newColumns; + } + + /** + * Return the new columns for the primary key + * + * @return string|string[]|null + */ + public function getNewColumns() + { + return $this->newColumns; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/CreateTable.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/CreateTable.php new file mode 100644 index 0000000..8f50afa --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/CreateTable.php @@ -0,0 +1,12 @@ +foreignKey = $foreignKey; + } + + /** + * Creates a new DropForeignKey object after building the ForeignKey + * definition out of the passed arguments. + * + * @param \Phinx\Db\Table\Table $table The table to delete the foreign key from + * @param string|string[] $columns The columns participating in the foreign key + * @param string|null $constraint The constraint name + * @return \Phinx\Db\Action\DropForeignKey + */ + public static function build(Table $table, $columns, $constraint = null) + { + if (is_string($columns)) { + $columns = [$columns]; + } + + $foreignKey = new ForeignKey(); + $foreignKey->setColumns($columns); + + if ($constraint) { + $foreignKey->setConstraint($constraint); + } + + return new static($table, $foreignKey); + } + + /** + * Returns the foreign key to remove + * + * @return \Phinx\Db\Table\ForeignKey + */ + public function getForeignKey() + { + return $this->foreignKey; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/DropIndex.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/DropIndex.php new file mode 100644 index 0000000..49ff940 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/DropIndex.php @@ -0,0 +1,75 @@ +index = $index; + } + + /** + * Creates a new DropIndex object after assembling the passed + * arguments. + * + * @param \Phinx\Db\Table\Table $table The table where the index is + * @param string[] $columns the indexed columns + * @return \Phinx\Db\Action\DropIndex + */ + public static function build(Table $table, array $columns = []) + { + $index = new Index(); + $index->setColumns($columns); + + return new static($table, $index); + } + + /** + * Creates a new DropIndex when the name of the index to drop + * is known. + * + * @param \Phinx\Db\Table\Table $table The table where the index is + * @param string $name The name of the index + * @return \Phinx\Db\Action\DropIndex + */ + public static function buildFromName(Table $table, $name) + { + $index = new Index(); + $index->setName($name); + + return new static($table, $index); + } + + /** + * Returns the index to be dropped + * + * @return \Phinx\Db\Table\Index + */ + public function getIndex() + { + return $this->index; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/DropTable.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/DropTable.php new file mode 100644 index 0000000..6343d0b --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/DropTable.php @@ -0,0 +1,12 @@ +column = $column; + } + + /** + * Creates a new RemoveColumn object after assembling the + * passed arguments. + * + * @param \Phinx\Db\Table\Table $table The table where the column is + * @param mixed $columnName The name of the column to drop + * @return \Phinx\Db\Action\RemoveColumn + */ + public static function build(Table $table, $columnName) + { + $column = new Column(); + $column->setName($columnName); + + return new static($table, $column); + } + + /** + * Returns the column to be dropped + * + * @return \Phinx\Db\Table\Column + */ + public function getColumn() + { + return $this->column; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/RenameColumn.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/RenameColumn.php new file mode 100644 index 0000000..53ffc44 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/RenameColumn.php @@ -0,0 +1,79 @@ +newName = $newName; + $this->column = $column; + } + + /** + * Creates a new RenameColumn object after building the passed + * arguments + * + * @param \Phinx\Db\Table\Table $table The table where the column is + * @param mixed $columnName The name of the column to be changed + * @param mixed $newName The new name for the column + * @return \Phinx\Db\Action\RenameColumn + */ + public static function build(Table $table, $columnName, $newName) + { + $column = new Column(); + $column->setName($columnName); + + return new static($table, $column, $newName); + } + + /** + * Returns the column to be changed + * + * @return \Phinx\Db\Table\Column + */ + public function getColumn() + { + return $this->column; + } + + /** + * Returns the new name for the column + * + * @return string + */ + public function getNewName() + { + return $this->newName; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Action/RenameTable.php b/vendor/robmorgan/phinx/src/Phinx/Db/Action/RenameTable.php new file mode 100644 index 0000000..bd0179d --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Action/RenameTable.php @@ -0,0 +1,42 @@ +newName = $newName; + } + + /** + * Return the new name for the table + * + * @return string + */ + public function getNewName() + { + return $this->newName; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AbstractAdapter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AbstractAdapter.php new file mode 100644 index 0000000..371a1c7 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AbstractAdapter.php @@ -0,0 +1,413 @@ +setOptions($options); + if ($input !== null) { + $this->setInput($input); + } + if ($output !== null) { + $this->setOutput($output); + } + } + + /** + * @inheritDoc + */ + public function setOptions(array $options) + { + $this->options = $options; + + if (isset($options['default_migration_table'])) { + $this->setSchemaTableName($options['default_migration_table']); + } + + if (isset($options['data_domain'])) { + $this->setDataDomain($options['data_domain']); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function getOptions() + { + return $this->options; + } + + /** + * @inheritDoc + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * @inheritDoc + */ + public function getOption($name) + { + if (!$this->hasOption($name)) { + return null; + } + + return $this->options[$name]; + } + + /** + * @inheritDoc + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + + return $this; + } + + /** + * @inheritDoc + */ + public function getInput() + { + return $this->input; + } + + /** + * @inheritDoc + */ + public function setOutput(OutputInterface $output) + { + $this->output = $output; + + return $this; + } + + /** + * @inheritDoc + */ + public function getOutput() + { + if ($this->output === null) { + $output = new NullOutput(); + $this->setOutput($output); + } + + return $this->output; + } + + /** + * @inheritDoc + * @return array + */ + public function getVersions() + { + $rows = $this->getVersionLog(); + + return array_keys($rows); + } + + /** + * Gets the schema table name. + * + * @return string + */ + public function getSchemaTableName() + { + return $this->schemaTableName; + } + + /** + * Sets the schema table name. + * + * @param string $schemaTableName Schema Table Name + * @return $this + */ + public function setSchemaTableName($schemaTableName) + { + $this->schemaTableName = $schemaTableName; + + return $this; + } + + /** + * Gets the data domain. + * + * @return array + */ + public function getDataDomain() + { + return $this->dataDomain; + } + + /** + * Sets the data domain. + * + * @param array $dataDomain Array for the data domain + * @return $this + */ + public function setDataDomain(array $dataDomain) + { + $this->dataDomain = []; + + // Iterate over data domain field definitions and perform initial and + // simple normalization. We make sure the definition as a base 'type' + // and it is compatible with the base Phinx types. + foreach ($dataDomain as $type => $options) { + if (!isset($options['type'])) { + throw new \InvalidArgumentException(sprintf( + 'You must specify a type for data domain type "%s".', + $type + )); + } + + // Replace type if it's the name of a Phinx constant + if (defined('static::' . $options['type'])) { + $options['type'] = constant('static::' . $options['type']); + } + + if (!in_array($options['type'], $this->getColumnTypes(), true)) { + throw new \InvalidArgumentException(sprintf( + 'An invalid column type "%s" was specified for data domain type "%s".', + $options['type'], + $type + )); + } + + $internal_type = $options['type']; + unset($options['type']); + + // Do a simple replacement for the 'length' / 'limit' option and + // detect hinting values for 'limit'. + if (isset($options['length'])) { + $options['limit'] = $options['length']; + unset($options['length']); + } + + if (isset($options['limit']) && !is_numeric($options['limit'])) { + if (!defined('static::' . $options['limit'])) { + throw new \InvalidArgumentException(sprintf( + 'An invalid limit value "%s" was specified for data domain type "%s".', + $options['limit'], + $type + )); + } + + $options['limit'] = constant('static::' . $options['limit']); + } + + // Save the data domain types in a more suitable format + $this->dataDomain[$type] = [ + 'type' => $internal_type, + 'options' => $options, + ]; + } + + return $this; + } + + /** + * @inheritdoc + */ + public function getColumnForType($columnName, $type, array $options) + { + $column = new Column(); + $column->setName($columnName); + + if (array_key_exists($type, $this->getDataDomain())) { + $column->setType($this->dataDomain[$type]['type']); + $column->setOptions($this->dataDomain[$type]['options']); + } else { + $column->setType($type); + } + + $column->setOptions($options); + + return $column; + } + + /** + * @inheritdoc + */ + public function hasSchemaTable() + { + return $this->hasTable($this->getSchemaTableName()); + } + + /** + * @inheritDoc + * @throws \InvalidArgumentException + * @return void + */ + public function createSchemaTable() + { + try { + $options = [ + 'id' => false, + 'primary_key' => 'version', + ]; + + $table = new Table($this->getSchemaTableName(), $options, $this); + $table->addColumn('version', 'biginteger') + ->addColumn('migration_name', 'string', ['limit' => 100, 'default' => null, 'null' => true]) + ->addColumn('start_time', 'timestamp', ['default' => null, 'null' => true]) + ->addColumn('end_time', 'timestamp', ['default' => null, 'null' => true]) + ->addColumn('breakpoint', 'boolean', ['default' => false]) + ->save(); + } catch (Exception $exception) { + throw new InvalidArgumentException( + 'There was a problem creating the schema table: ' . $exception->getMessage(), + (int)$exception->getCode(), + $exception + ); + } + } + + /** + * @inheritDoc + */ + public function getAdapterType() + { + return $this->getOption('adapter'); + } + + /** + * @inheritDoc + */ + public function isValidColumnType(Column $column) + { + return $column->getType() instanceof Literal || in_array($column->getType(), $this->getColumnTypes(), true); + } + + /** + * Determines if instead of executing queries a dump to standard output is needed + * + * @return bool + */ + public function isDryRunEnabled() + { + /** @var \Symfony\Component\Console\Input\InputInterface|null $input */ + $input = $this->getInput(); + + return $input && $input->hasOption('dry-run') ? (bool)$input->getOption('dry-run') : false; + } + + /** + * Adds user-created tables (e.g. not phinxlog) to a cached list + * + * @param string $tableName The name of the table + * @return void + */ + protected function addCreatedTable($tableName) + { + $tableName = $this->quoteTableName($tableName); + if (substr_compare($tableName, 'phinxlog', -strlen('phinxlog')) !== 0) { + $this->createdTables[] = $tableName; + } + } + + /** + * Updates the name of the cached table + * + * @param string $tableName Original name of the table + * @param string $newTableName New name of the table + * @return void + */ + protected function updateCreatedTableName($tableName, $newTableName) + { + $tableName = $this->quoteTableName($tableName); + $newTableName = $this->quoteTableName($newTableName); + $key = array_search($tableName, $this->createdTables, true); + if ($key !== false) { + $this->createdTables[$key] = $newTableName; + } + } + + /** + * Removes table from the cached created list + * + * @param string $tableName The name of the table + * @return void + */ + protected function removeCreatedTable($tableName) + { + $tableName = $this->quoteTableName($tableName); + $key = array_search($tableName, $this->createdTables, true); + if ($key !== false) { + unset($this->createdTables[$key]); + } + } + + /** + * Check if the table is in the cached list of created tables + * + * @param string $tableName The name of the table + * @return bool + */ + protected function hasCreatedTable($tableName) + { + $tableName = $this->quoteTableName($tableName); + + return in_array($tableName, $this->createdTables, true); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterFactory.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterFactory.php new file mode 100644 index 0000000..09327e3 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterFactory.php @@ -0,0 +1,170 @@ + + */ +class AdapterFactory +{ + /** + * @var \Phinx\Db\Adapter\AdapterFactory|null + */ + protected static $instance; + + /** + * Get the factory singleton instance. + * + * @return \Phinx\Db\Adapter\AdapterFactory + */ + public static function instance() + { + if (!static::$instance) { + static::$instance = new static(); + } + + return static::$instance; + } + + /** + * Class map of database adapters, indexed by PDO::ATTR_DRIVER_NAME. + * + * @var string[] + */ + protected $adapters = [ + 'mysql' => 'Phinx\Db\Adapter\MysqlAdapter', + 'pgsql' => 'Phinx\Db\Adapter\PostgresAdapter', + 'sqlite' => 'Phinx\Db\Adapter\SQLiteAdapter', + 'sqlsrv' => 'Phinx\Db\Adapter\SqlServerAdapter', + ]; + + /** + * Class map of adapters wrappers, indexed by name. + * + * @var string[] + */ + protected $wrappers = [ + 'prefix' => 'Phinx\Db\Adapter\TablePrefixAdapter', + 'proxy' => 'Phinx\Db\Adapter\ProxyAdapter', + 'timed' => 'Phinx\Db\Adapter\TimedOutputAdapter', + ]; + + /** + * Add or replace an adapter with a fully qualified class name. + * + * @param string $name Name + * @param string $class Class + * @throws \RuntimeException + * @return $this + */ + public function registerAdapter($name, $class) + { + if (!is_subclass_of($class, 'Phinx\Db\Adapter\AdapterInterface')) { + throw new RuntimeException(sprintf( + 'Adapter class "%s" must implement Phinx\\Db\\Adapter\\AdapterInterface', + $class + )); + } + $this->adapters[$name] = $class; + + return $this; + } + + /** + * Get an adapter class by name. + * + * @param string $name Name + * @throws \RuntimeException + * @return string + */ + protected function getClass($name) + { + if (empty($this->adapters[$name])) { + throw new RuntimeException(sprintf( + 'Adapter "%s" has not been registered', + $name + )); + } + + return $this->adapters[$name]; + } + + /** + * Get an adapter instance by name. + * + * @param string $name Name + * @param array $options Options + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function getAdapter($name, array $options) + { + $class = $this->getClass($name); + + return new $class($options); + } + + /** + * Add or replace a wrapper with a fully qualified class name. + * + * @param string $name Name + * @param string $class Class + * @throws \RuntimeException + * @return $this + */ + public function registerWrapper($name, $class) + { + if (!is_subclass_of($class, 'Phinx\Db\Adapter\WrapperInterface')) { + throw new RuntimeException(sprintf( + 'Wrapper class "%s" must be implement Phinx\\Db\\Adapter\\WrapperInterface', + $class + )); + } + $this->wrappers[$name] = $class; + + return $this; + } + + /** + * Get a wrapper class by name. + * + * @param string $name Name + * @throws \RuntimeException + * @return string + */ + protected function getWrapperClass($name) + { + if (empty($this->wrappers[$name])) { + throw new RuntimeException(sprintf( + 'Wrapper "%s" has not been registered', + $name + )); + } + + return $this->wrappers[$name]; + } + + /** + * Get a wrapper instance by name. + * + * @param string $name Name + * @param \Phinx\Db\Adapter\AdapterInterface $adapter Adapter + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function getWrapper($name, AdapterInterface $adapter) + { + $class = $this->getWrapperClass($name); + + return new $class($adapter); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterInterface.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterInterface.php new file mode 100644 index 0000000..d8cdc5b --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterInterface.php @@ -0,0 +1,502 @@ + + * @method \PDO getConnection() + */ +interface AdapterInterface +{ + public const PHINX_TYPE_STRING = 'string'; + public const PHINX_TYPE_CHAR = 'char'; + public const PHINX_TYPE_TEXT = 'text'; + public const PHINX_TYPE_INTEGER = 'integer'; + public const PHINX_TYPE_TINY_INTEGER = 'tinyinteger'; + public const PHINX_TYPE_SMALL_INTEGER = 'smallinteger'; + public const PHINX_TYPE_BIG_INTEGER = 'biginteger'; + public const PHINX_TYPE_BIT = 'bit'; + public const PHINX_TYPE_FLOAT = 'float'; + public const PHINX_TYPE_DECIMAL = 'decimal'; + public const PHINX_TYPE_DOUBLE = 'double'; + public const PHINX_TYPE_DATETIME = 'datetime'; + public const PHINX_TYPE_TIMESTAMP = 'timestamp'; + public const PHINX_TYPE_TIME = 'time'; + public const PHINX_TYPE_DATE = 'date'; + public const PHINX_TYPE_BINARY = 'binary'; + public const PHINX_TYPE_VARBINARY = 'varbinary'; + public const PHINX_TYPE_BINARYUUID = 'binaryuuid'; + public const PHINX_TYPE_BLOB = 'blob'; + public const PHINX_TYPE_TINYBLOB = 'tinyblob'; // Specific to Mysql. + public const PHINX_TYPE_MEDIUMBLOB = 'mediumblob'; // Specific to Mysql + public const PHINX_TYPE_LONGBLOB = 'longblob'; // Specific to Mysql + public const PHINX_TYPE_BOOLEAN = 'boolean'; + public const PHINX_TYPE_JSON = 'json'; + public const PHINX_TYPE_JSONB = 'jsonb'; + public const PHINX_TYPE_UUID = 'uuid'; + public const PHINX_TYPE_FILESTREAM = 'filestream'; + + // Geospatial database types + public const PHINX_TYPE_GEOMETRY = 'geometry'; + public const PHINX_TYPE_POINT = 'point'; + public const PHINX_TYPE_LINESTRING = 'linestring'; + public const PHINX_TYPE_POLYGON = 'polygon'; + + // only for mysql so far + public const PHINX_TYPE_MEDIUM_INTEGER = 'mediuminteger'; + public const PHINX_TYPE_ENUM = 'enum'; + public const PHINX_TYPE_SET = 'set'; + public const PHINX_TYPE_YEAR = 'year'; + + // only for postgresql so far + public const PHINX_TYPE_CIDR = 'cidr'; + public const PHINX_TYPE_INET = 'inet'; + public const PHINX_TYPE_MACADDR = 'macaddr'; + public const PHINX_TYPE_INTERVAL = 'interval'; + + /** + * Get all migrated version numbers. + * + * @return array + */ + public function getVersions(); + + /** + * Get all migration log entries, indexed by version creation time and sorted ascendingly by the configuration's + * version order option + * + * @return array + */ + public function getVersionLog(); + + /** + * Set adapter configuration options. + * + * @param array $options Options + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function setOptions(array $options); + + /** + * Get all adapter options. + * + * @return array + */ + public function getOptions(); + + /** + * Check if an option has been set. + * + * @param string $name Name + * @return bool + */ + public function hasOption($name); + + /** + * Get a single adapter option, or null if the option does not exist. + * + * @param string $name Name + * @return mixed + */ + public function getOption($name); + + /** + * Sets the console input. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function setInput(InputInterface $input); + + /** + * Gets the console input. + * + * @return \Symfony\Component\Console\Input\InputInterface + */ + public function getInput(); + + /** + * Sets the console output. + * + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function setOutput(OutputInterface $output); + + /** + * Gets the console output. + * + * @return \Symfony\Component\Console\Output\OutputInterface + */ + public function getOutput(); + + /** + * Returns a new Phinx\Db\Table\Column using the existent data domain. + * + * @param string $columnName The desired column name + * @param string $type The type for the column. Can be a data domain type. + * @param array $options Options array + * @return \Phinx\Db\Table\Column + */ + public function getColumnForType($columnName, $type, array $options); + + /** + * Records a migration being run. + * + * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param string $direction Direction + * @param string $startTime Start Time + * @param string $endTime End Time + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime); + + /** + * Toggle a migration breakpoint. + * + * @param \Phinx\Migration\MigrationInterface $migration Migration + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function toggleBreakpoint(MigrationInterface $migration); + + /** + * Reset all migration breakpoints. + * + * @return int The number of breakpoints reset + */ + public function resetAllBreakpoints(); + + /** + * Set a migration breakpoint. + * + * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint set + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function setBreakpoint(MigrationInterface $migration); + + /** + * Unset a migration breakpoint. + * + * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint unset + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function unsetBreakpoint(MigrationInterface $migration); + + /** + * Does the schema table exist? + * + * @deprecated use hasTable instead. + * @return bool + */ + public function hasSchemaTable(); + + /** + * Creates the schema table. + * + * @return void + */ + public function createSchemaTable(); + + /** + * Returns the adapter type. + * + * @return string + */ + public function getAdapterType(); + + /** + * Initializes the database connection. + * + * @throws \RuntimeException When the requested database driver is not installed. + * @return void + */ + public function connect(); + + /** + * Closes the database connection. + * + * @return void + */ + public function disconnect(); + + /** + * Does the adapter support transactions? + * + * @return bool + */ + public function hasTransactions(); + + /** + * Begin a transaction. + * + * @return void + */ + public function beginTransaction(); + + /** + * Commit a transaction. + * + * @return void + */ + public function commitTransaction(); + + /** + * Rollback a transaction. + * + * @return void + */ + public function rollbackTransaction(); + + /** + * Executes a SQL statement and returns the number of affected rows. + * + * @param string $sql SQL + * @return int + */ + public function execute($sql); + + /** + * Executes a list of migration actions for the given table + * + * @param \Phinx\Db\Table\Table $table The table to execute the actions for + * @param \Phinx\Db\Action\Action[] $actions The table to execute the actions for + * @return void + */ + public function executeActions(Table $table, array $actions); + + /** + * Returns a new Query object + * + * @return \Cake\Database\Query + */ + public function getQueryBuilder(); + + /** + * Executes a SQL statement. + * + * The return type depends on the underlying adapter being used. + * + * @param string $sql SQL + * @return mixed + */ + public function query($sql); + + /** + * Executes a query and returns only one row as an array. + * + * @param string $sql SQL + * @return array|false + */ + public function fetchRow($sql); + + /** + * Executes a query and returns an array of rows. + * + * @param string $sql SQL + * @return array + */ + public function fetchAll($sql); + + /** + * Inserts data into a table. + * + * @param \Phinx\Db\Table\Table $table Table where to insert data + * @param array $row Row + * @return void + */ + public function insert(Table $table, $row); + + /** + * Inserts data into a table in a bulk. + * + * @param \Phinx\Db\Table\Table $table Table where to insert data + * @param array $rows Rows + * @return void + */ + public function bulkinsert(Table $table, $rows); + + /** + * Quotes a table name for use in a query. + * + * @param string $tableName Table name + * @return string + */ + public function quoteTableName($tableName); + + /** + * Quotes a column name for use in a query. + * + * @param string $columnName Table name + * @return string + */ + public function quoteColumnName($columnName); + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table name + * @return bool + */ + public function hasTable($tableName); + + /** + * Creates the specified database table. + * + * @param \Phinx\Db\Table\Table $table Table + * @param \Phinx\Db\Table\Column[] $columns List of columns in the table + * @param \Phinx\Db\Table\Index[] $indexes List of indexes for the table + * @return void + */ + public function createTable(Table $table, array $columns = [], array $indexes = []); + + /** + * Truncates the specified table + * + * @param string $tableName Table name + * @return void + */ + public function truncateTable($tableName); + + /** + * Returns table columns + * + * @param string $tableName Table name + * @return \Phinx\Db\Table\Column[] + */ + public function getColumns($tableName); + + /** + * Checks to see if a column exists. + * + * @param string $tableName Table name + * @param string $columnName Column name + * @return bool + */ + public function hasColumn($tableName, $columnName); + + /** + * Checks to see if an index exists. + * + * @param string $tableName Table name + * @param string|string[] $columns Column(s) + * @return bool + */ + public function hasIndex($tableName, $columns); + + /** + * Checks to see if an index specified by name exists. + * + * @param string $tableName Table name + * @param string $indexName Index name + * @return bool + */ + public function hasIndexByName($tableName, $indexName); + + /** + * Checks to see if the specified primary key exists. + * + * @param string $tableName Table name + * @param string|string[] $columns Column(s) + * @param string|null $constraint Constraint name + * @return bool + */ + public function hasPrimaryKey($tableName, $columns, $constraint = null); + + /** + * Checks to see if a foreign key exists. + * + * @param string $tableName Table name + * @param string|string[] $columns Column(s) + * @param string|null $constraint Constraint name + * @return bool + */ + public function hasForeignKey($tableName, $columns, $constraint = null); + + /** + * Returns an array of the supported Phinx column types. + * + * @return string[] + */ + public function getColumnTypes(); + + /** + * Checks that the given column is of a supported type. + * + * @param \Phinx\Db\Table\Column $column Column + * @return bool + */ + public function isValidColumnType(Column $column); + + /** + * Converts the Phinx logical type to the adapter's SQL type. + * + * @param string $type Type + * @param int|null $limit Limit + * @return array + */ + public function getSqlType($type, $limit = null); + + /** + * Creates a new database. + * + * @param string $name Database Name + * @param array $options Options + * @return void + */ + public function createDatabase($name, $options = []); + + /** + * Checks to see if a database exists. + * + * @param string $name Database Name + * @return bool + */ + public function hasDatabase($name); + + /** + * Drops the specified database. + * + * @param string $name Database Name + * @return void + */ + public function dropDatabase($name); + + /** + * Creates the specified schema or throws an exception + * if there is no support for it. + * + * @param string $schemaName Schema Name + * @return void + */ + public function createSchema($schemaName = 'public'); + + /** + * Drops the specified schema table or throws an exception + * if there is no support for it. + * + * @param string $schemaName Schema name + * @return void + */ + public function dropSchema($schemaName); + + /** + * Cast a value to a boolean appropriate for the adapter. + * + * @param mixed $value The value to be cast + * @return mixed + */ + public function castToBool($value); +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterWrapper.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterWrapper.php new file mode 100644 index 0000000..b94b5e7 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/AdapterWrapper.php @@ -0,0 +1,494 @@ + + */ +abstract class AdapterWrapper implements AdapterInterface, WrapperInterface +{ + /** + * @var \Phinx\Db\Adapter\AdapterInterface + */ + protected $adapter; + + /** + * @inheritDoc + */ + public function __construct(AdapterInterface $adapter) + { + $this->setAdapter($adapter); + } + + /** + * @inheritDoc + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * @inheritDoc + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * @inheritDoc + */ + public function setOptions(array $options) + { + $this->adapter->setOptions($options); + + return $this; + } + + /** + * @inheritDoc + */ + public function getOptions() + { + return $this->adapter->getOptions(); + } + + /** + * @inheritDoc + */ + public function hasOption($name) + { + return $this->adapter->hasOption($name); + } + + /** + * @inheritDoc + */ + public function getOption($name) + { + return $this->adapter->getOption($name); + } + + /** + * @inheritDoc + */ + public function setInput(InputInterface $input) + { + $this->adapter->setInput($input); + + return $this; + } + + /** + * @inheritDoc + */ + public function getInput() + { + return $this->adapter->getInput(); + } + + /** + * @inheritDoc + */ + public function setOutput(OutputInterface $output) + { + $this->adapter->setOutput($output); + + return $this; + } + + /** + * @inheritDoc + */ + public function getOutput() + { + return $this->adapter->getOutput(); + } + + /** + * @inheritDoc + */ + public function getColumnForType($columnName, $type, array $options) + { + return $this->adapter->getColumnForType($columnName, $type, $options); + } + + /** + * @inheritDoc + */ + public function connect() + { + $this->getAdapter()->connect(); + } + + /** + * @inheritDoc + */ + public function disconnect() + { + $this->getAdapter()->disconnect(); + } + + /** + * @inheritDoc + */ + public function execute($sql) + { + return $this->getAdapter()->execute($sql); + } + + /** + * @inheritDoc + */ + public function query($sql) + { + return $this->getAdapter()->query($sql); + } + + /** + * @inheritDoc + */ + public function insert(Table $table, $row) + { + $this->getAdapter()->insert($table, $row); + } + + /** + * @inheritDoc + */ + public function bulkinsert(Table $table, $rows) + { + $this->getAdapter()->bulkinsert($table, $rows); + } + + /** + * @inheritDoc + */ + public function fetchRow($sql) + { + return $this->getAdapter()->fetchRow($sql); + } + + /** + * @inheritDoc + */ + public function fetchAll($sql) + { + return $this->getAdapter()->fetchAll($sql); + } + + /** + * @inheritDoc + */ + public function getVersions() + { + return $this->getAdapter()->getVersions(); + } + + /** + * @inheritDoc + */ + public function getVersionLog() + { + return $this->getAdapter()->getVersionLog(); + } + + /** + * @inheritDoc + */ + public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime) + { + $this->getAdapter()->migrated($migration, $direction, $startTime, $endTime); + + return $this; + } + + /** + * @inheritDoc + */ + public function toggleBreakpoint(MigrationInterface $migration) + { + $this->getAdapter()->toggleBreakpoint($migration); + + return $this; + } + + /** + * @inheritDoc + */ + public function resetAllBreakpoints() + { + return $this->getAdapter()->resetAllBreakpoints(); + } + + /** + * @inheritDoc + */ + public function setBreakpoint(MigrationInterface $migration) + { + $this->getAdapter()->setBreakpoint($migration); + + return $this; + } + + /** + * @inheritDoc + */ + public function unsetBreakpoint(MigrationInterface $migration) + { + $this->getAdapter()->unsetBreakpoint($migration); + + return $this; + } + + /** + * @inheritDoc + */ + public function hasSchemaTable() + { + return $this->getAdapter()->hasSchemaTable(); + } + + /** + * @inheritDoc + */ + public function createSchemaTable() + { + $this->getAdapter()->createSchemaTable(); + } + + /** + * @inheritDoc + */ + public function getColumnTypes() + { + return $this->getAdapter()->getColumnTypes(); + } + + /** + * @inheritDoc + */ + public function isValidColumnType(Column $column) + { + return $this->getAdapter()->isValidColumnType($column); + } + + /** + * @inheritDoc + */ + public function hasTransactions() + { + return $this->getAdapter()->hasTransactions(); + } + + /** + * @inheritDoc + */ + public function beginTransaction() + { + $this->getAdapter()->beginTransaction(); + } + + /** + * @inheritDoc + */ + public function commitTransaction() + { + $this->getAdapter()->commitTransaction(); + } + + /** + * @inheritDoc + */ + public function rollbackTransaction() + { + $this->getAdapter()->rollbackTransaction(); + } + + /** + * @inheritDoc + */ + public function quoteTableName($tableName) + { + return $this->getAdapter()->quoteTableName($tableName); + } + + /** + * @inheritDoc + */ + public function quoteColumnName($columnName) + { + return $this->getAdapter()->quoteColumnName($columnName); + } + + /** + * @inheritDoc + */ + public function hasTable($tableName) + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []) + { + $this->getAdapter()->createTable($table, $columns, $indexes); + } + + /** + * @inheritDoc + */ + public function getColumns($tableName) + { + return $this->getAdapter()->getColumns($tableName); + } + + /** + * @inheritDoc + */ + public function hasColumn($tableName, $columnName) + { + return $this->getAdapter()->hasColumn($tableName, $columnName); + } + + /** + * @inheritDoc + */ + public function hasIndex($tableName, $columns) + { + return $this->getAdapter()->hasIndex($tableName, $columns); + } + + /** + * @inheritDoc + */ + public function hasIndexByName($tableName, $indexName) + { + return $this->getAdapter()->hasIndexByName($tableName, $indexName); + } + + /** + * @inheritDoc + */ + public function hasPrimaryKey($tableName, $columns, $constraint = null) + { + return $this->getAdapter()->hasPrimaryKey($tableName, $columns, $constraint); + } + + /** + * @inheritDoc + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + return $this->getAdapter()->hasForeignKey($tableName, $columns, $constraint); + } + + /** + * @inheritDoc + */ + public function getSqlType($type, $limit = null) + { + return $this->getAdapter()->getSqlType($type, $limit); + } + + /** + * @inheritDoc + */ + public function createDatabase($name, $options = []) + { + $this->getAdapter()->createDatabase($name, $options); + } + + /** + * @inheritDoc + */ + public function hasDatabase($name) + { + return $this->getAdapter()->hasDatabase($name); + } + + /** + * @inheritDoc + */ + public function dropDatabase($name) + { + $this->getAdapter()->dropDatabase($name); + } + + /** + * @inheritDoc + */ + public function createSchema($schemaName = 'public') + { + $this->getAdapter()->createSchema($schemaName); + } + + /** + * @inheritDoc + */ + public function dropSchema($schemaName) + { + $this->getAdapter()->dropSchema($schemaName); + } + + /** + * @inheritDoc + */ + public function truncateTable($tableName) + { + $this->getAdapter()->truncateTable($tableName); + } + + /** + * @inheritDoc + */ + public function castToBool($value) + { + return $this->getAdapter()->castToBool($value); + } + + /** + * @return \PDO + */ + public function getConnection() + { + return $this->getAdapter()->getConnection(); + } + + /** + * @inheritDoc + */ + public function executeActions(Table $table, array $actions) + { + $this->getAdapter()->executeActions($table, $actions); + } + + /** + * @inheritDoc + */ + public function getQueryBuilder() + { + return $this->getAdapter()->getQueryBuilder(); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/DirectActionInterface.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/DirectActionInterface.php new file mode 100644 index 0000000..358c4c1 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/DirectActionInterface.php @@ -0,0 +1,139 @@ + + */ +class MysqlAdapter extends PdoAdapter +{ + /** + * @var string[] + */ + protected static $specificColumnTypes = [ + self::PHINX_TYPE_ENUM, + self::PHINX_TYPE_SET, + self::PHINX_TYPE_YEAR, + self::PHINX_TYPE_JSON, + self::PHINX_TYPE_BINARYUUID, + self::PHINX_TYPE_TINYBLOB, + self::PHINX_TYPE_MEDIUMBLOB, + self::PHINX_TYPE_LONGBLOB, + self::PHINX_TYPE_MEDIUM_INTEGER, + ]; + + /** + * @var bool[] + */ + protected $signedColumnTypes = [ + self::PHINX_TYPE_INTEGER => true, + self::PHINX_TYPE_TINY_INTEGER => true, + self::PHINX_TYPE_SMALL_INTEGER => true, + self::PHINX_TYPE_MEDIUM_INTEGER => true, + self::PHINX_TYPE_BIG_INTEGER => true, + self::PHINX_TYPE_FLOAT => true, + self::PHINX_TYPE_DECIMAL => true, + self::PHINX_TYPE_DOUBLE => true, + self::PHINX_TYPE_BOOLEAN => true, + ]; + + public const TEXT_TINY = 255; + public const TEXT_SMALL = 255; /* deprecated, alias of TEXT_TINY */ + public const TEXT_REGULAR = 65535; + public const TEXT_MEDIUM = 16777215; + public const TEXT_LONG = 4294967295; + + // According to https://dev.mysql.com/doc/refman/5.0/en/blob.html BLOB sizes are the same as TEXT + public const BLOB_TINY = 255; + public const BLOB_SMALL = 255; /* deprecated, alias of BLOB_TINY */ + public const BLOB_REGULAR = 65535; + public const BLOB_MEDIUM = 16777215; + public const BLOB_LONG = 4294967295; + + public const INT_TINY = 255; + public const INT_SMALL = 65535; + public const INT_MEDIUM = 16777215; + public const INT_REGULAR = 4294967295; + public const INT_BIG = 18446744073709551615; + + public const BIT = 64; + + public const TYPE_YEAR = 'year'; + + public const FIRST = 'FIRST'; + + /** + * {@inheritDoc} + * + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return void + */ + public function connect() + { + if ($this->connection === null) { + if (!class_exists('PDO') || !in_array('mysql', PDO::getAvailableDrivers(), true)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('You need to enable the PDO_Mysql extension for Phinx to run properly.'); + // @codeCoverageIgnoreEnd + } + + $options = $this->getOptions(); + + $dsn = 'mysql:'; + + if (!empty($options['unix_socket'])) { + // use socket connection + $dsn .= 'unix_socket=' . $options['unix_socket']; + } else { + // use network connection + $dsn .= 'host=' . $options['host']; + if (!empty($options['port'])) { + $dsn .= ';port=' . $options['port']; + } + } + + $dsn .= ';dbname=' . $options['name']; + + // charset support + if (!empty($options['charset'])) { + $dsn .= ';charset=' . $options['charset']; + } + + $driverOptions = []; + + // use custom data fetch mode + if (!empty($options['fetch_mode'])) { + $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode'])); + } + + // support arbitrary \PDO::MYSQL_ATTR_* driver options and pass them to PDO + // http://php.net/manual/en/ref.pdo-mysql.php#pdo-mysql.constants + foreach ($options as $key => $option) { + if (strpos($key, 'mysql_attr_') === 0) { + $pdoConstant = '\PDO::' . strtoupper($key); + if (!defined($pdoConstant)) { + throw new \UnexpectedValueException('Invalid PDO attribute: ' . $key . ' (' . $pdoConstant . ')'); + } + $driverOptions[constant($pdoConstant)] = $option; + } + } + + $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions); + + $this->setConnection($db); + } + } + + /** + * @inheritDoc + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * @inheritDoc + */ + public function hasTransactions() + { + return true; + } + + /** + * @inheritDoc + */ + public function beginTransaction() + { + $this->execute('START TRANSACTION'); + } + + /** + * @inheritDoc + */ + public function commitTransaction() + { + $this->execute('COMMIT'); + } + + /** + * @inheritDoc + */ + public function rollbackTransaction() + { + $this->execute('ROLLBACK'); + } + + /** + * @inheritDoc + */ + public function quoteTableName($tableName) + { + return str_replace('.', '`.`', $this->quoteColumnName($tableName)); + } + + /** + * @inheritDoc + */ + public function quoteColumnName($columnName) + { + return '`' . str_replace('`', '``', $columnName) . '`'; + } + + /** + * @inheritDoc + */ + public function hasTable($tableName) + { + if ($this->hasCreatedTable($tableName)) { + return true; + } + + if (strpos($tableName, '.') !== false) { + [$schema, $table] = explode('.', $tableName); + $exists = $this->hasTableWithSchema($schema, $table); + // Only break here on success, because it is possible for table names to contain a dot. + if ($exists) { + return true; + } + } + + $options = $this->getOptions(); + + return $this->hasTableWithSchema($options['name'], $tableName); + } + + /** + * @param string $schema The table schema + * @param string $tableName The table name + * @return bool + */ + protected function hasTableWithSchema($schema, $tableName) + { + $result = $this->fetchRow(sprintf( + "SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'", + $schema, + $tableName + )); + + return !empty($result); + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []) + { + // This method is based on the MySQL docs here: http://dev.mysql.com/doc/refman/5.1/en/create-index.html + $defaultOptions = [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci', + ]; + + $options = array_merge( + $defaultOptions, + array_intersect_key($this->getOptions(), $defaultOptions), + $table->getOptions() + ); + + // Add the default primary key + if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + $options['id'] = 'id'; + } + + if (isset($options['id']) && is_string($options['id'])) { + // Handle id => "field_name" to support AUTO_INCREMENT + $column = new Column(); + $column->setName($options['id']) + ->setType('integer') + ->setSigned($options['signed'] ?? true) + ->setIdentity(true); + + if (isset($options['limit'])) { + $column->setLimit($options['limit']); + } + + array_unshift($columns, $column); + if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) { + throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key'); + } + $options['primary_key'] = $options['id']; + } + + // open: process table options like collation etc + + // process table engine (default to InnoDB) + $optionsStr = 'ENGINE = InnoDB'; + if (isset($options['engine'])) { + $optionsStr = sprintf('ENGINE = %s', $options['engine']); + } + + // process table collation + if (isset($options['collation'])) { + $charset = explode('_', $options['collation']); + $optionsStr .= sprintf(' CHARACTER SET %s', $charset[0]); + $optionsStr .= sprintf(' COLLATE %s', $options['collation']); + } + + // set the table comment + if (isset($options['comment'])) { + $optionsStr .= sprintf(' COMMENT=%s ', $this->getConnection()->quote($options['comment'])); + } + + // set the table row format + if (isset($options['row_format'])) { + $optionsStr .= sprintf(' ROW_FORMAT=%s ', $options['row_format']); + } + + $sql = 'CREATE TABLE '; + $sql .= $this->quoteTableName($table->getName()) . ' ('; + foreach ($columns as $column) { + $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', '; + } + + // set the primary key(s) + if (isset($options['primary_key'])) { + $sql = rtrim($sql); + $sql .= ' PRIMARY KEY ('; + if (is_string($options['primary_key'])) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($options['primary_key']); + } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') + $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key'])); + } + $sql .= ')'; + } else { + $sql = substr(rtrim($sql), 0, -1); // no primary keys + } + + // set the indexes + foreach ($indexes as $index) { + $sql .= ', ' . $this->getIndexSqlDefinition($index); + } + + $sql .= ') ' . $optionsStr; + $sql = rtrim($sql); + + // execute the sql + $this->execute($sql); + + $this->addCreatedTable($table->getName()); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getChangePrimaryKeyInstructions(Table $table, $newColumns) + { + $instructions = new AlterInstructions(); + + // Drop the existing primary key + $primaryKey = $this->getPrimaryKey($table->getName()); + if (!empty($primaryKey['columns'])) { + $instructions->addAlter('DROP PRIMARY KEY'); + } + + // Add the primary key(s) + if (!empty($newColumns)) { + $sql = 'ADD PRIMARY KEY ('; + if (is_string($newColumns)) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($newColumns); + } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id') + $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns)); + } else { + throw new InvalidArgumentException(sprintf( + 'Invalid value for primary key: %s', + json_encode($newColumns) + )); + } + $sql .= ')'; + $instructions->addAlter($sql); + } + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getChangeCommentInstructions(Table $table, $newComment) + { + $instructions = new AlterInstructions(); + + // passing 'null' is to remove table comment + $newComment = $newComment ?? ''; + $sql = sprintf(' COMMENT=%s ', $this->getConnection()->quote($newComment)); + $instructions->addAlter($sql); + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getRenameTableInstructions($tableName, $newTableName) + { + $this->updateCreatedTableName($tableName, $newTableName); + $sql = sprintf( + 'RENAME TABLE %s TO %s', + $this->quoteTableName($tableName), + $this->quoteTableName($newTableName) + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + protected function getDropTableInstructions($tableName) + { + $this->removeCreatedTable($tableName); + $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName)); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + public function truncateTable($tableName) + { + $sql = sprintf( + 'TRUNCATE TABLE %s', + $this->quoteTableName($tableName) + ); + + $this->execute($sql); + } + + /** + * @inheritDoc + */ + public function getColumns($tableName) + { + $columns = []; + $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); + foreach ($rows as $columnInfo) { + $phinxType = $this->getPhinxType($columnInfo['Type']); + + $column = new Column(); + $column->setName($columnInfo['Field']) + ->setNull($columnInfo['Null'] !== 'NO') + ->setDefault($columnInfo['Default']) + ->setType($phinxType['name']) + ->setSigned(strpos($columnInfo['Type'], 'unsigned') === false) + ->setLimit($phinxType['limit']) + ->setScale($phinxType['scale']); + + if ($columnInfo['Extra'] === 'auto_increment') { + $column->setIdentity(true); + } + + if (isset($phinxType['values'])) { + $column->setValues($phinxType['values']); + } + + $columns[] = $column; + } + + return $columns; + } + + /** + * @inheritDoc + */ + public function hasColumn($tableName, $columnName) + { + $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); + foreach ($rows as $column) { + if (strcasecmp($column['Field'], $columnName) === 0) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function getAddColumnInstructions(Table $table, Column $column) + { + $alter = sprintf( + 'ADD %s %s', + $this->quoteColumnName($column->getName()), + $this->getColumnSqlDefinition($column) + ); + + $alter .= $this->afterClause($column); + + return new AlterInstructions([$alter]); + } + + /** + * Exposes the MySQL syntax to arrange a column `FIRST`. + * + * @param \Phinx\Db\Table\Column $column The column being altered. + * @return string The appropriate SQL fragment. + */ + protected function afterClause(Column $column) + { + $after = $column->getAfter(); + if (empty($after)) { + return ''; + } + + if ($after === self::FIRST) { + return ' FIRST'; + } + + return ' AFTER ' . $this->quoteColumnName($after); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName) + { + $rows = $this->fetchAll(sprintf('SHOW FULL COLUMNS FROM %s', $this->quoteTableName($tableName))); + + foreach ($rows as $row) { + if (strcasecmp($row['Field'], $columnName) === 0) { + $null = $row['Null'] === 'NO' ? 'NOT NULL' : 'NULL'; + $comment = isset($row['Comment']) ? ' COMMENT ' . '\'' . addslashes($row['Comment']) . '\'' : ''; + $extra = ' ' . strtoupper($row['Extra']); + if (($row['Default'] !== null)) { + $extra .= $this->getDefaultValueDefinition($row['Default']); + } + $definition = $row['Type'] . ' ' . $null . $extra . $comment; + + $alter = sprintf( + 'CHANGE COLUMN %s %s %s', + $this->quoteColumnName($columnName), + $this->quoteColumnName($newColumnName), + $definition + ); + + return new AlterInstructions([$alter]); + } + } + + throw new InvalidArgumentException(sprintf( + "The specified column doesn't exist: " . + $columnName + )); + } + + /** + * @inheritDoc + */ + protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn) + { + $alter = sprintf( + 'CHANGE %s %s %s%s', + $this->quoteColumnName($columnName), + $this->quoteColumnName($newColumn->getName()), + $this->getColumnSqlDefinition($newColumn), + $this->afterClause($newColumn) + ); + + return new AlterInstructions([$alter]); + } + + /** + * @inheritDoc + */ + protected function getDropColumnInstructions($tableName, $columnName) + { + $alter = sprintf('DROP COLUMN %s', $this->quoteColumnName($columnName)); + + return new AlterInstructions([$alter]); + } + + /** + * Get an array of indexes from a particular table. + * + * @param string $tableName Table name + * @return array + */ + protected function getIndexes($tableName) + { + $indexes = []; + $rows = $this->fetchAll(sprintf('SHOW INDEXES FROM %s', $this->quoteTableName($tableName))); + foreach ($rows as $row) { + if (!isset($indexes[$row['Key_name']])) { + $indexes[$row['Key_name']] = ['columns' => []]; + } + $indexes[$row['Key_name']]['columns'][] = strtolower($row['Column_name']); + } + + return $indexes; + } + + /** + * @inheritDoc + */ + public function hasIndex($tableName, $columns) + { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + + $columns = array_map('strtolower', $columns); + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $index) { + if ($columns == $index['columns']) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function hasIndexByName($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function getAddIndexInstructions(Table $table, Index $index) + { + $instructions = new AlterInstructions(); + + if ($index->getType() === Index::FULLTEXT) { + // Must be executed separately + // SQLSTATE[HY000]: General error: 1795 InnoDB presently supports one FULLTEXT index creation at a time + $alter = sprintf( + 'ALTER TABLE %s ADD %s', + $this->quoteTableName($table->getName()), + $this->getIndexSqlDefinition($index) + ); + + $instructions->addPostStep($alter); + } else { + $alter = sprintf( + 'ADD %s', + $this->getIndexSqlDefinition($index) + ); + + $instructions->addAlter($alter); + } + + return $instructions; + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getDropIndexByColumnsInstructions($tableName, $columns) + { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + + $indexes = $this->getIndexes($tableName); + $columns = array_map('strtolower', $columns); + + foreach ($indexes as $indexName => $index) { + if ($columns == $index['columns']) { + return new AlterInstructions([sprintf( + 'DROP INDEX %s', + $this->quoteColumnName($indexName) + )]); + } + } + + throw new InvalidArgumentException(sprintf( + "The specified index on columns '%s' does not exist", + implode(',', $columns) + )); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getDropIndexByNameInstructions($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + return new AlterInstructions([sprintf( + 'DROP INDEX %s', + $this->quoteColumnName($indexName) + )]); + } + } + + throw new InvalidArgumentException(sprintf( + "The specified index name '%s' does not exist", + $indexName + )); + } + + /** + * @inheritDoc + */ + public function hasPrimaryKey($tableName, $columns, $constraint = null) + { + $primaryKey = $this->getPrimaryKey($tableName); + + if (empty($primaryKey['constraint'])) { + return false; + } + + if ($constraint) { + return $primaryKey['constraint'] === $constraint; + } else { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + $missingColumns = array_diff($columns, $primaryKey['columns']); + + return empty($missingColumns); + } + } + + /** + * Get the primary key from a particular table. + * + * @param string $tableName Table name + * @return array + */ + public function getPrimaryKey($tableName) + { + $options = $this->getOptions(); + $rows = $this->fetchAll(sprintf( + "SELECT + k.CONSTRAINT_NAME, + k.COLUMN_NAME + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS t + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k + USING(CONSTRAINT_NAME,TABLE_SCHEMA,TABLE_NAME) + WHERE t.CONSTRAINT_TYPE='PRIMARY KEY' + AND t.TABLE_SCHEMA='%s' + AND t.TABLE_NAME='%s'", + $options['name'], + $tableName + )); + + $primaryKey = [ + 'columns' => [], + ]; + foreach ($rows as $row) { + $primaryKey['constraint'] = $row['CONSTRAINT_NAME']; + $primaryKey['columns'][] = $row['COLUMN_NAME']; + } + + return $primaryKey; + } + + /** + * @inheritDoc + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + $foreignKeys = $this->getForeignKeys($tableName); + if ($constraint) { + if (isset($foreignKeys[$constraint])) { + return !empty($foreignKeys[$constraint]); + } + + return false; + } + + foreach ($foreignKeys as $key) { + if ($columns == $key['columns']) { + return true; + } + } + + return false; + } + + /** + * Get an array of foreign keys from a particular table. + * + * @param string $tableName Table name + * @return array + */ + protected function getForeignKeys($tableName) + { + if (strpos($tableName, '.') !== false) { + [$schema, $tableName] = explode('.', $tableName); + } + + $foreignKeys = []; + $rows = $this->fetchAll(sprintf( + "SELECT + CONSTRAINT_NAME, + CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS TABLE_NAME, + COLUMN_NAME, + CONCAT(REFERENCED_TABLE_SCHEMA, '.', REFERENCED_TABLE_NAME) AS REFERENCED_TABLE_NAME, + REFERENCED_COLUMN_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE REFERENCED_TABLE_NAME IS NOT NULL + AND TABLE_SCHEMA = %s + AND TABLE_NAME = '%s' + ORDER BY POSITION_IN_UNIQUE_CONSTRAINT", + empty($schema) ? 'DATABASE()' : "'$schema'", + $tableName + )); + foreach ($rows as $row) { + $foreignKeys[$row['CONSTRAINT_NAME']]['table'] = $row['TABLE_NAME']; + $foreignKeys[$row['CONSTRAINT_NAME']]['columns'][] = $row['COLUMN_NAME']; + $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_table'] = $row['REFERENCED_TABLE_NAME']; + $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME']; + } + + return $foreignKeys; + } + + /** + * @inheritDoc + */ + protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey) + { + $alter = sprintf( + 'ADD %s', + $this->getForeignKeySqlDefinition($foreignKey) + ); + + return new AlterInstructions([$alter]); + } + + /** + * @inheritDoc + */ + protected function getDropForeignKeyInstructions($tableName, $constraint) + { + $alter = sprintf( + 'DROP FOREIGN KEY %s', + $constraint + ); + + return new AlterInstructions([$alter]); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getDropForeignKeyByColumnsInstructions($tableName, $columns) + { + $instructions = new AlterInstructions(); + + foreach ($columns as $column) { + $rows = $this->fetchAll(sprintf( + "SELECT + CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE REFERENCED_TABLE_SCHEMA = DATABASE() + AND REFERENCED_TABLE_NAME IS NOT NULL + AND TABLE_NAME = '%s' + AND COLUMN_NAME = '%s' + ORDER BY POSITION_IN_UNIQUE_CONSTRAINT", + $tableName, + $column + )); + + foreach ($rows as $row) { + $instructions->merge($this->getDropForeignKeyInstructions($tableName, $row['CONSTRAINT_NAME'])); + } + } + + if (empty($instructions->getAlterParts())) { + throw new InvalidArgumentException(sprintf( + "Not foreign key on columns '%s' exist", + implode(',', $columns) + )); + } + + return $instructions; + } + + /** + * {@inheritDoc} + * + * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException + */ + public function getSqlType($type, $limit = null) + { + switch ($type) { + case static::PHINX_TYPE_FLOAT: + case static::PHINX_TYPE_DOUBLE: + case static::PHINX_TYPE_DECIMAL: + case static::PHINX_TYPE_DATE: + case static::PHINX_TYPE_ENUM: + case static::PHINX_TYPE_SET: + case static::PHINX_TYPE_JSON: + // Geospatial database types + case static::PHINX_TYPE_GEOMETRY: + case static::PHINX_TYPE_POINT: + case static::PHINX_TYPE_LINESTRING: + case static::PHINX_TYPE_POLYGON: + return ['name' => $type]; + case static::PHINX_TYPE_DATETIME: + case static::PHINX_TYPE_TIMESTAMP: + case static::PHINX_TYPE_TIME: + return ['name' => $type, 'limit' => $limit]; + case static::PHINX_TYPE_STRING: + return ['name' => 'varchar', 'limit' => $limit ?: 255]; + case static::PHINX_TYPE_CHAR: + return ['name' => 'char', 'limit' => $limit ?: 255]; + case static::PHINX_TYPE_TEXT: + if ($limit) { + $sizes = [ + // Order matters! Size must always be tested from longest to shortest! + 'longtext' => static::TEXT_LONG, + 'mediumtext' => static::TEXT_MEDIUM, + 'text' => static::TEXT_REGULAR, + 'tinytext' => static::TEXT_SMALL, + ]; + foreach ($sizes as $name => $length) { + if ($limit >= $length) { + return ['name' => $name]; + } + } + } + + return ['name' => 'text']; + case static::PHINX_TYPE_BINARY: + if ($limit === null) { + $limit = 255; + } + + if ($limit > 255) { + return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit); + } + + return ['name' => 'binary', 'limit' => $limit]; + case static::PHINX_TYPE_BINARYUUID: + return ['name' => 'binary', 'limit' => 16]; + case static::PHINX_TYPE_VARBINARY: + if ($limit === null) { + $limit = 255; + } + + if ($limit > 255) { + return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit); + } + + return ['name' => 'varbinary', 'limit' => $limit]; + case static::PHINX_TYPE_BLOB: + if ($limit !== null) { + // Rework this part as the choosen types were always UNDER the required length + $sizes = [ + 'tinyblob' => static::BLOB_SMALL, + 'blob' => static::BLOB_REGULAR, + 'mediumblob' => static::BLOB_MEDIUM, + ]; + + foreach ($sizes as $name => $length) { + if ($limit <= $length) { + return ['name' => $name]; + } + } + + // For more length requirement, the longblob is used + return ['name' => 'longblob']; + } + + // If not limit is provided, fallback on blob + return ['name' => 'blob']; + case static::PHINX_TYPE_TINYBLOB: + // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit + return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_TINY); + case static::PHINX_TYPE_MEDIUMBLOB: + // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit + return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_MEDIUM); + case static::PHINX_TYPE_LONGBLOB: + // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit + return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_LONG); + case static::PHINX_TYPE_BIT: + return ['name' => 'bit', 'limit' => $limit ?: 64]; + case static::PHINX_TYPE_BIG_INTEGER: + return ['name' => 'bigint', 'limit' => $limit ?: 20]; + case static::PHINX_TYPE_MEDIUM_INTEGER: + return ['name' => 'mediumint', 'limit' => $limit ?: 8]; + case static::PHINX_TYPE_SMALL_INTEGER: + return ['name' => 'smallint', 'limit' => $limit ?: 6]; + case static::PHINX_TYPE_TINY_INTEGER: + return ['name' => 'tinyint', 'limit' => $limit ?: 4]; + case static::PHINX_TYPE_INTEGER: + if ($limit && $limit >= static::INT_TINY) { + $sizes = [ + // Order matters! Size must always be tested from longest to shortest! + 'bigint' => static::INT_BIG, + 'int' => static::INT_REGULAR, + 'mediumint' => static::INT_MEDIUM, + 'smallint' => static::INT_SMALL, + 'tinyint' => static::INT_TINY, + ]; + $limits = [ + 'tinyint' => 4, + 'smallint' => 6, + 'mediumint' => 8, + 'int' => 11, + 'bigint' => 20, + ]; + foreach ($sizes as $name => $length) { + if ($limit >= $length) { + $def = ['name' => $name]; + if (isset($limits[$name])) { + $def['limit'] = $limits[$name]; + } + + return $def; + } + } + } elseif (!$limit) { + $limit = 11; + } + + return ['name' => 'int', 'limit' => $limit]; + case static::PHINX_TYPE_BOOLEAN: + return ['name' => 'tinyint', 'limit' => 1]; + case static::PHINX_TYPE_UUID: + return ['name' => 'char', 'limit' => 36]; + case static::PHINX_TYPE_YEAR: + if (!$limit || in_array($limit, [2, 4])) { + $limit = 4; + } + + return ['name' => 'year', 'limit' => $limit]; + default: + throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by MySQL.'); + } + } + + /** + * Returns Phinx type by SQL type + * + * @internal param string $sqlType SQL type + * @param string $sqlTypeDef SQL Type definition + * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException + * @return array Phinx type + */ + public function getPhinxType($sqlTypeDef) + { + $matches = []; + if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*(.+)*$/', $sqlTypeDef, $matches)) { + throw new UnsupportedColumnTypeException('Column type "' . $sqlTypeDef . '" is not supported by MySQL.'); + } + + $limit = null; + $scale = null; + $type = $matches[1]; + if (count($matches) > 2) { + $limit = $matches[3] ? (int)$matches[3] : null; + } + if (count($matches) > 4) { + $scale = (int)$matches[5]; + } + if ($type === 'tinyint' && $limit === 1) { + $type = static::PHINX_TYPE_BOOLEAN; + $limit = null; + } + switch ($type) { + case 'varchar': + $type = static::PHINX_TYPE_STRING; + if ($limit === 255) { + $limit = null; + } + break; + case 'char': + $type = static::PHINX_TYPE_CHAR; + if ($limit === 255) { + $limit = null; + } + if ($limit === 36) { + $type = static::PHINX_TYPE_UUID; + } + break; + case 'tinyint': + $type = static::PHINX_TYPE_TINY_INTEGER; + $limit = static::INT_TINY; + break; + case 'smallint': + $type = static::PHINX_TYPE_SMALL_INTEGER; + $limit = static::INT_SMALL; + break; + case 'mediumint': + $type = static::PHINX_TYPE_MEDIUM_INTEGER; + $limit = static::INT_MEDIUM; + break; + case 'int': + $type = static::PHINX_TYPE_INTEGER; + if ($limit === 11) { + $limit = null; + } + break; + case 'bigint': + if ($limit === 20) { + $limit = null; + } + $type = static::PHINX_TYPE_BIG_INTEGER; + break; + case 'bit': + $type = static::PHINX_TYPE_BIT; + if ($limit === 64) { + $limit = null; + } + break; + case 'blob': + $type = static::PHINX_TYPE_BLOB; + $limit = static::BLOB_REGULAR; + break; + case 'tinyblob': + $type = static::PHINX_TYPE_TINYBLOB; + $limit = static::BLOB_TINY; + break; + case 'mediumblob': + $type = static::PHINX_TYPE_MEDIUMBLOB; + $limit = static::BLOB_MEDIUM; + break; + case 'longblob': + $type = static::PHINX_TYPE_LONGBLOB; + $limit = static::BLOB_LONG; + break; + case 'tinytext': + $type = static::PHINX_TYPE_TEXT; + $limit = static::TEXT_TINY; + break; + case 'mediumtext': + $type = static::PHINX_TYPE_TEXT; + $limit = static::TEXT_MEDIUM; + break; + case 'longtext': + $type = static::PHINX_TYPE_TEXT; + $limit = static::TEXT_LONG; + break; + case 'binary': + if ($limit === null) { + $limit = 255; + } + + if ($limit > 255) { + $type = static::PHINX_TYPE_BLOB; + break; + } + + if ($limit === 16) { + $type = static::PHINX_TYPE_BINARYUUID; + } + break; + } + + try { + // Call this to check if parsed type is supported. + $this->getSqlType($type, $limit); + } catch (UnsupportedColumnTypeException $e) { + $type = Literal::from($type); + } + + $phinxType = [ + 'name' => $type, + 'limit' => $limit, + 'scale' => $scale, + ]; + + if ($type === static::PHINX_TYPE_ENUM || $type === static::PHINX_TYPE_SET) { + $values = trim($matches[6], '()'); + $phinxType['values'] = []; + $opened = false; + $escaped = false; + $wasEscaped = false; + $value = ''; + $valuesLength = strlen($values); + for ($i = 0; $i < $valuesLength; $i++) { + $char = $values[$i]; + if ($char === "'" && !$opened) { + $opened = true; + } elseif ( + !$escaped + && ($i + 1) < $valuesLength + && ( + $char === "'" && $values[$i + 1] === "'" + || $char === '\\' && $values[$i + 1] === '\\' + ) + ) { + $escaped = true; + } elseif ($char === "'" && $opened && !$escaped) { + $phinxType['values'][] = $value; + $value = ''; + $opened = false; + } elseif (($char === "'" || $char === '\\') && $opened && $escaped) { + $value .= $char; + $escaped = false; + $wasEscaped = true; + } elseif ($opened) { + if ($values[$i - 1] === '\\' && !$wasEscaped) { + if ($char === 'n') { + $char = "\n"; + } elseif ($char === 'r') { + $char = "\r"; + } elseif ($char === 't') { + $char = "\t"; + } + if ($values[$i] !== $char) { + $value = substr($value, 0, strlen($value) - 1); + } + } + $value .= $char; + $wasEscaped = false; + } + } + } + + return $phinxType; + } + + /** + * @inheritDoc + */ + public function createDatabase($name, $options = []) + { + $charset = $options['charset'] ?? 'utf8'; + + if (isset($options['collation'])) { + $this->execute(sprintf( + 'CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s` COLLATE `%s`', + $name, + $charset, + $options['collation'] + )); + } else { + $this->execute(sprintf('CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s`', $name, $charset)); + } + } + + /** + * @inheritDoc + */ + public function hasDatabase($name) + { + $rows = $this->fetchAll( + sprintf( + 'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \'%s\'', + $name + ) + ); + + foreach ($rows as $row) { + if (!empty($row)) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function dropDatabase($name) + { + $this->execute(sprintf('DROP DATABASE IF EXISTS `%s`', $name)); + $this->createdTables = []; + } + + /** + * Gets the MySQL Column Definition for a Column object. + * + * @param \Phinx\Db\Table\Column $column Column + * @return string + */ + protected function getColumnSqlDefinition(Column $column) + { + if ($column->getType() instanceof Literal) { + $def = (string)$column->getType(); + } else { + $sqlType = $this->getSqlType($column->getType(), $column->getLimit()); + $def = strtoupper($sqlType['name']); + } + if ($column->getPrecision() && $column->getScale()) { + $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')'; + } elseif (isset($sqlType['limit'])) { + $def .= '(' . $sqlType['limit'] . ')'; + } + + $values = $column->getValues(); + if ($values && is_array($values)) { + $def .= '(' . implode(', ', array_map(function ($value) { + // we special case NULL as it's not actually allowed an enum value, + // and we want MySQL to issue an error on the create statement, but + // quote coerces it to an empty string, which will not error + return $value === null ? 'NULL' : $this->getConnection()->quote($value); + }, $values)) . ')'; + } + + $def .= $column->getEncoding() ? ' CHARACTER SET ' . $column->getEncoding() : ''; + $def .= $column->getCollation() ? ' COLLATE ' . $column->getCollation() : ''; + $def .= !$column->isSigned() && isset($this->signedColumnTypes[$column->getType()]) ? ' unsigned' : ''; + $def .= $column->isNull() ? ' NULL' : ' NOT NULL'; + + if ( + version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8') > -1 + && in_array($column->getType(), [ + static::PHINX_TYPE_GEOMETRY, + static::PHINX_TYPE_POINT, + static::PHINX_TYPE_LINESTRING, + static::PHINX_TYPE_POLYGON, + ]) + && !is_null($column->getSrid()) + ) { + $def .= " SRID {$column->getSrid()}"; + } + + $def .= $column->isIdentity() ? ' AUTO_INCREMENT' : ''; + $def .= $this->getDefaultValueDefinition($column->getDefault(), $column->getType()); + + if ($column->getComment()) { + $def .= ' COMMENT ' . $this->getConnection()->quote($column->getComment()); + } + + if ($column->getUpdate()) { + $def .= ' ON UPDATE ' . $column->getUpdate(); + } + + return $def; + } + + /** + * Gets the MySQL Index Definition for an Index object. + * + * @param \Phinx\Db\Table\Index $index Index + * @return string + */ + protected function getIndexSqlDefinition(Index $index) + { + $def = ''; + $limit = ''; + + if ($index->getType() === Index::UNIQUE) { + $def .= ' UNIQUE'; + } + + if ($index->getType() === Index::FULLTEXT) { + $def .= ' FULLTEXT'; + } + + $def .= ' KEY'; + + if (is_string($index->getName())) { + $def .= ' `' . $index->getName() . '`'; + } + + $columnNames = $index->getColumns(); + $order = $index->getOrder() ?? []; + $columnNames = array_map(function ($columnName) use ($order) { + $ret = '`' . $columnName . '`'; + if (isset($order[$columnName])) { + $ret .= ' ' . $order[$columnName]; + } + + return $ret; + }, $columnNames); + + if (!is_array($index->getLimit())) { + if ($index->getLimit()) { + $limit = '(' . $index->getLimit() . ')'; + } + $def .= ' (' . implode(',', $columnNames) . $limit . ')'; + } else { + $columns = $index->getColumns(); + $limits = $index->getLimit(); + $def .= ' ('; + foreach ($columns as $column) { + $limit = !isset($limits[$column]) || $limits[$column] <= 0 ? '' : '(' . $limits[$column] . ')'; + $columnSort = isset($order[$column]) ?? ''; + $def .= '`' . $column . '`' . $limit . ' ' . $columnSort . ', '; + } + $def = rtrim($def, ', '); + $def .= ' )'; + } + + return $def; + } + + /** + * Gets the MySQL Foreign Key Definition for an ForeignKey object. + * + * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key + * @return string + */ + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey) + { + $def = ''; + if ($foreignKey->getConstraint()) { + $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint()); + } + $columnNames = []; + foreach ($foreignKey->getColumns() as $column) { + $columnNames[] = $this->quoteColumnName($column); + } + $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')'; + $refColumnNames = []; + foreach ($foreignKey->getReferencedColumns() as $column) { + $refColumnNames[] = $this->quoteColumnName($column); + } + $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')'; + if ($foreignKey->getOnDelete()) { + $def .= ' ON DELETE ' . $foreignKey->getOnDelete(); + } + if ($foreignKey->getOnUpdate()) { + $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate(); + } + + return $def; + } + + /** + * Describes a database table. This is a MySQL adapter specific method. + * + * @param string $tableName Table name + * @return array + */ + public function describeTable($tableName) + { + $options = $this->getOptions(); + + // mysql specific + $sql = sprintf( + "SELECT * + FROM information_schema.tables + WHERE table_schema = '%s' + AND table_name = '%s'", + $options['name'], + $tableName + ); + + return $this->fetchRow($sql); + } + + /** + * Returns MySQL column types (inherited and MySQL specified). + * + * @return string[] + */ + public function getColumnTypes() + { + return array_merge(parent::getColumnTypes(), static::$specificColumnTypes); + } + + /** + * @inheritDoc + */ + public function getDecoratedConnection() + { + $options = $this->getOptions(); + $options = [ + 'username' => $options['user'] ?? null, + 'password' => $options['pass'] ?? null, + 'database' => $options['name'], + 'quoteIdentifiers' => true, + ] + $options; + + $driver = new MysqlDriver($options); + $driver->setConnection($this->connection); + + return new Connection(['driver' => $driver] + $options); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/PdoAdapter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/PdoAdapter.php new file mode 100644 index 0000000..b42b1b0 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/PdoAdapter.php @@ -0,0 +1,971 @@ + + */ +abstract class PdoAdapter extends AbstractAdapter implements DirectActionInterface +{ + /** + * @var \PDO|null + */ + protected $connection; + + /** + * Writes a message to stdout if verbose output is on + * + * @param string $message The message to show + * @return void + */ + protected function verboseLog($message) + { + if ( + !$this->isDryRunEnabled() && + $this->getOutput()->getVerbosity() < OutputInterface::VERBOSITY_VERY_VERBOSE + ) { + return; + } + + $this->getOutput()->writeln($message); + } + + /** + * Create PDO connection + * + * @param string $dsn Connection string + * @param string|null $username Database username + * @param string|null $password Database password + * @param array $options Connection options + * @return \PDO + */ + protected function createPdoConnection($dsn, $username = null, $password = null, array $options = []) + { + $adapterOptions = $this->getOptions() + [ + 'attr_errmode' => PDO::ERRMODE_EXCEPTION, + ]; + + try { + $db = new PDO($dsn, $username, $password, $options); + + foreach ($adapterOptions as $key => $option) { + if (strpos($key, 'attr_') === 0) { + $pdoConstant = '\PDO::' . strtoupper($key); + if (!defined($pdoConstant)) { + throw new \UnexpectedValueException('Invalid PDO attribute: ' . $key . ' (' . $pdoConstant . ')'); + } + $db->setAttribute(constant($pdoConstant), $option); + } + } + } catch (PDOException $e) { + throw new InvalidArgumentException(sprintf( + 'There was a problem connecting to the database: %s', + $e->getMessage() + ), $e->getCode(), $e); + } + + return $db; + } + + /** + * @inheritDoc + */ + public function setOptions(array $options) + { + parent::setOptions($options); + + if (isset($options['connection'])) { + $this->setConnection($options['connection']); + } + + return $this; + } + + /** + * Sets the database connection. + * + * @param \PDO $connection Connection + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function setConnection(PDO $connection) + { + $this->connection = $connection; + + // Create the schema table if it doesn't already exist + if (!$this->hasSchemaTable()) { + $this->createSchemaTable(); + } else { + $table = new DbTable($this->getSchemaTableName(), [], $this); + if (!$table->hasColumn('migration_name')) { + $table + ->addColumn( + 'migration_name', + 'string', + ['limit' => 100, 'after' => 'version', 'default' => null, 'null' => true] + ) + ->save(); + } + if (!$table->hasColumn('breakpoint')) { + $table + ->addColumn('breakpoint', 'boolean', ['default' => false]) + ->save(); + } + } + + return $this; + } + + /** + * Gets the database connection + * + * @return \PDO + */ + public function getConnection() + { + if ($this->connection === null) { + $this->connect(); + } + + return $this->connection; + } + + /** + * @inheritDoc + */ + public function connect() + { + } + + /** + * @inheritDoc + */ + public function disconnect() + { + } + + /** + * @inheritDoc + */ + public function execute($sql) + { + $sql = rtrim($sql, "; \t\n\r\0\x0B") . ';'; + $this->verboseLog($sql); + + if ($this->isDryRunEnabled()) { + return 0; + } + + return $this->getConnection()->exec($sql); + } + + /** + * Returns the Cake\Database connection object using the same underlying + * PDO object as this connection. + * + * @return \Cake\Database\Connection + */ + abstract public function getDecoratedConnection(); + + /** + * @inheritDoc + */ + public function getQueryBuilder() + { + return $this->getDecoratedConnection()->newQuery(); + } + + /** + * Executes a query and returns PDOStatement. + * + * @param string $sql SQL + * @return \PDOStatement + */ + public function query($sql) + { + return $this->getConnection()->query($sql); + } + + /** + * @inheritDoc + */ + public function fetchRow($sql) + { + return $this->query($sql)->fetch(); + } + + /** + * @inheritDoc + */ + public function fetchAll($sql) + { + return $this->query($sql)->fetchAll(); + } + + /** + * @inheritDoc + */ + public function insert(Table $table, $row) + { + $sql = sprintf( + 'INSERT INTO %s ', + $this->quoteTableName($table->getName()) + ); + $columns = array_keys($row); + $sql .= '(' . implode(', ', array_map([$this, 'quoteColumnName'], $columns)) . ')'; + + foreach ($row as $column => $value) { + if (is_bool($value)) { + $row[$column] = $this->castToBool($value); + } + } + + if ($this->isDryRunEnabled()) { + $sql .= ' VALUES (' . implode(', ', array_map([$this, 'quoteValue'], $row)) . ');'; + $this->output->writeln($sql); + } else { + $sql .= ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; + $stmt = $this->getConnection()->prepare($sql); + $stmt->execute(array_values($row)); + } + } + + /** + * Quotes a database value. + * + * @param mixed $value The value to quote + * @return mixed + */ + protected function quoteValue($value) + { + if (is_numeric($value)) { + return $value; + } + + if ($value === null) { + return 'null'; + } + + return $this->getConnection()->quote($value); + } + + /** + * Quotes a database string. + * + * @param string $value The string to quote + * @return string + */ + protected function quoteString($value) + { + return $this->getConnection()->quote($value); + } + + /** + * @inheritDoc + */ + public function bulkinsert(Table $table, $rows) + { + $sql = sprintf( + 'INSERT INTO %s ', + $this->quoteTableName($table->getName()) + ); + $current = current($rows); + $keys = array_keys($current); + $sql .= '(' . implode(', ', array_map([$this, 'quoteColumnName'], $keys)) . ') VALUES '; + + if ($this->isDryRunEnabled()) { + $values = array_map(function ($row) { + return '(' . implode(', ', array_map([$this, 'quoteValue'], $row)) . ')'; + }, $rows); + $sql .= implode(', ', $values) . ';'; + $this->output->writeln($sql); + } else { + $count_keys = count($keys); + $query = '(' . implode(', ', array_fill(0, $count_keys, '?')) . ')'; + $count_vars = count($rows); + $queries = array_fill(0, $count_vars, $query); + $sql .= implode(',', $queries); + $stmt = $this->getConnection()->prepare($sql); + $vals = []; + + foreach ($rows as $row) { + foreach ($row as $v) { + if (is_bool($v)) { + $vals[] = $this->castToBool($v); + } else { + $vals[] = $v; + } + } + } + + $stmt->execute($vals); + } + } + + /** + * @inheritDoc + */ + public function getVersions() + { + $rows = $this->getVersionLog(); + + return array_keys($rows); + } + + /** + * {@inheritDoc} + * + * @throws \RuntimeException + */ + public function getVersionLog() + { + $result = []; + + switch ($this->options['version_order']) { + case Config::VERSION_ORDER_CREATION_TIME: + $orderBy = 'version ASC'; + break; + case Config::VERSION_ORDER_EXECUTION_TIME: + $orderBy = 'start_time ASC, version ASC'; + break; + default: + throw new RuntimeException('Invalid version_order configuration option'); + } + + // This will throw an exception if doing a --dry-run without any migrations as phinxlog + // does not exist, so in that case, we can just expect to trivially return empty set + try { + $rows = $this->fetchAll(sprintf('SELECT * FROM %s ORDER BY %s', $this->quoteTableName($this->getSchemaTableName()), $orderBy)); + } catch (PDOException $e) { + if (!$this->isDryRunEnabled()) { + throw $e; + } + $rows = []; + } + + foreach ($rows as $version) { + $result[$version['version']] = $version; + } + + return $result; + } + + /** + * @inheritDoc + */ + public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime) + { + if (strcasecmp($direction, MigrationInterface::UP) === 0) { + // up + $sql = sprintf( + "INSERT INTO %s (%s, %s, %s, %s, %s) VALUES ('%s', '%s', '%s', '%s', %s);", + $this->quoteTableName($this->getSchemaTableName()), + $this->quoteColumnName('version'), + $this->quoteColumnName('migration_name'), + $this->quoteColumnName('start_time'), + $this->quoteColumnName('end_time'), + $this->quoteColumnName('breakpoint'), + $migration->getVersion(), + substr($migration->getName(), 0, 100), + $startTime, + $endTime, + $this->castToBool(false) + ); + + $this->execute($sql); + } else { + // down + $sql = sprintf( + "DELETE FROM %s WHERE %s = '%s'", + $this->quoteTableName($this->getSchemaTableName()), + $this->quoteColumnName('version'), + $migration->getVersion() + ); + + $this->execute($sql); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function toggleBreakpoint(MigrationInterface $migration) + { + $this->query( + sprintf( + 'UPDATE %1$s SET %2$s = CASE %2$s WHEN %3$s THEN %4$s ELSE %3$s END, %7$s = %7$s WHERE %5$s = \'%6$s\';', + $this->quoteTableName($this->getSchemaTableName()), + $this->quoteColumnName('breakpoint'), + $this->castToBool(true), + $this->castToBool(false), + $this->quoteColumnName('version'), + $migration->getVersion(), + $this->quoteColumnName('start_time') + ) + ); + + return $this; + } + + /** + * @inheritDoc + */ + public function resetAllBreakpoints() + { + return $this->execute( + sprintf( + 'UPDATE %1$s SET %2$s = %3$s, %4$s = %4$s WHERE %2$s <> %3$s;', + $this->quoteTableName($this->getSchemaTableName()), + $this->quoteColumnName('breakpoint'), + $this->castToBool(false), + $this->quoteColumnName('start_time') + ) + ); + } + + /** + * @inheritDoc + */ + public function setBreakpoint(MigrationInterface $migration) + { + return $this->markBreakpoint($migration, true); + } + + /** + * @inheritDoc + */ + public function unsetBreakpoint(MigrationInterface $migration) + { + return $this->markBreakpoint($migration, false); + } + + /** + * Mark a migration breakpoint. + * + * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint + * @param bool $state The required state of the breakpoint + * @return \Phinx\Db\Adapter\AdapterInterface + */ + protected function markBreakpoint(MigrationInterface $migration, $state) + { + $this->query( + sprintf( + 'UPDATE %1$s SET %2$s = %3$s, %4$s = %4$s WHERE %5$s = \'%6$s\';', + $this->quoteTableName($this->getSchemaTableName()), + $this->quoteColumnName('breakpoint'), + $this->castToBool($state), + $this->quoteColumnName('start_time'), + $this->quoteColumnName('version'), + $migration->getVersion() + ) + ); + + return $this; + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function createSchema($schemaName = 'public') + { + throw new BadMethodCallException('Creating a schema is not supported'); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropSchema($name) + { + throw new BadMethodCallException('Dropping a schema is not supported'); + } + + /** + * @inheritDoc + */ + public function getColumnTypes() + { + return [ + 'string', + 'char', + 'text', + 'tinyinteger', + 'smallinteger', + 'integer', + 'biginteger', + 'bit', + 'float', + 'decimal', + 'double', + 'datetime', + 'timestamp', + 'time', + 'date', + 'blob', + 'binary', + 'varbinary', + 'boolean', + 'uuid', + // Geospatial data types + 'geometry', + 'point', + 'linestring', + 'polygon', + ]; + } + + /** + * @inheritDoc + */ + public function castToBool($value) + { + return (bool)$value ? 1 : 0; + } + + /** + * Retrieve a database connection attribute + * + * @see http://php.net/manual/en/pdo.getattribute.php + * @param int $attribute One of the PDO::ATTR_* constants + * @return mixed + */ + public function getAttribute($attribute) + { + return $this->connection->getAttribute($attribute); + } + + /** + * Get the definition for a `DEFAULT` statement. + * + * @param mixed $default Default value + * @param string|null $columnType column type added + * @return string + */ + protected function getDefaultValueDefinition($default, $columnType = null) + { + // Ensure a defaults of CURRENT_TIMESTAMP(3) is not quoted. + if (is_string($default) && strpos($default, 'CURRENT_TIMESTAMP') !== 0) { + $default = $this->getConnection()->quote($default); + } elseif (is_bool($default)) { + $default = $this->castToBool($default); + } elseif ($default !== null && $columnType === static::PHINX_TYPE_BOOLEAN) { + $default = $this->castToBool((bool)$default); + } + + return isset($default) ? " DEFAULT $default" : ''; + } + + /** + * Executes all the ALTER TABLE instructions passed for the given table + * + * @param string $tableName The table name to use in the ALTER statement + * @param \Phinx\Db\Util\AlterInstructions $instructions The object containing the alter sequence + * @return void + */ + protected function executeAlterSteps($tableName, AlterInstructions $instructions) + { + $alter = sprintf('ALTER TABLE %s %%s', $this->quoteTableName($tableName)); + $instructions->execute($alter, [$this, 'execute']); + } + + /** + * @inheritDoc + */ + public function addColumn(Table $table, Column $column) + { + $instructions = $this->getAddColumnInstructions($table, $column); + $this->executeAlterSteps($table->getName(), $instructions); + } + + /** + * Returns the instructions to add the specified column to a database table. + * + * @param \Phinx\Db\Table\Table $table Table + * @param \Phinx\Db\Table\Column $column Column + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getAddColumnInstructions(Table $table, Column $column); + + /** + * @inheritdoc + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $instructions = $this->getRenameColumnInstructions($tableName, $columnName, $newColumnName); + $this->executeAlterSteps($tableName, $instructions); + } + + /** + * Returns the instructions to rename the specified column. + * + * @param string $tableName Table name + * @param string $columnName Column Name + * @param string $newColumnName New Column Name + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName); + + /** + * @inheritdoc + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + $instructions = $this->getChangeColumnInstructions($tableName, $columnName, $newColumn); + $this->executeAlterSteps($tableName, $instructions); + } + + /** + * Returns the instructions to change a table column type. + * + * @param string $tableName Table name + * @param string $columnName Column Name + * @param \Phinx\Db\Table\Column $newColumn New Column + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn); + + /** + * @inheritdoc + */ + public function dropColumn($tableName, $columnName) + { + $instructions = $this->getDropColumnInstructions($tableName, $columnName); + $this->executeAlterSteps($tableName, $instructions); + } + + /** + * Returns the instructions to drop the specified column. + * + * @param string $tableName Table name + * @param string $columnName Column Name + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getDropColumnInstructions($tableName, $columnName); + + /** + * @inheritdoc + */ + public function addIndex(Table $table, Index $index) + { + $instructions = $this->getAddIndexInstructions($table, $index); + $this->executeAlterSteps($table->getName(), $instructions); + } + + /** + * Returns the instructions to add the specified index to a database table. + * + * @param \Phinx\Db\Table\Table $table Table + * @param \Phinx\Db\Table\Index $index Index + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getAddIndexInstructions(Table $table, Index $index); + + /** + * @inheritdoc + */ + public function dropIndex($tableName, $columns) + { + $instructions = $this->getDropIndexByColumnsInstructions($tableName, $columns); + $this->executeAlterSteps($tableName, $instructions); + } + + /** + * Returns the instructions to drop the specified index from a database table. + * + * @param string $tableName The name of of the table where the index is + * @param mixed $columns Column(s) + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getDropIndexByColumnsInstructions($tableName, $columns); + + /** + * @inheritdoc + */ + public function dropIndexByName($tableName, $indexName) + { + $instructions = $this->getDropIndexByNameInstructions($tableName, $indexName); + $this->executeAlterSteps($tableName, $instructions); + } + + /** + * Returns the instructions to drop the index specified by name from a database table. + * + * @param string $tableName The table name whe the index is + * @param string $indexName The name of the index + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getDropIndexByNameInstructions($tableName, $indexName); + + /** + * @inheritdoc + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + $instructions = $this->getAddForeignKeyInstructions($table, $foreignKey); + $this->executeAlterSteps($table->getName(), $instructions); + } + + /** + * Returns the instructions to adds the specified foreign key to a database table. + * + * @param \Phinx\Db\Table\Table $table The table to add the constraint to + * @param \Phinx\Db\Table\ForeignKey $foreignKey The foreign key to add + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey); + + /** + * @inheritDoc + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + if ($constraint) { + $instructions = $this->getDropForeignKeyInstructions($tableName, $constraint); + } else { + $instructions = $this->getDropForeignKeyByColumnsInstructions($tableName, $columns); + } + + $this->executeAlterSteps($tableName, $instructions); + } + + /** + * Returns the instructions to drop the specified foreign key from a database table. + * + * @param string $tableName The table where the foreign key constraint is + * @param string $constraint Constraint name + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getDropForeignKeyInstructions($tableName, $constraint); + + /** + * Returns the instructions to drop the specified foreign key from a database table. + * + * @param string $tableName The table where the foreign key constraint is + * @param string[] $columns The list of column names + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getDropForeignKeyByColumnsInstructions($tableName, $columns); + + /** + * @inheritdoc + */ + public function dropTable($tableName) + { + $instructions = $this->getDropTableInstructions($tableName); + $this->executeAlterSteps($tableName, $instructions); + } + + /** + * Returns the instructions to drop the specified database table. + * + * @param string $tableName Table name + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getDropTableInstructions($tableName); + + /** + * @inheritdoc + */ + public function renameTable($tableName, $newTableName) + { + $instructions = $this->getRenameTableInstructions($tableName, $newTableName); + $this->executeAlterSteps($tableName, $instructions); + } + + /** + * Returns the instructions to rename the specified database table. + * + * @param string $tableName Table name + * @param string $newTableName New Name + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getRenameTableInstructions($tableName, $newTableName); + + /** + * @inheritdoc + */ + public function changePrimaryKey(Table $table, $newColumns) + { + $instructions = $this->getChangePrimaryKeyInstructions($table, $newColumns); + $this->executeAlterSteps($table->getName(), $instructions); + } + + /** + * Returns the instructions to change the primary key for the specified database table. + * + * @param \Phinx\Db\Table\Table $table Table + * @param string|string[]|null $newColumns Column name(s) to belong to the primary key, or null to drop the key + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getChangePrimaryKeyInstructions(Table $table, $newColumns); + + /** + * @inheritdoc + */ + public function changeComment(Table $table, $newComment) + { + $instructions = $this->getChangeCommentInstructions($table, $newComment); + $this->executeAlterSteps($table->getName(), $instructions); + } + + /** + * Returns the instruction to change the comment for the specified database table. + * + * @param \Phinx\Db\Table\Table $table Table + * @param string|null $newComment New comment string, or null to drop the comment + * @return \Phinx\Db\Util\AlterInstructions + */ + abstract protected function getChangeCommentInstructions(Table $table, $newComment); + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + * @return void + */ + public function executeActions(Table $table, array $actions) + { + $instructions = new AlterInstructions(); + + foreach ($actions as $action) { + switch (true) { + case $action instanceof AddColumn: + $instructions->merge($this->getAddColumnInstructions($table, $action->getColumn())); + break; + + case $action instanceof AddIndex: + $instructions->merge($this->getAddIndexInstructions($table, $action->getIndex())); + break; + + case $action instanceof AddForeignKey: + $instructions->merge($this->getAddForeignKeyInstructions($table, $action->getForeignKey())); + break; + + case $action instanceof ChangeColumn: + $instructions->merge($this->getChangeColumnInstructions( + $table->getName(), + $action->getColumnName(), + $action->getColumn() + )); + break; + + case $action instanceof DropForeignKey && !$action->getForeignKey()->getConstraint(): + $instructions->merge($this->getDropForeignKeyByColumnsInstructions( + $table->getName(), + $action->getForeignKey()->getColumns() + )); + break; + + case $action instanceof DropForeignKey && $action->getForeignKey()->getConstraint(): + $instructions->merge($this->getDropForeignKeyInstructions( + $table->getName(), + $action->getForeignKey()->getConstraint() + )); + break; + + case $action instanceof DropIndex && $action->getIndex()->getName() !== null: + $instructions->merge($this->getDropIndexByNameInstructions( + $table->getName(), + $action->getIndex()->getName() + )); + break; + + case $action instanceof DropIndex && $action->getIndex()->getName() == null: + $instructions->merge($this->getDropIndexByColumnsInstructions( + $table->getName(), + $action->getIndex()->getColumns() + )); + break; + + case $action instanceof DropTable: + $instructions->merge($this->getDropTableInstructions( + $table->getName() + )); + break; + + case $action instanceof RemoveColumn: + $instructions->merge($this->getDropColumnInstructions( + $table->getName(), + $action->getColumn()->getName() + )); + break; + + case $action instanceof RenameColumn: + $instructions->merge($this->getRenameColumnInstructions( + $table->getName(), + $action->getColumn()->getName(), + $action->getNewName() + )); + break; + + case $action instanceof RenameTable: + $instructions->merge($this->getRenameTableInstructions( + $table->getName(), + $action->getNewName() + )); + break; + + case $action instanceof ChangePrimaryKey: + $instructions->merge($this->getChangePrimaryKeyInstructions( + $table, + $action->getNewColumns() + )); + break; + + case $action instanceof ChangeComment: + $instructions->merge($this->getChangeCommentInstructions( + $table, + $action->getNewComment() + )); + break; + + default: + throw new InvalidArgumentException( + sprintf("Don't know how to execute action: '%s'", get_class($action)) + ); + } + } + + $this->executeAlterSteps($table->getName(), $instructions); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/PostgresAdapter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/PostgresAdapter.php new file mode 100644 index 0000000..b9b637d --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/PostgresAdapter.php @@ -0,0 +1,1525 @@ +connection === null) { + if (!class_exists('PDO') || !in_array('pgsql', PDO::getAvailableDrivers(), true)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('You need to enable the PDO_Pgsql extension for Phinx to run properly.'); + // @codeCoverageIgnoreEnd + } + + $options = $this->getOptions(); + + $dsn = 'pgsql:dbname=' . $options['name']; + + if (isset($options['host'])) { + $dsn .= ';host=' . $options['host']; + } + + // if custom port is specified use it + if (isset($options['port'])) { + $dsn .= ';port=' . $options['port']; + } + + $driverOptions = []; + + // use custom data fetch mode + if (!empty($options['fetch_mode'])) { + $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode'])); + } + + $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions); + + try { + if (isset($options['schema'])) { + $db->exec('SET search_path TO ' . $this->quoteSchemaName($options['schema'])); + } + } catch (PDOException $exception) { + throw new InvalidArgumentException( + sprintf('Schema does not exists: %s', $options['schema']), + $exception->getCode(), + $exception + ); + } + + $this->setConnection($db); + } + } + + /** + * @inheritDoc + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * @inheritDoc + */ + public function hasTransactions() + { + return true; + } + + /** + * @inheritDoc + */ + public function beginTransaction() + { + $this->execute('BEGIN'); + } + + /** + * @inheritDoc + */ + public function commitTransaction() + { + $this->execute('COMMIT'); + } + + /** + * @inheritDoc + */ + public function rollbackTransaction() + { + $this->execute('ROLLBACK'); + } + + /** + * Quotes a schema name for use in a query. + * + * @param string $schemaName Schema Name + * @return string + */ + public function quoteSchemaName($schemaName) + { + return $this->quoteColumnName($schemaName); + } + + /** + * @inheritDoc + */ + public function quoteTableName($tableName) + { + $parts = $this->getSchemaName($tableName); + + return $this->quoteSchemaName($parts['schema']) . '.' . $this->quoteColumnName($parts['table']); + } + + /** + * @inheritDoc + */ + public function quoteColumnName($columnName) + { + return '"' . $columnName . '"'; + } + + /** + * @inheritDoc + */ + public function hasTable($tableName) + { + if ($this->hasCreatedTable($tableName)) { + return true; + } + + $parts = $this->getSchemaName($tableName); + $result = $this->getConnection()->query( + sprintf( + 'SELECT * + FROM information_schema.tables + WHERE table_schema = %s + AND table_name = %s', + $this->getConnection()->quote($parts['schema']), + $this->getConnection()->quote($parts['table']) + ) + ); + + return $result->rowCount() === 1; + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []) + { + $queries = []; + + $options = $table->getOptions(); + $parts = $this->getSchemaName($table->getName()); + + // Add the default primary key + if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + $options['id'] = 'id'; + } + + if (isset($options['id']) && is_string($options['id'])) { + // Handle id => "field_name" to support AUTO_INCREMENT + $column = new Column(); + $column->setName($options['id']) + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) { + throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key'); + } + $options['primary_key'] = $options['id']; + } + + // TODO - process table options like collation etc + $sql = 'CREATE TABLE '; + $sql .= $this->quoteTableName($table->getName()) . ' ('; + + $this->columnsWithComments = []; + foreach ($columns as $column) { + $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', '; + + // set column comments, if needed + if ($column->getComment()) { + $this->columnsWithComments[] = $column; + } + } + + // set the primary key(s) + if (isset($options['primary_key'])) { + $sql = rtrim($sql); + $sql .= sprintf(' CONSTRAINT %s PRIMARY KEY (', $this->quoteColumnName($parts['table'] . '_pkey')); + if (is_string($options['primary_key'])) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($options['primary_key']); + } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') + $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key'])); + } + $sql .= ')'; + } else { + $sql = rtrim($sql, ', '); // no primary keys + } + + $sql .= ')'; + $queries[] = $sql; + + // process column comments + if (!empty($this->columnsWithComments)) { + foreach ($this->columnsWithComments as $column) { + $queries[] = $this->getColumnCommentSqlDefinition($column, $table->getName()); + } + } + + // set the indexes + if (!empty($indexes)) { + foreach ($indexes as $index) { + $queries[] = $this->getIndexSqlDefinition($index, $table->getName()); + } + } + + // process table comments + if (isset($options['comment'])) { + $queries[] = sprintf( + 'COMMENT ON TABLE %s IS %s', + $this->quoteTableName($table->getName()), + $this->getConnection()->quote($options['comment']) + ); + } + + foreach ($queries as $query) { + $this->execute($query); + } + + $this->addCreatedTable($table->getName()); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getChangePrimaryKeyInstructions(Table $table, $newColumns) + { + $parts = $this->getSchemaName($table->getName()); + + $instructions = new AlterInstructions(); + + // Drop the existing primary key + $primaryKey = $this->getPrimaryKey($table->getName()); + if (!empty($primaryKey['constraint'])) { + $sql = sprintf( + 'DROP CONSTRAINT %s', + $this->quoteColumnName($primaryKey['constraint']) + ); + $instructions->addAlter($sql); + } + + // Add the new primary key + if (!empty($newColumns)) { + $sql = sprintf( + 'ADD CONSTRAINT %s PRIMARY KEY (', + $this->quoteColumnName($parts['table'] . '_pkey') + ); + if (is_string($newColumns)) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($newColumns); + } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id') + $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns)); + } else { + throw new InvalidArgumentException(sprintf( + 'Invalid value for primary key: %s', + json_encode($newColumns) + )); + } + $sql .= ')'; + $instructions->addAlter($sql); + } + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getChangeCommentInstructions(Table $table, $newComment) + { + $instructions = new AlterInstructions(); + + // passing 'null' is to remove table comment + $newComment = $newComment !== null + ? $this->getConnection()->quote($newComment) + : 'NULL'; + $sql = sprintf( + 'COMMENT ON TABLE %s IS %s', + $this->quoteTableName($table->getName()), + $newComment + ); + $instructions->addPostStep($sql); + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getRenameTableInstructions($tableName, $newTableName) + { + $this->updateCreatedTableName($tableName, $newTableName); + $sql = sprintf( + 'ALTER TABLE %s RENAME TO %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($newTableName) + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + protected function getDropTableInstructions($tableName) + { + $this->removeCreatedTable($tableName); + $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName)); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + public function truncateTable($tableName) + { + $sql = sprintf( + 'TRUNCATE TABLE %s RESTART IDENTITY', + $this->quoteTableName($tableName) + ); + + $this->execute($sql); + } + + /** + * @inheritDoc + */ + public function getColumns($tableName) + { + $parts = $this->getSchemaName($tableName); + $columns = []; + $sql = sprintf( + 'SELECT column_name, data_type, udt_name, is_identity, is_nullable, + column_default, character_maximum_length, numeric_precision, numeric_scale, + datetime_precision + FROM information_schema.columns + WHERE table_schema = %s AND table_name = %s + ORDER BY ordinal_position', + $this->getConnection()->quote($parts['schema']), + $this->getConnection()->quote($parts['table']) + ); + $columnsInfo = $this->fetchAll($sql); + + foreach ($columnsInfo as $columnInfo) { + $isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED'; + + if ($isUserDefined) { + $columnType = Literal::from($columnInfo['udt_name']); + } else { + $columnType = $this->getPhinxType($columnInfo['data_type']); + } + + // If the default value begins with a ' or looks like a function mark it as literal + if (isset($columnInfo['column_default'][0]) && $columnInfo['column_default'][0] === "'") { + if (preg_match('/^\'(.*)\'::[^:]+$/', $columnInfo['column_default'], $match)) { + // '' and \' are replaced with a single ' + $columnDefault = preg_replace('/[\'\\\\]\'/', "'", $match[1]); + } else { + $columnDefault = Literal::from($columnInfo['column_default']); + } + } elseif (preg_match('/^\D[a-z_\d]*\(.*\)$/', $columnInfo['column_default'])) { + $columnDefault = Literal::from($columnInfo['column_default']); + } else { + $columnDefault = $columnInfo['column_default']; + } + + $column = new Column(); + $column->setName($columnInfo['column_name']) + ->setType($columnType) + ->setNull($columnInfo['is_nullable'] === 'YES') + ->setDefault($columnDefault) + ->setIdentity($columnInfo['is_identity'] === 'YES') + ->setScale($columnInfo['numeric_scale']); + + if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) { + $column->setTimezone(true); + } + + if (isset($columnInfo['character_maximum_length'])) { + $column->setLimit($columnInfo['character_maximum_length']); + } + + if (in_array($columnType, [static::PHINX_TYPE_TIME, static::PHINX_TYPE_DATETIME], true)) { + $column->setPrecision($columnInfo['datetime_precision']); + } elseif ( + !in_array($columnType, [ + self::PHINX_TYPE_SMALL_INTEGER, + self::PHINX_TYPE_INTEGER, + self::PHINX_TYPE_BIG_INTEGER, + ], true) + ) { + $column->setPrecision($columnInfo['numeric_precision']); + } + $columns[] = $column; + } + + return $columns; + } + + /** + * @inheritDoc + */ + public function hasColumn($tableName, $columnName) + { + $parts = $this->getSchemaName($tableName); + $sql = sprintf( + 'SELECT count(*) + FROM information_schema.columns + WHERE table_schema = %s AND table_name = %s AND column_name = %s', + $this->getConnection()->quote($parts['schema']), + $this->getConnection()->quote($parts['table']), + $this->getConnection()->quote($columnName) + ); + + $result = $this->fetchRow($sql); + + return $result['count'] > 0; + } + + /** + * @inheritDoc + */ + protected function getAddColumnInstructions(Table $table, Column $column) + { + $instructions = new AlterInstructions(); + $instructions->addAlter(sprintf( + 'ADD %s %s', + $this->quoteColumnName($column->getName()), + $this->getColumnSqlDefinition($column) + )); + + if ($column->getComment()) { + $instructions->addPostStep($this->getColumnCommentSqlDefinition($column, $table->getName())); + } + + return $instructions; + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName) + { + $parts = $this->getSchemaName($tableName); + $sql = sprintf( + 'SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists + FROM information_schema.columns + WHERE table_schema = %s AND table_name = %s AND column_name = %s', + $this->getConnection()->quote($parts['schema']), + $this->getConnection()->quote($parts['table']), + $this->getConnection()->quote($columnName) + ); + + $result = $this->fetchRow($sql); + if (!(bool)$result['column_exists']) { + throw new InvalidArgumentException("The specified column does not exist: $columnName"); + } + + $instructions = new AlterInstructions(); + $instructions->addPostStep( + sprintf( + 'ALTER TABLE %s RENAME COLUMN %s TO %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName), + $this->quoteColumnName($newColumnName) + ) + ); + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn) + { + $quotedColumnName = $this->quoteColumnName($columnName); + $instructions = new AlterInstructions(); + if ($newColumn->getType() === 'boolean') { + $sql = sprintf('ALTER COLUMN %s DROP DEFAULT', $quotedColumnName); + $instructions->addAlter($sql); + } + $sql = sprintf( + 'ALTER COLUMN %s TYPE %s', + $quotedColumnName, + $this->getColumnSqlDefinition($newColumn) + ); + if (in_array($newColumn->getType(), ['smallinteger', 'integer', 'biginteger'], true)) { + $sql .= sprintf( + ' USING (%s::bigint)', + $quotedColumnName + ); + } + if ($newColumn->getType() === 'uuid') { + $sql .= sprintf( + ' USING (%s::uuid)', + $quotedColumnName + ); + } + //NULL and DEFAULT cannot be set while changing column type + $sql = preg_replace('/ NOT NULL/', '', $sql); + $sql = preg_replace('/ NULL/', '', $sql); + //If it is set, DEFAULT is the last definition + $sql = preg_replace('/DEFAULT .*/', '', $sql); + if ($newColumn->getType() === 'boolean') { + $sql .= sprintf( + ' USING (CASE WHEN %s IS NULL THEN NULL WHEN %s::int=0 THEN FALSE ELSE TRUE END)', + $quotedColumnName, + $quotedColumnName + ); + } + $instructions->addAlter($sql); + + // process null + $sql = sprintf( + 'ALTER COLUMN %s', + $quotedColumnName + ); + + if ($newColumn->isNull()) { + $sql .= ' DROP NOT NULL'; + } else { + $sql .= ' SET NOT NULL'; + } + + $instructions->addAlter($sql); + + if ($newColumn->getDefault() !== null) { + $instructions->addAlter(sprintf( + 'ALTER COLUMN %s SET %s', + $quotedColumnName, + $this->getDefaultValueDefinition($newColumn->getDefault(), $newColumn->getType()) + )); + } else { + //drop default + $instructions->addAlter(sprintf( + 'ALTER COLUMN %s DROP DEFAULT', + $quotedColumnName + )); + } + + // rename column + if ($columnName !== $newColumn->getName()) { + $instructions->addPostStep(sprintf( + 'ALTER TABLE %s RENAME COLUMN %s TO %s', + $this->quoteTableName($tableName), + $quotedColumnName, + $this->quoteColumnName($newColumn->getName()) + )); + } + + // change column comment if needed + if ($newColumn->getComment()) { + $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName)); + } + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getDropColumnInstructions($tableName, $columnName) + { + $alter = sprintf( + 'DROP COLUMN %s', + $this->quoteColumnName($columnName) + ); + + return new AlterInstructions([$alter]); + } + + /** + * Get an array of indexes from a particular table. + * + * @param string $tableName Table name + * @return array + */ + protected function getIndexes($tableName) + { + $parts = $this->getSchemaName($tableName); + + $indexes = []; + $sql = sprintf( + "SELECT + i.relname AS index_name, + a.attname AS column_name + FROM + pg_class t, + pg_class i, + pg_index ix, + pg_attribute a, + pg_namespace nsp + WHERE + t.oid = ix.indrelid + AND i.oid = ix.indexrelid + AND a.attrelid = t.oid + AND a.attnum = ANY(ix.indkey) + AND t.relnamespace = nsp.oid + AND nsp.nspname = %s + AND t.relkind = 'r' + AND t.relname = %s + ORDER BY + t.relname, + i.relname", + $this->getConnection()->quote($parts['schema']), + $this->getConnection()->quote($parts['table']) + ); + $rows = $this->fetchAll($sql); + foreach ($rows as $row) { + if (!isset($indexes[$row['index_name']])) { + $indexes[$row['index_name']] = ['columns' => []]; + } + $indexes[$row['index_name']]['columns'][] = $row['column_name']; + } + + return $indexes; + } + + /** + * @inheritDoc + */ + public function hasIndex($tableName, $columns) + { + if (is_string($columns)) { + $columns = [$columns]; + } + $indexes = $this->getIndexes($tableName); + foreach ($indexes as $index) { + if (array_diff($index['columns'], $columns) === array_diff($columns, $index['columns'])) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function hasIndexByName($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function getAddIndexInstructions(Table $table, Index $index) + { + $instructions = new AlterInstructions(); + $instructions->addPostStep($this->getIndexSqlDefinition($index, $table->getName())); + + return $instructions; + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getDropIndexByColumnsInstructions($tableName, $columns) + { + $parts = $this->getSchemaName($tableName); + + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + + $indexes = $this->getIndexes($tableName); + foreach ($indexes as $indexName => $index) { + $a = array_diff($columns, $index['columns']); + if (empty($a)) { + return new AlterInstructions([], [sprintf( + 'DROP INDEX IF EXISTS %s', + '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName)) + )]); + } + } + + throw new InvalidArgumentException(sprintf( + "The specified index on columns '%s' does not exist", + implode(',', $columns) + )); + } + + /** + * @inheritDoc + */ + protected function getDropIndexByNameInstructions($tableName, $indexName) + { + $parts = $this->getSchemaName($tableName); + + $sql = sprintf( + 'DROP INDEX IF EXISTS %s', + '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName)) + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + public function hasPrimaryKey($tableName, $columns, $constraint = null) + { + $primaryKey = $this->getPrimaryKey($tableName); + + if (empty($primaryKey)) { + return false; + } + + if ($constraint) { + return $primaryKey['constraint'] === $constraint; + } else { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + $missingColumns = array_diff($columns, $primaryKey['columns']); + + return empty($missingColumns); + } + } + + /** + * Get the primary key from a particular table. + * + * @param string $tableName Table name + * @return array + */ + public function getPrimaryKey($tableName) + { + $parts = $this->getSchemaName($tableName); + $rows = $this->fetchAll(sprintf( + "SELECT + tc.constraint_name, + kcu.column_name + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + WHERE constraint_type = 'PRIMARY KEY' + AND tc.table_schema = %s + AND tc.table_name = %s + ORDER BY kcu.position_in_unique_constraint", + $this->getConnection()->quote($parts['schema']), + $this->getConnection()->quote($parts['table']) + )); + + $primaryKey = [ + 'columns' => [], + ]; + foreach ($rows as $row) { + $primaryKey['constraint'] = $row['constraint_name']; + $primaryKey['columns'][] = $row['column_name']; + } + + return $primaryKey; + } + + /** + * @inheritDoc + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + $foreignKeys = $this->getForeignKeys($tableName); + if ($constraint) { + if (isset($foreignKeys[$constraint])) { + return !empty($foreignKeys[$constraint]); + } + + return false; + } + + foreach ($foreignKeys as $key) { + $a = array_diff($columns, $key['columns']); + if (empty($a)) { + return true; + } + } + + return false; + } + + /** + * Get an array of foreign keys from a particular table. + * + * @param string $tableName Table name + * @return array + */ + protected function getForeignKeys($tableName) + { + $parts = $this->getSchemaName($tableName); + $foreignKeys = []; + $rows = $this->fetchAll(sprintf( + "SELECT + tc.constraint_name, + tc.table_name, kcu.column_name, + ccu.table_name AS referenced_table_name, + ccu.column_name AS referenced_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + WHERE constraint_type = 'FOREIGN KEY' AND tc.table_schema = %s AND tc.table_name = %s + ORDER BY kcu.position_in_unique_constraint", + $this->getConnection()->quote($parts['schema']), + $this->getConnection()->quote($parts['table']) + )); + foreach ($rows as $row) { + $foreignKeys[$row['constraint_name']]['table'] = $row['table_name']; + $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name']; + $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name']; + $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name']; + } + + return $foreignKeys; + } + + /** + * @inheritDoc + */ + protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey) + { + $alter = sprintf( + 'ADD %s', + $this->getForeignKeySqlDefinition($foreignKey, $table->getName()) + ); + + return new AlterInstructions([$alter]); + } + + /** + * @inheritDoc + */ + protected function getDropForeignKeyInstructions($tableName, $constraint) + { + $alter = sprintf( + 'DROP CONSTRAINT %s', + $this->quoteColumnName($constraint) + ); + + return new AlterInstructions([$alter]); + } + + /** + * @inheritDoc + */ + protected function getDropForeignKeyByColumnsInstructions($tableName, $columns) + { + $instructions = new AlterInstructions(); + + $parts = $this->getSchemaName($tableName); + $sql = 'SELECT c.CONSTRAINT_NAME + FROM ( + SELECT CONSTRAINT_NAME, array_agg(COLUMN_NAME::varchar) as columns + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = %s + AND TABLE_NAME IS NOT NULL + AND TABLE_NAME = %s + AND POSITION_IN_UNIQUE_CONSTRAINT IS NOT NULL + GROUP BY CONSTRAINT_NAME + ) c + WHERE + ARRAY[%s]::varchar[] <@ c.columns AND + ARRAY[%s]::varchar[] @> c.columns'; + + $array = []; + foreach ($columns as $col) { + $array[] = "'$col'"; + } + + $rows = $this->fetchAll(sprintf( + $sql, + $this->getConnection()->quote($parts['schema']), + $this->getConnection()->quote($parts['table']), + implode(',', $array), + implode(',', $array) + )); + + foreach ($rows as $row) { + $newInstr = $this->getDropForeignKeyInstructions($tableName, $row['constraint_name']); + $instructions->merge($newInstr); + } + + return $instructions; + } + + /** + * {@inheritDoc} + * + * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException + */ + public function getSqlType($type, $limit = null) + { + switch ($type) { + case static::PHINX_TYPE_TEXT: + case static::PHINX_TYPE_TIME: + case static::PHINX_TYPE_DATE: + case static::PHINX_TYPE_BOOLEAN: + case static::PHINX_TYPE_JSON: + case static::PHINX_TYPE_JSONB: + case static::PHINX_TYPE_UUID: + case static::PHINX_TYPE_CIDR: + case static::PHINX_TYPE_INET: + case static::PHINX_TYPE_MACADDR: + case static::PHINX_TYPE_TIMESTAMP: + case static::PHINX_TYPE_INTEGER: + return ['name' => $type]; + case static::PHINX_TYPE_TINY_INTEGER: + return ['name' => 'smallint']; + case static::PHINX_TYPE_SMALL_INTEGER: + return ['name' => 'smallint']; + case static::PHINX_TYPE_DECIMAL: + return ['name' => $type, 'precision' => 18, 'scale' => 0]; + case static::PHINX_TYPE_DOUBLE: + return ['name' => 'double precision']; + case static::PHINX_TYPE_STRING: + return ['name' => 'character varying', 'limit' => 255]; + case static::PHINX_TYPE_CHAR: + return ['name' => 'character', 'limit' => 255]; + case static::PHINX_TYPE_BIG_INTEGER: + return ['name' => 'bigint']; + case static::PHINX_TYPE_FLOAT: + return ['name' => 'real']; + case static::PHINX_TYPE_DATETIME: + return ['name' => 'timestamp']; + case static::PHINX_TYPE_BINARYUUID: + return ['name' => 'uuid']; + case static::PHINX_TYPE_BLOB: + case static::PHINX_TYPE_BINARY: + return ['name' => 'bytea']; + case static::PHINX_TYPE_INTERVAL: + return ['name' => 'interval']; + // Geospatial database types + // Spatial storage in Postgres is done via the PostGIS extension, + // which enables the use of the "geography" type in combination + // with SRID 4326. + case static::PHINX_TYPE_GEOMETRY: + return ['name' => 'geography', 'type' => 'geometry', 'srid' => 4326]; + case static::PHINX_TYPE_POINT: + return ['name' => 'geography', 'type' => 'point', 'srid' => 4326]; + case static::PHINX_TYPE_LINESTRING: + return ['name' => 'geography', 'type' => 'linestring', 'srid' => 4326]; + case static::PHINX_TYPE_POLYGON: + return ['name' => 'geography', 'type' => 'polygon', 'srid' => 4326]; + default: + if ($this->isArrayType($type)) { + return ['name' => $type]; + } + // Return array type + throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by Postgresql.'); + } + } + + /** + * Returns Phinx type by SQL type + * + * @param string $sqlType SQL type + * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException + * @return string Phinx type + */ + public function getPhinxType($sqlType) + { + switch ($sqlType) { + case 'character varying': + case 'varchar': + return static::PHINX_TYPE_STRING; + case 'character': + case 'char': + return static::PHINX_TYPE_CHAR; + case 'text': + return static::PHINX_TYPE_TEXT; + case 'json': + return static::PHINX_TYPE_JSON; + case 'jsonb': + return static::PHINX_TYPE_JSONB; + case 'smallint': + return static::PHINX_TYPE_SMALL_INTEGER; + case 'int': + case 'int4': + case 'integer': + return static::PHINX_TYPE_INTEGER; + case 'decimal': + case 'numeric': + return static::PHINX_TYPE_DECIMAL; + case 'bigint': + case 'int8': + return static::PHINX_TYPE_BIG_INTEGER; + case 'real': + case 'float4': + return static::PHINX_TYPE_FLOAT; + case 'double precision': + return static::PHINX_TYPE_DOUBLE; + case 'bytea': + return static::PHINX_TYPE_BINARY; + case 'interval': + return static::PHINX_TYPE_INTERVAL; + case 'time': + case 'timetz': + case 'time with time zone': + case 'time without time zone': + return static::PHINX_TYPE_TIME; + case 'date': + return static::PHINX_TYPE_DATE; + case 'timestamp': + case 'timestamptz': + case 'timestamp with time zone': + case 'timestamp without time zone': + return static::PHINX_TYPE_DATETIME; + case 'bool': + case 'boolean': + return static::PHINX_TYPE_BOOLEAN; + case 'uuid': + return static::PHINX_TYPE_UUID; + case 'cidr': + return static::PHINX_TYPE_CIDR; + case 'inet': + return static::PHINX_TYPE_INET; + case 'macaddr': + return static::PHINX_TYPE_MACADDR; + default: + throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by Postgresql.'); + } + } + + /** + * @inheritDoc + */ + public function createDatabase($name, $options = []) + { + $charset = $options['charset'] ?? 'utf8'; + $this->execute(sprintf("CREATE DATABASE %s WITH ENCODING = '%s'", $name, $charset)); + } + + /** + * @inheritDoc + */ + public function hasDatabase($name) + { + $sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $name); + $result = $this->fetchRow($sql); + + return $result['count'] > 0; + } + + /** + * @inheritDoc + */ + public function dropDatabase($name) + { + $this->disconnect(); + $this->execute(sprintf('DROP DATABASE IF EXISTS %s', $name)); + $this->createdTables = []; + $this->connect(); + } + + /** + * Get the defintion for a `DEFAULT` statement. + * + * @param mixed $default default value + * @param string|null $columnType column type added + * @return string + */ + protected function getDefaultValueDefinition($default, $columnType = null) + { + if (is_string($default) && $default !== 'CURRENT_TIMESTAMP') { + $default = $this->getConnection()->quote($default); + } elseif (is_bool($default)) { + $default = $this->castToBool($default); + } elseif ($columnType === static::PHINX_TYPE_BOOLEAN) { + $default = $this->castToBool((bool)$default); + } + + return isset($default) ? 'DEFAULT ' . $default : ''; + } + + /** + * Gets the PostgreSQL Column Definition for a Column object. + * + * @param \Phinx\Db\Table\Column $column Column + * @return string + */ + protected function getColumnSqlDefinition(Column $column) + { + $buffer = []; + if ($column->isIdentity()) { + $buffer[] = $column->getType() === 'biginteger' ? 'BIGSERIAL' : 'SERIAL'; + } elseif ($column->getType() instanceof Literal) { + $buffer[] = (string)$column->getType(); + } else { + $sqlType = $this->getSqlType($column->getType(), $column->getLimit()); + $buffer[] = strtoupper($sqlType['name']); + + // integers cant have limits in postgres + if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && ($column->getPrecision() || $column->getScale())) { + $buffer[] = sprintf( + '(%s, %s)', + $column->getPrecision() ?: $sqlType['precision'], + $column->getScale() ?: $sqlType['scale'] + ); + } elseif ($sqlType['name'] === self::PHINX_TYPE_GEOMETRY) { + // geography type must be written with geometry type and srid, like this: geography(POLYGON,4326) + $buffer[] = sprintf( + '(%s,%s)', + strtoupper($sqlType['type']), + $column->getSrid() ?: $sqlType['srid'] + ); + } elseif (in_array($sqlType['name'], [self::PHINX_TYPE_TIME, self::PHINX_TYPE_TIMESTAMP], true)) { + if (is_numeric($column->getPrecision())) { + $buffer[] = sprintf('(%s)', $column->getPrecision()); + } + + if ($column->isTimezone()) { + $buffer[] = strtoupper('with time zone'); + } + } elseif ( + !in_array($column->getType(), [ + self::PHINX_TYPE_TINY_INTEGER, + self::PHINX_TYPE_SMALL_INTEGER, + self::PHINX_TYPE_INTEGER, + self::PHINX_TYPE_BIG_INTEGER, + self::PHINX_TYPE_BOOLEAN, + self::PHINX_TYPE_TEXT, + self::PHINX_TYPE_BINARY, + ], true) + ) { + if ($column->getLimit() || isset($sqlType['limit'])) { + $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']); + } + } + } + + $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL'; + + if ($column->getDefault() !== null) { + $buffer[] = $this->getDefaultValueDefinition($column->getDefault(), $column->getType()); + } + + return implode(' ', $buffer); + } + + /** + * Gets the PostgreSQL Column Comment Definition for a column object. + * + * @param \Phinx\Db\Table\Column $column Column + * @param string $tableName Table name + * @return string + */ + protected function getColumnCommentSqlDefinition(Column $column, $tableName) + { + // passing 'null' is to remove column comment + $comment = strcasecmp($column->getComment(), 'NULL') !== 0 + ? $this->getConnection()->quote($column->getComment()) + : 'NULL'; + + return sprintf( + 'COMMENT ON COLUMN %s.%s IS %s;', + $this->quoteTableName($tableName), + $this->quoteColumnName($column->getName()), + $comment + ); + } + + /** + * Gets the PostgreSQL Index Definition for an Index object. + * + * @param \Phinx\Db\Table\Index $index Index + * @param string $tableName Table name + * @return string + */ + protected function getIndexSqlDefinition(Index $index, $tableName) + { + $parts = $this->getSchemaName($tableName); + $columnNames = $index->getColumns(); + + if (is_string($index->getName())) { + $indexName = $index->getName(); + } else { + $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames)); + } + + $order = $index->getOrder() ?? []; + $columnNames = array_map(function ($columnName) use ($order) { + $ret = '"' . $columnName . '"'; + if (isset($order[$columnName])) { + $ret .= ' ' . $order[$columnName]; + } + + return $ret; + }, $columnNames); + + $includedColumns = $index->getInclude() ? sprintf('INCLUDE ("%s")', implode('","', $index->getInclude())) : ''; + + return sprintf( + 'CREATE %s INDEX %s ON %s (%s) %s;', + ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''), + $this->quoteColumnName($indexName), + $this->quoteTableName($tableName), + implode(',', $columnNames), + $includedColumns + ); + } + + /** + * Gets the MySQL Foreign Key Definition for an ForeignKey object. + * + * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key + * @param string $tableName Table name + * @return string + */ + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName) + { + $parts = $this->getSchemaName($tableName); + + $constraintName = $foreignKey->getConstraint() ?: ($parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey'); + $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) . + ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")' . + " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . + implode('", "', $foreignKey->getReferencedColumns()) . '")'; + if ($foreignKey->getOnDelete()) { + $def .= " ON DELETE {$foreignKey->getOnDelete()}"; + } + if ($foreignKey->getOnUpdate()) { + $def .= " ON UPDATE {$foreignKey->getOnUpdate()}"; + } + + return $def; + } + + /** + * @inheritDoc + */ + public function createSchemaTable() + { + // Create the public/custom schema if it doesn't already exist + if ($this->hasSchema($this->getGlobalSchemaName()) === false) { + $this->createSchema($this->getGlobalSchemaName()); + } + + $this->setSearchPath(); + + parent::createSchemaTable(); + } + + /** + * @inheritDoc + */ + public function getVersions() + { + $this->setSearchPath(); + + return parent::getVersions(); + } + + /** + * @inheritDoc + */ + public function getVersionLog() + { + $this->setSearchPath(); + + return parent::getVersionLog(); + } + + /** + * Creates the specified schema. + * + * @param string $schemaName Schema Name + * @return void + */ + public function createSchema($schemaName = 'public') + { + // from postgres 9.3 we can use "CREATE SCHEMA IF NOT EXISTS schema_name" + $sql = sprintf('CREATE SCHEMA IF NOT EXISTS %s', $this->quoteSchemaName($schemaName)); + $this->execute($sql); + } + + /** + * Checks to see if a schema exists. + * + * @param string $schemaName Schema Name + * @return bool + */ + public function hasSchema($schemaName) + { + $sql = sprintf( + 'SELECT count(*) + FROM pg_namespace + WHERE nspname = %s', + $this->getConnection()->quote($schemaName) + ); + $result = $this->fetchRow($sql); + + return $result['count'] > 0; + } + + /** + * Drops the specified schema table. + * + * @param string $schemaName Schema name + * @return void + */ + public function dropSchema($schemaName) + { + $sql = sprintf('DROP SCHEMA IF EXISTS %s CASCADE', $this->quoteSchemaName($schemaName)); + $this->execute($sql); + + foreach ($this->createdTables as $idx => $createdTable) { + if ($this->getSchemaName($createdTable)['schema'] === $this->quoteSchemaName($schemaName)) { + unset($this->createdTables[$idx]); + } + } + } + + /** + * Drops all schemas. + * + * @return void + */ + public function dropAllSchemas() + { + foreach ($this->getAllSchemas() as $schema) { + $this->dropSchema($schema); + } + } + + /** + * Returns schemas. + * + * @return array + */ + public function getAllSchemas() + { + $sql = "SELECT schema_name + FROM information_schema.schemata + WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'"; + $items = $this->fetchAll($sql); + $schemaNames = []; + foreach ($items as $item) { + $schemaNames[] = $item['schema_name']; + } + + return $schemaNames; + } + + /** + * @inheritDoc + */ + public function getColumnTypes() + { + return array_merge(parent::getColumnTypes(), static::$specificColumnTypes); + } + + /** + * @inheritDoc + */ + public function isValidColumnType(Column $column) + { + // If not a standard column type, maybe it is array type? + return parent::isValidColumnType($column) || $this->isArrayType($column->getType()); + } + + /** + * Check if the given column is an array of a valid type. + * + * @param string $columnType Column type + * @return bool + */ + protected function isArrayType($columnType) + { + if (!preg_match('/^([a-z]+)(?:\[\]){1,}$/', $columnType, $matches)) { + return false; + } + + $baseType = $matches[1]; + + return in_array($baseType, $this->getColumnTypes(), true); + } + + /** + * @param string $tableName Table name + * @return array + */ + protected function getSchemaName($tableName) + { + $schema = $this->getGlobalSchemaName(); + $table = $tableName; + if (strpos($tableName, '.') !== false) { + [$schema, $table] = explode('.', $tableName); + } + + return [ + 'schema' => $schema, + 'table' => $table, + ]; + } + + /** + * Gets the schema name. + * + * @return string + */ + protected function getGlobalSchemaName() + { + $options = $this->getOptions(); + + return empty($options['schema']) ? 'public' : $options['schema']; + } + + /** + * @inheritDoc + */ + public function castToBool($value) + { + return (bool)$value ? 'TRUE' : 'FALSE'; + } + + /** + * @inheritDoc + */ + public function getDecoratedConnection() + { + $options = $this->getOptions(); + $options = [ + 'username' => $options['user'] ?? null, + 'password' => $options['pass'] ?? null, + 'database' => $options['name'], + 'quoteIdentifiers' => true, + ] + $options; + + $driver = new PostgresDriver($options); + + $driver->setConnection($this->connection); + + return new Connection(['driver' => $driver] + $options); + } + + /** + * Sets search path of schemas to look through for a table + * + * @return void + */ + public function setSearchPath() + { + $this->execute( + sprintf( + 'SET search_path TO %s,"$user",public', + $this->quoteSchemaName($this->getGlobalSchemaName()) + ) + ); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/ProxyAdapter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/ProxyAdapter.php new file mode 100644 index 0000000..e41d9e2 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/ProxyAdapter.php @@ -0,0 +1,123 @@ + + */ +class ProxyAdapter extends AdapterWrapper +{ + /** + * @var \Phinx\Db\Action\Action[] + */ + protected $commands = []; + + /** + * @inheritDoc + */ + public function getAdapterType() + { + return 'ProxyAdapter'; + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []) + { + $this->commands[] = new CreateTable($table); + } + + /** + * @inheritDoc + */ + public function executeActions(Table $table, array $actions) + { + $this->commands = array_merge($this->commands, $actions); + } + + /** + * Gets an array of the recorded commands in reverse. + * + * @throws \Phinx\Migration\IrreversibleMigrationException if a command cannot be reversed. + * @return \Phinx\Db\Plan\Intent + */ + public function getInvertedCommands() + { + $inverted = new Intent(); + + foreach (array_reverse($this->commands) as $command) { + switch (true) { + case $command instanceof CreateTable: + $inverted->addAction(new DropTable($command->getTable())); + break; + + case $command instanceof RenameTable: + $inverted->addAction(new RenameTable(new Table($command->getNewName()), $command->getTable()->getName())); + break; + + case $command instanceof AddColumn: + $inverted->addAction(new RemoveColumn($command->getTable(), $command->getColumn())); + break; + + case $command instanceof RenameColumn: + $column = clone $command->getColumn(); + $name = $column->getName(); + $column->setName($command->getNewName()); + $inverted->addAction(new RenameColumn($command->getTable(), $column, $name)); + break; + + case $command instanceof AddIndex: + $inverted->addAction(new DropIndex($command->getTable(), $command->getIndex())); + break; + + case $command instanceof AddForeignKey: + $inverted->addAction(new DropForeignKey($command->getTable(), $command->getForeignKey())); + break; + + default: + throw new IrreversibleMigrationException(sprintf( + 'Cannot reverse a "%s" command', + get_class($command) + )); + } + } + + return $inverted; + } + + /** + * Execute the recorded commands in reverse. + * + * @return void + */ + public function executeInvertedCommands() + { + $plan = new Plan($this->getInvertedCommands()); + $plan->executeInverse($this->getAdapter()); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/SQLiteAdapter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/SQLiteAdapter.php new file mode 100644 index 0000000..b5ce231 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/SQLiteAdapter.php @@ -0,0 +1,1639 @@ + + * @author Richard McIntyre + */ +class SQLiteAdapter extends PdoAdapter +{ + public const MEMORY = ':memory:'; + + /** + * List of supported Phinx column types with their SQL equivalents + * some types have an affinity appended to ensure they do not receive NUMERIC affinity + * + * @var string[] + */ + protected static $supportedColumnTypes = [ + self::PHINX_TYPE_BIG_INTEGER => 'biginteger', + self::PHINX_TYPE_BINARY => 'binary_blob', + self::PHINX_TYPE_BINARYUUID => 'binary_blob', + self::PHINX_TYPE_BLOB => 'blob', + self::PHINX_TYPE_BOOLEAN => 'boolean_integer', + self::PHINX_TYPE_CHAR => 'char', + self::PHINX_TYPE_DATE => 'date_text', + self::PHINX_TYPE_DATETIME => 'datetime_text', + self::PHINX_TYPE_DECIMAL => 'decimal', + self::PHINX_TYPE_DOUBLE => 'double', + self::PHINX_TYPE_FLOAT => 'float', + self::PHINX_TYPE_INTEGER => 'integer', + self::PHINX_TYPE_JSON => 'json_text', + self::PHINX_TYPE_JSONB => 'jsonb_text', + self::PHINX_TYPE_SMALL_INTEGER => 'smallinteger', + self::PHINX_TYPE_STRING => 'varchar', + self::PHINX_TYPE_TEXT => 'text', + self::PHINX_TYPE_TIME => 'time_text', + self::PHINX_TYPE_TIMESTAMP => 'timestamp_text', + self::PHINX_TYPE_TINY_INTEGER => 'tinyinteger', + self::PHINX_TYPE_UUID => 'uuid_text', + self::PHINX_TYPE_VARBINARY => 'varbinary_blob', + ]; + + /** + * List of aliases of supported column types + * + * @var string[] + */ + protected static $supportedColumnTypeAliases = [ + 'varchar' => self::PHINX_TYPE_STRING, + 'tinyint' => self::PHINX_TYPE_TINY_INTEGER, + 'tinyinteger' => self::PHINX_TYPE_TINY_INTEGER, + 'smallint' => self::PHINX_TYPE_SMALL_INTEGER, + 'int' => self::PHINX_TYPE_INTEGER, + 'mediumint' => self::PHINX_TYPE_INTEGER, + 'mediuminteger' => self::PHINX_TYPE_INTEGER, + 'bigint' => self::PHINX_TYPE_BIG_INTEGER, + 'tinytext' => self::PHINX_TYPE_TEXT, + 'mediumtext' => self::PHINX_TYPE_TEXT, + 'longtext' => self::PHINX_TYPE_TEXT, + 'tinyblob' => self::PHINX_TYPE_BLOB, + 'mediumblob' => self::PHINX_TYPE_BLOB, + 'longblob' => self::PHINX_TYPE_BLOB, + 'real' => self::PHINX_TYPE_FLOAT, + ]; + + /** + * List of known but unsupported Phinx column types + * + * @var string[] + */ + protected static $unsupportedColumnTypes = [ + self::PHINX_TYPE_BIT, + self::PHINX_TYPE_CIDR, + self::PHINX_TYPE_DECIMAL, + self::PHINX_TYPE_ENUM, + self::PHINX_TYPE_FILESTREAM, + self::PHINX_TYPE_GEOMETRY, + self::PHINX_TYPE_INET, + self::PHINX_TYPE_INTERVAL, + self::PHINX_TYPE_LINESTRING, + self::PHINX_TYPE_MACADDR, + self::PHINX_TYPE_POINT, + self::PHINX_TYPE_POLYGON, + self::PHINX_TYPE_SET, + ]; + + /** + * @var string[] + */ + protected $definitionsWithLimits = [ + 'CHAR', + 'CHARACTER', + 'VARCHAR', + 'VARYING CHARACTER', + 'NCHAR', + 'NATIVE CHARACTER', + 'NVARCHAR', + ]; + + /** + * @var string + */ + protected $suffix = '.sqlite3'; + + /** + * Indicates whether the database library version is at least the specified version + * + * @param string $ver The version to check against e.g. '3.28.0' + * @return bool + */ + public function databaseVersionAtLeast($ver) + { + $actual = $this->query('SELECT sqlite_version()')->fetchColumn(); + + return version_compare($actual, $ver, '>='); + } + + /** + * {@inheritDoc} + * + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return void + */ + public function connect() + { + if ($this->connection === null) { + if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers(), true)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.'); + // @codeCoverageIgnoreEnd + } + + $options = $this->getOptions(); + + // use a memory database if the option was specified + if (!empty($options['memory']) || $options['name'] === static::MEMORY) { + $dsn = 'sqlite:' . static::MEMORY; + } else { + $dsn = 'sqlite:' . $options['name'] . $this->suffix; + } + + $driverOptions = []; + + // use custom data fetch mode + if (!empty($options['fetch_mode'])) { + $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode'])); + } + + $db = $this->createPdoConnection($dsn, null, null, $driverOptions); + + $this->setConnection($db); + } + } + + /** + * @inheritDoc + */ + public function setOptions(array $options) + { + parent::setOptions($options); + + if (isset($options['suffix'])) { + $this->suffix = $options['suffix']; + } + //don't "fix" the file extension if it is blank, some people + //might want a SQLITE db file with absolutely no extension. + if ($this->suffix !== '' && strpos($this->suffix, '.') !== 0) { + $this->suffix = '.' . $this->suffix; + } + + return $this; + } + + /** + * @inheritDoc + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * @inheritDoc + */ + public function hasTransactions() + { + return true; + } + + /** + * @inheritDoc + */ + public function beginTransaction() + { + $this->getConnection()->beginTransaction(); + } + + /** + * @inheritDoc + */ + public function commitTransaction() + { + $this->getConnection()->commit(); + } + + /** + * @inheritDoc + */ + public function rollbackTransaction() + { + $this->getConnection()->rollBack(); + } + + /** + * @inheritDoc + */ + public function quoteTableName($tableName) + { + return str_replace('.', '`.`', $this->quoteColumnName($tableName)); + } + + /** + * @inheritDoc + */ + public function quoteColumnName($columnName) + { + return '`' . str_replace('`', '``', $columnName) . '`'; + } + + /** + * @param string $tableName Table name + * @param bool $quoted Whether to return the schema name and table name escaped and quoted. If quoted, the schema (if any) will also be appended with a dot + * @return array + */ + protected function getSchemaName($tableName, $quoted = false) + { + if (preg_match("/.\.([^\.]+)$/", $tableName, $match)) { + $table = $match[1]; + $schema = substr($tableName, 0, strlen($tableName) - strlen($match[0]) + 1); + $result = ['schema' => $schema, 'table' => $table]; + } else { + $result = ['schema' => '', 'table' => $tableName]; + } + + if ($quoted) { + $result['schema'] = $result['schema'] !== '' ? $this->quoteColumnName($result['schema']) . '.' : ''; + $result['table'] = $this->quoteColumnName($result['table']); + } + + return $result; + } + + /** + * Retrieves information about a given table from one of the SQLite pragmas + * + * @param string $tableName The table to query + * @param string $pragma The pragma to query + * @return array + */ + protected function getTableInfo($tableName, $pragma = 'table_info') + { + $info = $this->getSchemaName($tableName, true); + + return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table'])); + } + + /** + * Searches through all available schemata to find a table and returns an array + * containing the bare schema name and whether the table exists at all. + * If no schema was specified and the table does not exist the "main" schema is returned + * + * @param string $tableName The name of the table to find + * @return array + */ + protected function resolveTable($tableName) + { + $info = $this->getSchemaName($tableName); + if ($info['schema'] === '') { + // if no schema is specified we search all schemata + $rows = $this->fetchAll('PRAGMA database_list;'); + // the temp schema is always first to be searched + $schemata = ['temp']; + foreach ($rows as $row) { + if (strtolower($row['name']) !== 'temp') { + $schemata[] = $row['name']; + } + } + $defaultSchema = 'main'; + } else { + // otherwise we search just the specified schema + $schemata = (array)$info['schema']; + $defaultSchema = $info['schema']; + } + + $table = strtolower($info['table']); + foreach ($schemata as $schema) { + if (strtolower($schema) === 'temp') { + $master = 'sqlite_temp_master'; + } else { + $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master'); + } + try { + $rows = $this->fetchAll(sprintf("SELECT name FROM %s WHERE type='table' AND lower(name) = %s", $master, $this->quoteString($table))); + } catch (PDOException $e) { + // an exception can occur if the schema part of the table refers to a database which is not attached + break; + } + + // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined, + // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII + foreach ($rows as $row) { + if (strtolower($row['name']) === $table) { + return ['schema' => $schema, 'table' => $row['name'], 'exists' => true]; + } + } + } + + return ['schema' => $defaultSchema, 'table' => $info['table'], 'exists' => false]; + } + + /** + * @inheritDoc + */ + public function hasTable($tableName) + { + return $this->hasCreatedTable($tableName) || $this->resolveTable($tableName)['exists']; + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []) + { + // Add the default primary key + $options = $table->getOptions(); + if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + $options['id'] = 'id'; + } + + if (isset($options['id']) && is_string($options['id'])) { + // Handle id => "field_name" to support AUTO_INCREMENT + $column = new Column(); + $column->setName($options['id']) + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + } + + $sql = 'CREATE TABLE '; + $sql .= $this->quoteTableName($table->getName()) . ' ('; + foreach ($columns as $column) { + $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', '; + + if (isset($options['primary_key']) && $column->getIdentity()) { + //remove column from the primary key array as it is already defined as an autoincrement + //primary id + $identityColumnIndex = array_search($column->getName(), $options['primary_key'], true); + if ($identityColumnIndex !== false) { + unset($options['primary_key'][$identityColumnIndex]); + + if (empty($options['primary_key'])) { + //The last primary key has been removed + unset($options['primary_key']); + } + } + } + } + + // set the primary key(s) + if (isset($options['primary_key'])) { + $sql = rtrim($sql); + $sql .= ' PRIMARY KEY ('; + if (is_string($options['primary_key'])) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($options['primary_key']); + } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') + $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key'])); + } + $sql .= ')'; + } else { + $sql = substr(rtrim($sql), 0, -1); // no primary keys + } + + $sql = rtrim($sql) . ');'; + // execute the sql + $this->execute($sql); + + foreach ($indexes as $index) { + $this->addIndex($table, $index); + } + + $this->addCreatedTable($table->getName()); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getChangePrimaryKeyInstructions(Table $table, $newColumns) + { + $instructions = new AlterInstructions(); + + // Drop the existing primary key + $primaryKey = $this->getPrimaryKey($table->getName()); + if (!empty($primaryKey)) { + $instructions->merge( + // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation + $this->getDropPrimaryKeyInstructions($table, $primaryKey[0]) + ); + } + + // Add the primary key(s) + if (!empty($newColumns)) { + if (!is_string($newColumns)) { + throw new InvalidArgumentException(sprintf( + 'Invalid value for primary key: %s', + json_encode($newColumns) + )); + } + + $instructions->merge( + $this->getAddPrimaryKeyInstructions($table, $newColumns) + ); + } + + return $instructions; + } + + /** + * {@inheritDoc} + * + * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used. + * + * @throws \BadMethodCallException + */ + protected function getChangeCommentInstructions(Table $table, $newComment) + { + throw new BadMethodCallException('SQLite does not have table comments'); + } + + /** + * @inheritDoc + */ + protected function getRenameTableInstructions($tableName, $newTableName) + { + $this->updateCreatedTableName($tableName, $newTableName); + $sql = sprintf( + 'ALTER TABLE %s RENAME TO %s', + $this->quoteTableName($tableName), + $this->quoteTableName($newTableName) + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + protected function getDropTableInstructions($tableName) + { + $this->removeCreatedTable($tableName); + $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName)); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + public function truncateTable($tableName) + { + $info = $this->resolveTable($tableName); + // first try deleting the rows + $this->execute(sprintf( + 'DELETE FROM %s.%s', + $this->quoteColumnName($info['schema']), + $this->quoteColumnName($info['table']) + )); + + // assuming no error occurred, reset the autoincrement (if any) + if ($this->hasTable($info['schema'] . '.sqlite_sequence')) { + $this->execute(sprintf( + 'DELETE FROM %s.%s where name = %s', + $this->quoteColumnName($info['schema']), + 'sqlite_sequence', + $this->quoteString($info['table']) + )); + } + } + + /** + * Parses a default-value expression to yield either a Literal representing + * a string value, a string representing an expression, or some other scalar + * + * @param mixed $v The default-value expression to interpret + * @param string $t The Phinx type of the column + * @return mixed + */ + protected function parseDefaultValue($v, $t) + { + if ($v === null) { + return null; + } + + // split the input into tokens + $trimChars = " \t\n\r\0\x0B"; + $pattern = <<getTableInfo($tableName) as $col) { + $type = strtolower($col['type']); + if ($col['pk'] > 1) { + // the table has a composite primary key + return null; + } elseif ($col['pk'] == 0) { + // the column is not a primary key column and is thus not relevant + continue; + } elseif ($type !== 'integer') { + // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias + return null; + } else { + // the column is a candidate for a row ID alias + $result = $col['name']; + } + } + // if there is no suitable PK column, stop now + if ($result === null) { + return null; + } + // make sure the table does not have a PK-origin autoindex + // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table + foreach ($this->getTableInfo($tableName, 'index_list') as $idx) { + if ($idx['origin'] === 'pk') { + return null; + } + } + + return $result; + } + + /** + * @inheritDoc + */ + public function getColumns($tableName) + { + $columns = []; + + $rows = $this->getTableInfo($tableName); + $identity = $this->resolveIdentity($tableName); + + foreach ($rows as $columnInfo) { + $column = new Column(); + $type = $this->getPhinxType($columnInfo['type']); + $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']); + + $column->setName($columnInfo['name']) + // SQLite on PHP 8.1 returns int for notnull, older versions return a string + ->setNull((int)$columnInfo['notnull'] !== 1) + ->setDefault($default) + ->setType($type['name']) + ->setLimit($type['limit']) + ->setScale($type['scale']) + ->setIdentity($columnInfo['name'] === $identity); + + $columns[] = $column; + } + + return $columns; + } + + /** + * @inheritDoc + */ + public function hasColumn($tableName, $columnName) + { + $rows = $this->getTableInfo($tableName); + foreach ($rows as $column) { + if (strcasecmp($column['name'], $columnName) === 0) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function getAddColumnInstructions(Table $table, Column $column) + { + $tableName = $table->getName(); + + $instructions = $this->beginAlterByCopyTable($tableName); + + $instructions->addPostStep(function ($state) use ($tableName, $column) { + // we use the final column to anchor our regex to insert the new column, + // as the alternative is unwinding all possible table constraints which + // gets messy quickly with CHECK constraints. + $columns = $this->getColumns($tableName); + if (!$columns) { + return $state; + } + $finalColumnName = end($columns)->getName(); + $sql = preg_replace( + sprintf( + "/(%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+)([,)])/", + $this->quoteColumnName($finalColumnName) + ), + sprintf( + '$1, %s %s$2', + $this->quoteColumnName($column->getName()), + $this->getColumnSqlDefinition($column) + ), + $state['createSQL'], + 1 + ); + $this->execute($sql); + + return $state; + }); + + $instructions->addPostStep(function ($state) use ($tableName) { + $newState = $this->calculateNewTableColumns($tableName, false, false); + + return $newState + $state; + }); + + return $this->copyAndDropTmpTable($instructions, $tableName); + } + + /** + * Returns the original CREATE statement for the give table + * + * @param string $tableName The table name to get the create statement for + * @return string + */ + protected function getDeclaringSql($tableName) + { + $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'table'"); + + $sql = ''; + foreach ($rows as $table) { + if ($table['tbl_name'] === $tableName) { + $sql = $table['sql']; + } + } + + return $sql; + } + + /** + * Returns the original CREATE statement for the give index + * + * @param string $tableName The table name to get the create statement for + * @param string $indexName The table index + * @return string + */ + protected function getDeclaringIndexSql($tableName, $indexName) + { + $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'index'"); + + $sql = ''; + foreach ($rows as $table) { + if ($table['tbl_name'] === $tableName && $table['name'] === $indexName) { + $sql = $table['sql'] . '; '; + } + } + + return $sql; + } + + /** + * Copies all the data from a tmp table to another table + * + * @param string $tableName The table name to copy the data to + * @param string $tmpTableName The tmp table name where the data is stored + * @param string[] $writeColumns The list of columns in the target table + * @param string[] $selectColumns The list of columns in the tmp table + * @return void + */ + protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns) + { + $sql = sprintf( + 'INSERT INTO %s(%s) SELECT %s FROM %s', + $this->quoteTableName($tableName), + implode(', ', $writeColumns), + implode(', ', $selectColumns), + $this->quoteTableName($tmpTableName) + ); + $this->execute($sql); + } + + /** + * Modifies the passed instructions to copy all data from the table into + * the provided tmp table and then drops the table and rename tmp table. + * + * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify + * @param string $tableName The table name to copy the data to + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function copyAndDropTmpTable($instructions, $tableName) + { + $instructions->addPostStep(function ($state) use ($tableName) { + $this->copyDataToNewTable( + $state['tmpTableName'], + $tableName, + $state['writeColumns'], + $state['selectColumns'] + ); + + $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName))); + $this->execute(sprintf( + 'ALTER TABLE %s RENAME TO %s', + $this->quoteTableName($state['tmpTableName']), + $this->quoteTableName($tableName) + )); + + return $state; + }); + + return $instructions; + } + + /** + * Returns the columns and type to use when copying a table to another in the process + * of altering a table + * + * @param string $tableName The table to modify + * @param string|false $columnName The column name that is about to change + * @param string|false $newColumnName Optionally the new name for the column + * @throws \InvalidArgumentException + * @return array + */ + protected function calculateNewTableColumns($tableName, $columnName, $newColumnName) + { + $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName))); + $selectColumns = []; + $writeColumns = []; + $columnType = null; + $found = false; + + foreach ($columns as $column) { + $selectName = $column['name']; + $writeName = $selectName; + + if ($selectName === $columnName) { + $writeName = $newColumnName; + $found = true; + $columnType = $column['type']; + $selectName = $newColumnName === false ? $newColumnName : $selectName; + } + + $selectColumns[] = $selectName; + $writeColumns[] = $writeName; + } + + $selectColumns = array_filter($selectColumns, 'strlen'); + $writeColumns = array_filter($writeColumns, 'strlen'); + $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns); + $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns); + + if ($columnName && !$found) { + throw new InvalidArgumentException(sprintf( + 'The specified column doesn\'t exist: ' . $columnName + )); + } + + return compact('writeColumns', 'selectColumns', 'columnType'); + } + + /** + * Returns the initial instructions to alter a table using the + * create-copy-drop strategy + * + * @param string $tableName The table to modify + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function beginAlterByCopyTable($tableName) + { + $instructions = new AlterInstructions(); + $instructions->addPostStep(function ($state) use ($tableName) { + $tmpTableName = "tmp_{$tableName}"; + $createSQL = $this->getDeclaringSql($tableName); + + // Table name in SQLite can be hilarious inside declaring SQL: + // - tableName + // - `tableName` + // - "tableName" + // - [this is a valid table name too!] + // - etc. + // Just remove all characters before first "(" and build them again + $createSQL = preg_replace( + "/^CREATE TABLE .* \(/Ui", + '', + $createSQL + ); + + $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}"; + + return compact('createSQL', 'tmpTableName') + $state; + }); + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName) + { + $instructions = $this->beginAlterByCopyTable($tableName); + + $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) { + $sql = str_replace( + $this->quoteColumnName($columnName), + $this->quoteColumnName($newColumnName), + $state['createSQL'] + ); + $this->execute($sql); + + return $state; + }); + + $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) { + $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName); + + return $newState + $state; + }); + + return $this->copyAndDropTmpTable($instructions, $tableName); + } + + /** + * @inheritDoc + */ + protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn) + { + $instructions = $this->beginAlterByCopyTable($tableName); + + $newColumnName = $newColumn->getName(); + $instructions->addPostStep(function ($state) use ($columnName, $newColumn) { + $sql = preg_replace( + sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)), + sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)), + $state['createSQL'], + 1 + ); + $this->execute($sql); + + return $state; + }); + + $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) { + $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName); + + return $newState + $state; + }); + + return $this->copyAndDropTmpTable($instructions, $tableName); + } + + /** + * @inheritDoc + */ + protected function getDropColumnInstructions($tableName, $columnName) + { + $instructions = $this->beginAlterByCopyTable($tableName); + + $instructions->addPostStep(function ($state) use ($tableName, $columnName) { + $newState = $this->calculateNewTableColumns($tableName, $columnName, false); + + return $newState + $state; + }); + + $instructions->addPostStep(function ($state) use ($columnName) { + $sql = preg_replace( + sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])), + '', + $state['createSQL'] + ); + + if (substr($sql, -2) === ', ') { + $sql = substr($sql, 0, -2) . ')'; + } + + $this->execute($sql); + + return $state; + }); + + return $this->copyAndDropTmpTable($instructions, $tableName); + } + + /** + * Get an array of indexes from a particular table. + * + * @param string $tableName Table name + * @return array + */ + protected function getIndexes($tableName) + { + $indexes = []; + $schema = $this->getSchemaName($tableName, true)['schema']; + $indexList = $this->getTableInfo($tableName, 'index_list'); + + foreach ($indexList as $index) { + $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name']))); + $cols = []; + foreach ($indexData as $indexItem) { + $cols[] = $indexItem['name']; + } + $indexes[$index['name']] = $cols; + } + + return $indexes; + } + + /** + * Finds the names of a table's indexes matching the supplied columns + * + * @param string $tableName The table to which the index belongs + * @param string|string[] $columns The columns of the index + * @return array + */ + protected function resolveIndex($tableName, $columns) + { + $columns = array_map('strtolower', (array)$columns); + $indexes = $this->getIndexes($tableName); + $matches = []; + + foreach ($indexes as $name => $index) { + $indexCols = array_map('strtolower', $index); + if ($columns == $indexCols) { + $matches[] = $name; + } + } + + return $matches; + } + + /** + * @inheritDoc + */ + public function hasIndex($tableName, $columns) + { + return (bool)$this->resolveIndex($tableName, $columns); + } + + /** + * @inheritDoc + */ + public function hasIndexByName($tableName, $indexName) + { + $indexName = strtolower($indexName); + $indexes = $this->getIndexes($tableName); + + foreach (array_keys($indexes) as $index) { + if ($indexName === strtolower($index)) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function getAddIndexInstructions(Table $table, Index $index) + { + $indexColumnArray = []; + foreach ($index->getColumns() as $column) { + $indexColumnArray[] = sprintf('`%s` ASC', $column); + } + $indexColumns = implode(',', $indexColumnArray); + $sql = sprintf( + 'CREATE %s ON %s (%s)', + $this->getIndexSqlDefinition($table, $index), + $this->quoteTableName($table->getName()), + $indexColumns + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + protected function getDropIndexByColumnsInstructions($tableName, $columns) + { + $instructions = new AlterInstructions(); + $indexNames = $this->resolveIndex($tableName, $columns); + $schema = $this->getSchemaName($tableName, true)['schema']; + foreach ($indexNames as $indexName) { + if (strpos($indexName, 'sqlite_autoindex_') !== 0) { + $instructions->addPostStep(sprintf( + 'DROP INDEX %s%s', + $schema, + $this->quoteColumnName($indexName) + )); + } + } + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getDropIndexByNameInstructions($tableName, $indexName) + { + $instructions = new AlterInstructions(); + $indexName = strtolower($indexName); + $indexes = $this->getIndexes($tableName); + + $found = false; + foreach (array_keys($indexes) as $index) { + if ($indexName === strtolower($index)) { + $found = true; + break; + } + } + + if ($found) { + $schema = $this->getSchemaName($tableName, true)['schema']; + $instructions->addPostStep(sprintf( + 'DROP INDEX %s%s', + $schema, + $this->quoteColumnName($indexName) + )); + } + + return $instructions; + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + public function hasPrimaryKey($tableName, $columns, $constraint = null) + { + if ($constraint !== null) { + throw new InvalidArgumentException('SQLite does not support named constraints.'); + } + + $columns = array_map('strtolower', (array)$columns); + $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName)); + + if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) { + return false; + } + + return true; + } + + /** + * Get the primary key from a particular table. + * + * @param string $tableName Table name + * @return string[] + */ + protected function getPrimaryKey($tableName) + { + $primaryKey = []; + + $rows = $this->getTableInfo($tableName); + + foreach ($rows as $row) { + if ($row['pk'] > 0) { + $primaryKey[$row['pk'] - 1] = $row['name']; + } + } + + return $primaryKey; + } + + /** + * @inheritDoc + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + if ($constraint !== null) { + return preg_match( + "/,?\sCONSTRAINT\s" . preg_quote($this->quoteColumnName($constraint)) . ' FOREIGN KEY/', + $this->getDeclaringSql($tableName) + ) === 1; + } + + $columns = array_map('strtolower', (array)$columns); + $foreignKeys = $this->getForeignKeys($tableName); + + foreach ($foreignKeys as $key) { + $key = array_map('strtolower', $key); + if (array_diff($key, $columns) || array_diff($columns, $key)) { + continue; + } + + return true; + } + + return false; + } + + /** + * Get an array of foreign keys from a particular table. + * + * @param string $tableName Table name + * @return array + */ + protected function getForeignKeys($tableName) + { + $foreignKeys = []; + + $rows = $this->getTableInfo($tableName, 'foreign_key_list'); + + foreach ($rows as $row) { + if (!isset($foreignKeys[$row['id']])) { + $foreignKeys[$row['id']] = []; + } + $foreignKeys[$row['id']][$row['seq']] = $row['from']; + } + + return $foreignKeys; + } + + /** + * @param \Phinx\Db\Table\Table $table The Table + * @param string $column Column Name + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function getAddPrimaryKeyInstructions(Table $table, $column) + { + $instructions = $this->beginAlterByCopyTable($table->getName()); + + $tableName = $table->getName(); + $instructions->addPostStep(function ($state) use ($column) { + $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/"; + + $sql = $state['createSQL']; + + if (preg_match($matchPattern, $state['createSQL'], $matches)) { + if (isset($matches[2])) { + if ($matches[2] === 'INTEGER') { + $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT'; + } else { + $replace = '$1 $2 NOT NULL PRIMARY KEY'; + } + + $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1); + } + } + + $this->execute($sql); + + return $state; + }); + + $instructions->addPostStep(function ($state) { + $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName']))); + $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name')); + $selectColumns = $writeColumns = $names; + + return compact('selectColumns', 'writeColumns') + $state; + }); + + return $this->copyAndDropTmpTable($instructions, $tableName); + } + + /** + * @param \Phinx\Db\Table\Table $table Table + * @param string $column Column Name + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function getDropPrimaryKeyInstructions($table, $column) + { + $instructions = $this->beginAlterByCopyTable($table->getName()); + + $instructions->addPostStep(function ($state) { + $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/"; + $sql = preg_replace($search, '', $state['createSQL'], 1); + + if ($sql) { + $this->execute($sql); + } + + return $state; + }); + + $instructions->addPostStep(function ($state) use ($column) { + $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column); + + return $newState + $state; + }); + + return $this->copyAndDropTmpTable($instructions, $table->getName()); + } + + /** + * @inheritDoc + */ + protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey) + { + $instructions = $this->beginAlterByCopyTable($table->getName()); + + $tableName = $table->getName(); + $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) { + $this->execute('pragma foreign_keys = ON'); + $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); '; + + //Delete indexes from original table and recreate them in temporary table + $schema = $this->getSchemaName($tableName, true)['schema']; + $tmpTableName = $state['tmpTableName']; + $indexes = $this->getIndexes($tableName); + foreach (array_keys($indexes) as $indexName) { + if (strpos($indexName, 'sqlite_autoindex_') !== 0) { + $sql .= sprintf( + 'DROP INDEX %s%s; ', + $schema, + $this->quoteColumnName($indexName) + ); + $createIndexSQL = $this->getDeclaringIndexSQL($tableName, $indexName); + $sql .= preg_replace( + "/\b${tableName}\b/", + $tmpTableName, + $createIndexSQL + ); + } + } + + $this->execute($sql); + + return $state; + }); + + $instructions->addPostStep(function ($state) { + $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName']))); + $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name')); + $selectColumns = $writeColumns = $names; + + return compact('selectColumns', 'writeColumns') + $state; + }); + + return $this->copyAndDropTmpTable($instructions, $tableName); + } + + /** + * {@inheritDoc} + * + * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used. + * + * @throws \BadMethodCallException + */ + protected function getDropForeignKeyInstructions($tableName, $constraint) + { + throw new BadMethodCallException('SQLite does not have named foreign keys'); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getDropForeignKeyByColumnsInstructions($tableName, $columns) + { + $instructions = $this->beginAlterByCopyTable($tableName); + + $instructions->addPostStep(function ($state) use ($columns) { + $sql = ''; + + foreach ($columns as $columnName) { + $search = sprintf( + "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/", + $this->quoteColumnName($columnName) + ); + $sql = preg_replace($search, '', $state['createSQL'], 1); + } + + if ($sql) { + $this->execute($sql); + } + + return $state; + }); + + $instructions->addPostStep(function ($state) use ($columns) { + $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]); + + $selectColumns = $newState['selectColumns']; + $columns = array_map([$this, 'quoteColumnName'], $columns); + $diff = array_diff($columns, $selectColumns); + + if (!empty($diff)) { + throw new InvalidArgumentException(sprintf( + 'The specified columns don\'t exist: ' . implode(', ', $diff) + )); + } + + return $newState + $state; + }); + + return $this->copyAndDropTmpTable($instructions, $tableName); + } + + /** + * {@inheritDoc} + * + * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException + */ + public function getSqlType($type, $limit = null) + { + $typeLC = strtolower($type); + if ($type instanceof Literal) { + $name = $type; + } elseif (isset(static::$supportedColumnTypes[$typeLC])) { + $name = static::$supportedColumnTypes[$typeLC]; + } elseif (in_array($typeLC, static::$unsupportedColumnTypes, true)) { + throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.'); + } else { + throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.'); + } + + return ['name' => $name, 'limit' => $limit]; + } + + /** + * Returns Phinx type by SQL type + * + * @param string|null $sqlTypeDef SQL Type definition + * @return array + */ + public function getPhinxType($sqlTypeDef) + { + $limit = null; + $scale = null; + if ($sqlTypeDef === null) { + // in SQLite columns can legitimately have null as a type, which is distinct from the empty string + $name = null; + } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) { + // doesn't match the pattern of a type we'd know about + $name = Literal::from($sqlTypeDef); + } else { + // possibly a known type + $type = $match[1]; + $typeLC = strtolower($type); + $affinity = $match[2] ?? ''; + $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null; + $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null; + if (in_array($typeLC, ['tinyint', 'tinyinteger'], true) && $limit === 1) { + // the type is a MySQL-style boolean + $name = static::PHINX_TYPE_BOOLEAN; + $limit = null; + } elseif (isset(static::$supportedColumnTypes[$typeLC])) { + // the type is an explicitly supported type + $name = $typeLC; + } elseif (isset(static::$supportedColumnTypeAliases[$typeLC])) { + // the type is an alias for a supported type + $name = static::$supportedColumnTypeAliases[$typeLC]; + } elseif (in_array($typeLC, static::$unsupportedColumnTypes, true)) { + // unsupported but known types are passed through lowercased, and without appended affinity + $name = Literal::from($typeLC); + } else { + // unknown types are passed through as-is + $name = Literal::from($type . $affinity); + } + } + + return [ + 'name' => $name, + 'limit' => $limit, + 'scale' => $scale, + ]; + } + + /** + * @inheritDoc + */ + public function createDatabase($name, $options = []) + { + touch($name . $this->suffix); + } + + /** + * @inheritDoc + */ + public function hasDatabase($name) + { + return is_file($name . $this->suffix); + } + + /** + * @inheritDoc + */ + public function dropDatabase($name) + { + $this->createdTables = []; + if ($this->getOption('memory')) { + $this->disconnect(); + $this->connect(); + } + if (file_exists($name . $this->suffix)) { + unlink($name . $this->suffix); + } + } + + /** + * Gets the SQLite Column Definition for a Column object. + * + * @param \Phinx\Db\Table\Column $column Column + * @return string + */ + protected function getColumnSqlDefinition(Column $column) + { + $isLiteralType = $column->getType() instanceof Literal; + if ($isLiteralType) { + $def = (string)$column->getType(); + } else { + $sqlType = $this->getSqlType($column->getType()); + $def = strtoupper($sqlType['name']); + + $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits, true); + if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) { + $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')'; + } + } + if ($column->getPrecision() && $column->getScale()) { + $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')'; + } + + $default = $column->getDefault(); + + $def .= $column->isNull() ? ' NULL' : ' NOT NULL'; + $def .= $this->getDefaultValueDefinition($default, $column->getType()); + $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : ''; + + $def .= $this->getCommentDefinition($column); + + return $def; + } + + /** + * Gets the comment Definition for a Column object. + * + * @param \Phinx\Db\Table\Column $column Column + * @return string + */ + protected function getCommentDefinition(Column $column) + { + if ($column->getComment()) { + return ' /* ' . $column->getComment() . ' */ '; + } + + return ''; + } + + /** + * Gets the SQLite Index Definition for an Index object. + * + * @param \Phinx\Db\Table\Table $table Table + * @param \Phinx\Db\Table\Index $index Index + * @return string + */ + protected function getIndexSqlDefinition(Table $table, Index $index) + { + if ($index->getType() === Index::UNIQUE) { + $def = 'UNIQUE INDEX'; + } else { + $def = 'INDEX'; + } + if (is_string($index->getName())) { + $indexName = $index->getName(); + } else { + $indexName = $table->getName() . '_'; + foreach ($index->getColumns() as $column) { + $indexName .= $column . '_'; + } + $indexName .= 'index'; + } + $def .= ' `' . $indexName . '`'; + + return $def; + } + + /** + * @inheritDoc + */ + public function getColumnTypes() + { + return array_keys(static::$supportedColumnTypes); + } + + /** + * Gets the SQLite Foreign Key Definition for an ForeignKey object. + * + * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key + * @return string + */ + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey) + { + $def = ''; + if ($foreignKey->getConstraint()) { + $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint()); + } + $columnNames = []; + foreach ($foreignKey->getColumns() as $column) { + $columnNames[] = $this->quoteColumnName($column); + } + $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')'; + $refColumnNames = []; + foreach ($foreignKey->getReferencedColumns() as $column) { + $refColumnNames[] = $this->quoteColumnName($column); + } + $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')'; + if ($foreignKey->getOnDelete()) { + $def .= ' ON DELETE ' . $foreignKey->getOnDelete(); + } + if ($foreignKey->getOnUpdate()) { + $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate(); + } + + return $def; + } + + /** + * @inheritDoc + */ + public function getDecoratedConnection() + { + $options = $this->getOptions(); + $options['quoteIdentifiers'] = true; + + if (!empty($options['name'])) { + $options['database'] = $options['name']; + + if (file_exists($options['name'] . $this->suffix)) { + $options['database'] = $options['name'] . $this->suffix; + } + } + + if ($this->connection === null) { + throw new RuntimeException('You need to connect first.'); + } + + $driver = new SqliteDriver($options); + $driver->setConnection($this->connection); + + return new Connection(['driver' => $driver] + $options); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/SqlServerAdapter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/SqlServerAdapter.php new file mode 100644 index 0000000..d975108 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/SqlServerAdapter.php @@ -0,0 +1,1353 @@ + + */ +class SqlServerAdapter extends PdoAdapter +{ + /** + * @var string[] + */ + protected static $specificColumnTypes = [ + self::PHINX_TYPE_FILESTREAM, + self::PHINX_TYPE_BINARYUUID, + ]; + + /** + * @var string + */ + protected $schema = 'dbo'; + + /** + * @var bool[] + */ + protected $signedColumnTypes = [ + self::PHINX_TYPE_INTEGER => true, + self::PHINX_TYPE_BIG_INTEGER => true, + self::PHINX_TYPE_FLOAT => true, + self::PHINX_TYPE_DECIMAL => true, + ]; + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + * @return void + */ + public function connect() + { + if ($this->connection === null) { + if (!class_exists('PDO') || !in_array('sqlsrv', PDO::getAvailableDrivers(), true)) { + // try our connection via freetds (Mac/Linux) + $this->connectDblib(); + + return; + } + + $options = $this->getOptions(); + + $dsn = 'sqlsrv:server=' . $options['host']; + // if port is specified use it, otherwise use the SqlServer default + if (!empty($options['port'])) { + $dsn .= ',' . $options['port']; + } + $dsn .= ';database=' . $options['name'] . ';MultipleActiveResultSets=false'; + + $driverOptions = []; + + // charset support + if (isset($options['charset'])) { + $driverOptions[PDO::SQLSRV_ATTR_ENCODING] = $options['charset']; + } + + // use custom data fetch mode + if (!empty($options['fetch_mode'])) { + $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode'])); + } + + // support arbitrary \PDO::SQLSRV_ATTR_* driver options and pass them to PDO + // http://php.net/manual/en/ref.pdo-sqlsrv.php#pdo-sqlsrv.constants + foreach ($options as $key => $option) { + if (strpos($key, 'sqlsrv_attr_') === 0) { + $pdoConstant = '\PDO::' . strtoupper($key); + if (!defined($pdoConstant)) { + throw new \UnexpectedValueException('Invalid PDO attribute: ' . $key . ' (' . $pdoConstant . ')'); + } + $driverOptions[constant($pdoConstant)] = $option; + } + } + + $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions); + + $this->setConnection($db); + } + } + + /** + * Connect to MSSQL using dblib/freetds. + * + * The "sqlsrv" driver is not available on Unix machines. + * + * @throws \InvalidArgumentException + * @throws \RuntimeException + * @return void + */ + protected function connectDblib() + { + if (!class_exists('PDO') || !in_array('dblib', PDO::getAvailableDrivers(), true)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('You need to enable the PDO_Dblib extension for Phinx to run properly.'); + // @codeCoverageIgnoreEnd + } + + $options = $this->getOptions(); + + // if port is specified use it, otherwise use the SqlServer default + if (empty($options['port'])) { + $dsn = 'dblib:host=' . $options['host'] . ';dbname=' . $options['name']; + } else { + $dsn = 'dblib:host=' . $options['host'] . ':' . $options['port'] . ';dbname=' . $options['name']; + } + + $driverOptions = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]; + + try { + $db = new PDO($dsn, $options['user'], $options['pass'], $driverOptions); + } catch (PDOException $exception) { + throw new InvalidArgumentException(sprintf( + 'There was a problem connecting to the database: %s', + $exception->getMessage() + )); + } + + $this->setConnection($db); + } + + /** + * @inheritDoc + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * @inheritDoc + */ + public function hasTransactions() + { + return true; + } + + /** + * @inheritDoc + */ + public function beginTransaction() + { + $this->execute('BEGIN TRANSACTION'); + } + + /** + * @inheritDoc + */ + public function commitTransaction() + { + $this->execute('COMMIT TRANSACTION'); + } + + /** + * @inheritDoc + */ + public function rollbackTransaction() + { + $this->execute('ROLLBACK TRANSACTION'); + } + + /** + * @inheritDoc + */ + public function quoteTableName($tableName) + { + return str_replace('.', '].[', $this->quoteColumnName($tableName)); + } + + /** + * @inheritDoc + */ + public function quoteColumnName($columnName) + { + return '[' . str_replace(']', '\]', $columnName) . ']'; + } + + /** + * @inheritDoc + */ + public function hasTable($tableName) + { + if ($this->hasCreatedTable($tableName)) { + return true; + } + + $result = $this->fetchRow(sprintf("SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = '%s';", $tableName)); + + return $result['count'] > 0; + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []) + { + $options = $table->getOptions(); + + // Add the default primary key + if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + $options['id'] = 'id'; + } + + if (isset($options['id']) && is_string($options['id'])) { + // Handle id => "field_name" to support AUTO_INCREMENT + $column = new Column(); + $column->setName($options['id']) + ->setType('integer') + ->setIdentity(true); + + array_unshift($columns, $column); + if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) { + throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key'); + } + $options['primary_key'] = $options['id']; + } + + $sql = 'CREATE TABLE '; + $sql .= $this->quoteTableName($table->getName()) . ' ('; + $sqlBuffer = []; + $columnsWithComments = []; + foreach ($columns as $column) { + $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column); + + // set column comments, if needed + if ($column->getComment()) { + $columnsWithComments[] = $column; + } + } + + // set the primary key(s) + if (isset($options['primary_key'])) { + $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName()); + if (is_string($options['primary_key'])) { // handle primary_key => 'id' + $pkSql .= $this->quoteColumnName($options['primary_key']); + } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') + $pkSql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key'])); + } + $pkSql .= ')'; + $sqlBuffer[] = $pkSql; + } + + $sql .= implode(', ', $sqlBuffer); + $sql .= ');'; + + // process column comments + foreach ($columnsWithComments as $column) { + $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName()); + } + + // set the indexes + foreach ($indexes as $index) { + $sql .= $this->getIndexSqlDefinition($index, $table->getName()); + } + + // execute the sql + $this->execute($sql); + + $this->addCreatedTable($table->getName()); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getChangePrimaryKeyInstructions(Table $table, $newColumns) + { + $instructions = new AlterInstructions(); + + // Drop the existing primary key + $primaryKey = $this->getPrimaryKey($table->getName()); + if (!empty($primaryKey['constraint'])) { + $sql = sprintf( + 'DROP CONSTRAINT %s', + $this->quoteColumnName($primaryKey['constraint']) + ); + $instructions->addAlter($sql); + } + + // Add the primary key(s) + if (!empty($newColumns)) { + $sql = sprintf( + 'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (', + $this->quoteTableName($table->getName()), + $this->quoteColumnName('PK_' . $table->getName()) + ); + if (is_string($newColumns)) { // handle primary_key => 'id' + $sql .= $this->quoteColumnName($newColumns); + } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id') + $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns)); + } else { + throw new InvalidArgumentException(sprintf( + 'Invalid value for primary key: %s', + json_encode($newColumns) + )); + } + $sql .= ')'; + $instructions->addPostStep($sql); + } + + return $instructions; + } + + /** + * @inheritDoc + * + * SqlServer does not implement this functionality, and so will always throw an exception if used. + * @throws \BadMethodCallException + */ + protected function getChangeCommentInstructions(Table $table, $newComment) + { + throw new BadMethodCallException('SqlServer does not have table comments'); + } + + /** + * Gets the SqlServer Column Comment Defininition for a column object. + * + * @param \Phinx\Db\Table\Column $column Column + * @param string $tableName Table name + * @return string + */ + protected function getColumnCommentSqlDefinition(Column $column, $tableName) + { + // passing 'null' is to remove column comment + $currentComment = $this->getColumnComment($tableName, $column->getName()); + + $comment = strcasecmp($column->getComment(), 'NULL') !== 0 ? $this->getConnection()->quote($column->getComment()) : '\'\''; + $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty'; + + return sprintf( + "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';", + $command, + $comment, + $this->schema, + $tableName, + $column->getName() + ); + } + + /** + * @inheritDoc + */ + protected function getRenameTableInstructions($tableName, $newTableName) + { + $this->updateCreatedTableName($tableName, $newTableName); + $sql = sprintf( + "EXEC sp_rename '%s', '%s'", + $tableName, + $newTableName + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + protected function getDropTableInstructions($tableName) + { + $this->removeCreatedTable($tableName); + $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName)); + + return new AlterInstructions([], [$sql]); + } + + /** + * @inheritDoc + */ + public function truncateTable($tableName) + { + $sql = sprintf( + 'TRUNCATE TABLE %s', + $this->quoteTableName($tableName) + ); + + $this->execute($sql); + } + + /** + * @param string $tableName Table name + * @param string $columnName Column name + * @return string|false + */ + public function getColumnComment($tableName, $columnName) + { + $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment + FROM sys.schemas + INNER JOIN sys.tables + ON schemas.schema_id = tables.schema_id + INNER JOIN sys.columns + ON tables.object_id = columns.object_id + INNER JOIN sys.extended_properties + ON tables.object_id = extended_properties.major_id + AND columns.column_id = extended_properties.minor_id + AND extended_properties.name = 'MS_Description' + WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName); + $row = $this->fetchRow($sql); + + if ($row) { + return trim($row['comment']); + } + + return false; + } + + /** + * @inheritDoc + */ + public function getColumns($tableName) + { + $columns = []; + $sql = sprintf( + "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type], + IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default], + CHARACTER_MAXIMUM_LENGTH AS [char_length], + NUMERIC_PRECISION AS [precision], + NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position], + COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity] + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = '%s' + ORDER BY ordinal_position", + $tableName + ); + $rows = $this->fetchAll($sql); + foreach ($rows as $columnInfo) { + try { + $type = $this->getPhinxType($columnInfo['type']); + } catch (UnsupportedColumnTypeException $e) { + $type = Literal::from($columnInfo['type']); + } + + $column = new Column(); + $column->setName($columnInfo['name']) + ->setType($type) + ->setNull($columnInfo['null'] !== 'NO') + ->setDefault($this->parseDefault($columnInfo['default'])) + ->setIdentity($columnInfo['identity'] === '1') + ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name'])); + + if (!empty($columnInfo['char_length'])) { + $column->setLimit($columnInfo['char_length']); + } + + $columns[$columnInfo['name']] = $column; + } + + return $columns; + } + + /** + * @param string $default Default + * @return int|string|null + */ + protected function parseDefault($default) + { + $result = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default); + + if (strtoupper($result) === 'NULL') { + $result = null; + } elseif (is_numeric($result)) { + $result = (int)$result; + } + + return $result; + } + + /** + * @inheritDoc + */ + public function hasColumn($tableName, $columnName) + { + $sql = sprintf( + "SELECT count(*) as [count] + FROM information_schema.columns + WHERE table_name = '%s' AND column_name = '%s'", + $tableName, + $columnName + ); + $result = $this->fetchRow($sql); + + return $result['count'] > 0; + } + + /** + * @inheritDoc + */ + protected function getAddColumnInstructions(Table $table, Column $column) + { + $alter = sprintf( + 'ALTER TABLE %s ADD %s %s', + $table->getName(), + $this->quoteColumnName($column->getName()), + $this->getColumnSqlDefinition($column) + ); + + return new AlterInstructions([], [$alter]); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName) + { + if (!$this->hasColumn($tableName, $columnName)) { + throw new InvalidArgumentException("The specified column does not exist: $columnName"); + } + + $instructions = new AlterInstructions(); + + $oldConstraintName = "DF_{$tableName}_{$columnName}"; + $newConstraintName = "DF_{$tableName}_{$newColumnName}"; + $sql = <<addPostStep(sprintf( + $sql, + $oldConstraintName, + $newConstraintName + )); + + $instructions->addPostStep(sprintf( + "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ", + $tableName, + $columnName, + $newColumnName + )); + + return $instructions; + } + + /** + * Returns the instructions to change a column default value + * + * @param string $tableName The table where the column is + * @param \Phinx\Db\Table\Column $newColumn The column to alter + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function getChangeDefault($tableName, Column $newColumn) + { + $constraintName = "DF_{$tableName}_{$newColumn->getName()}"; + $default = $newColumn->getDefault(); + $instructions = new AlterInstructions(); + + if ($default === null) { + $default = 'DEFAULT NULL'; + } else { + $default = ltrim($this->getDefaultValueDefinition($default)); + } + + if (empty($default)) { + return $instructions; + } + + $instructions->addPostStep(sprintf( + 'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s', + $this->quoteTableName($tableName), + $constraintName, + $default, + $this->quoteColumnName($newColumn->getName()) + )); + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn) + { + $columns = $this->getColumns($tableName); + $changeDefault = + $newColumn->getDefault() !== $columns[$columnName]->getDefault() || + $newColumn->getType() !== $columns[$columnName]->getType(); + + $instructions = new AlterInstructions(); + + if ($columnName !== $newColumn->getName()) { + $instructions->merge( + $this->getRenameColumnInstructions($tableName, $columnName, $newColumn->getName()) + ); + } + + if ($changeDefault) { + $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumn->getName())); + } + + $instructions->addPostStep(sprintf( + 'ALTER TABLE %s ALTER COLUMN %s %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($newColumn->getName()), + $this->getColumnSqlDefinition($newColumn, false) + )); + // change column comment if needed + if ($newColumn->getComment()) { + $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName)); + } + + if ($changeDefault) { + $instructions->merge($this->getChangeDefault($tableName, $newColumn)); + } + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getDropColumnInstructions($tableName, $columnName) + { + $instructions = $this->getDropDefaultConstraint($tableName, $columnName); + + $instructions->addPostStep(sprintf( + 'ALTER TABLE %s DROP COLUMN %s', + $this->quoteTableName($tableName), + $this->quoteColumnName($columnName) + )); + + return $instructions; + } + + /** + * @param string $tableName Table name + * @param string|null $columnName Column name + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function getDropDefaultConstraint($tableName, $columnName) + { + $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName); + + if (!$defaultConstraint) { + return new AlterInstructions(); + } + + return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint); + } + + /** + * @param string $tableName Table name + * @param string $columnName Column name + * @return string|false + */ + protected function getDefaultConstraint($tableName, $columnName) + { + $sql = "SELECT + default_constraints.name +FROM + sys.all_columns + + INNER JOIN + sys.tables + ON all_columns.object_id = tables.object_id + + INNER JOIN + sys.schemas + ON tables.schema_id = schemas.schema_id + + INNER JOIN + sys.default_constraints + ON all_columns.default_object_id = default_constraints.object_id + +WHERE + schemas.name = 'dbo' + AND tables.name = '{$tableName}' + AND all_columns.name = '{$columnName}'"; + + $rows = $this->fetchAll($sql); + + return empty($rows) ? false : $rows[0]['name']; + } + + /** + * @param int $tableId Table ID + * @param int $indexId Index ID + * @return array + */ + protected function getIndexColums($tableId, $indexId) + { + $sql = "SELECT AC.[name] AS [column_name] +FROM sys.[index_columns] IC + INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id] +WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId} AND IC.[object_id] = {$tableId} +ORDER BY IC.[key_ordinal];"; + + $rows = $this->fetchAll($sql); + $columns = []; + foreach ($rows as $row) { + $columns[] = strtolower($row['column_name']); + } + + return $columns; + } + + /** + * Get an array of indexes from a particular table. + * + * @param string $tableName Table name + * @return array + */ + public function getIndexes($tableName) + { + $indexes = []; + $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id] +FROM sys.[tables] AS T + INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id] +WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP' AND T.[name] = '{$tableName}' +ORDER BY T.[name], I.[index_id];"; + + $rows = $this->fetchAll($sql); + foreach ($rows as $row) { + $columns = $this->getIndexColums($row['table_id'], $row['index_id']); + $indexes[$row['index_name']] = ['columns' => $columns]; + } + + return $indexes; + } + + /** + * @inheritDoc + */ + public function hasIndex($tableName, $columns) + { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + + $columns = array_map('strtolower', $columns); + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $index) { + $a = array_diff($columns, $index['columns']); + + if (empty($a)) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function hasIndexByName($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function getAddIndexInstructions(Table $table, Index $index) + { + $sql = $this->getIndexSqlDefinition($index, $table->getName()); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getDropIndexByColumnsInstructions($tableName, $columns) + { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + + $indexes = $this->getIndexes($tableName); + $columns = array_map('strtolower', $columns); + $instructions = new AlterInstructions(); + + foreach ($indexes as $indexName => $index) { + $a = array_diff($columns, $index['columns']); + if (empty($a)) { + $instructions->addPostStep(sprintf( + 'DROP INDEX %s ON %s', + $this->quoteColumnName($indexName), + $this->quoteTableName($tableName) + )); + + return $instructions; + } + } + + throw new InvalidArgumentException(sprintf( + "The specified index on columns '%s' does not exist", + implode(',', $columns) + )); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + */ + protected function getDropIndexByNameInstructions($tableName, $indexName) + { + $indexes = $this->getIndexes($tableName); + $instructions = new AlterInstructions(); + + foreach ($indexes as $name => $index) { + if ($name === $indexName) { + $instructions->addPostStep(sprintf( + 'DROP INDEX %s ON %s', + $this->quoteColumnName($indexName), + $this->quoteTableName($tableName) + )); + + return $instructions; + } + } + + throw new InvalidArgumentException(sprintf( + "The specified index name '%s' does not exist", + $indexName + )); + } + + /** + * @inheritDoc + */ + public function hasPrimaryKey($tableName, $columns, $constraint = null) + { + $primaryKey = $this->getPrimaryKey($tableName); + + if (empty($primaryKey)) { + return false; + } + + if ($constraint) { + return $primaryKey['constraint'] === $constraint; + } + + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + $missingColumns = array_diff($columns, $primaryKey['columns']); + + return empty($missingColumns); + } + + /** + * Get the primary key from a particular table. + * + * @param string $tableName Table name + * @return array + */ + public function getPrimaryKey($tableName) + { + $rows = $this->fetchAll(sprintf( + "SELECT + tc.constraint_name, + kcu.column_name + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + WHERE constraint_type = 'PRIMARY KEY' + AND tc.table_name = '%s' + ORDER BY kcu.ordinal_position", + $tableName + )); + + $primaryKey = [ + 'columns' => [], + ]; + foreach ($rows as $row) { + $primaryKey['constraint'] = $row['constraint_name']; + $primaryKey['columns'][] = $row['column_name']; + } + + return $primaryKey; + } + + /** + * @inheritDoc + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + $foreignKeys = $this->getForeignKeys($tableName); + if ($constraint) { + if (isset($foreignKeys[$constraint])) { + return !empty($foreignKeys[$constraint]); + } + + return false; + } + + foreach ($foreignKeys as $key) { + $a = array_diff($columns, $key['columns']); + if (empty($a)) { + return true; + } + } + + return false; + } + + /** + * Get an array of foreign keys from a particular table. + * + * @param string $tableName Table name + * @return array + */ + protected function getForeignKeys($tableName) + { + $foreignKeys = []; + $rows = $this->fetchAll(sprintf( + "SELECT + tc.constraint_name, + tc.table_name, kcu.column_name, + ccu.table_name AS referenced_table_name, + ccu.column_name AS referenced_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' + ORDER BY kcu.ordinal_position", + $tableName + )); + foreach ($rows as $row) { + $foreignKeys[$row['constraint_name']]['table'] = $row['table_name']; + $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name']; + $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name']; + $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name']; + } + + return $foreignKeys; + } + + /** + * @inheritDoc + */ + protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey) + { + $instructions = new AlterInstructions(); + $instructions->addPostStep(sprintf( + 'ALTER TABLE %s ADD %s', + $this->quoteTableName($table->getName()), + $this->getForeignKeySqlDefinition($foreignKey, $table->getName()) + )); + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getDropForeignKeyInstructions($tableName, $constraint) + { + $instructions = new AlterInstructions(); + $instructions->addPostStep(sprintf( + 'ALTER TABLE %s DROP CONSTRAINT %s', + $this->quoteTableName($tableName), + $constraint + )); + + return $instructions; + } + + /** + * @inheritDoc + */ + protected function getDropForeignKeyByColumnsInstructions($tableName, $columns) + { + $instructions = new AlterInstructions(); + + foreach ($columns as $column) { + $rows = $this->fetchAll(sprintf( + "SELECT + tc.constraint_name, + tc.table_name, kcu.column_name, + ccu.table_name AS referenced_table_name, + ccu.column_name AS referenced_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s' + ORDER BY kcu.ordinal_position", + $tableName, + $column + )); + foreach ($rows as $row) { + $instructions->merge( + $this->getDropForeignKeyInstructions($tableName, $row['constraint_name']) + ); + } + } + + return $instructions; + } + + /** + * {@inheritDoc} + * + * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException + */ + public function getSqlType($type, $limit = null) + { + switch ($type) { + case static::PHINX_TYPE_FLOAT: + case static::PHINX_TYPE_DECIMAL: + case static::PHINX_TYPE_DATETIME: + case static::PHINX_TYPE_TIME: + case static::PHINX_TYPE_DATE: + return ['name' => $type]; + case static::PHINX_TYPE_STRING: + return ['name' => 'nvarchar', 'limit' => 255]; + case static::PHINX_TYPE_CHAR: + return ['name' => 'nchar', 'limit' => 255]; + case static::PHINX_TYPE_TEXT: + return ['name' => 'ntext']; + case static::PHINX_TYPE_INTEGER: + return ['name' => 'int']; + case static::PHINX_TYPE_TINY_INTEGER: + return ['name' => 'tinyint']; + case static::PHINX_TYPE_SMALL_INTEGER: + return ['name' => 'smallint']; + case static::PHINX_TYPE_BIG_INTEGER: + return ['name' => 'bigint']; + case static::PHINX_TYPE_TIMESTAMP: + return ['name' => 'datetime']; + case static::PHINX_TYPE_BLOB: + case static::PHINX_TYPE_BINARY: + return ['name' => 'varbinary']; + case static::PHINX_TYPE_BOOLEAN: + return ['name' => 'bit']; + case static::PHINX_TYPE_BINARYUUID: + case static::PHINX_TYPE_UUID: + return ['name' => 'uniqueidentifier']; + case static::PHINX_TYPE_FILESTREAM: + return ['name' => 'varbinary', 'limit' => 'max']; + // Geospatial database types + case static::PHINX_TYPE_GEOMETRY: + case static::PHINX_TYPE_POINT: + case static::PHINX_TYPE_LINESTRING: + case static::PHINX_TYPE_POLYGON: + // SQL Server stores all spatial data using a single data type. + // Specific types (point, polygon, etc) are set at insert time. + return ['name' => 'geography']; + default: + throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SqlServer.'); + } + } + + /** + * Returns Phinx type by SQL type + * + * @internal param string $sqlType SQL type + * @param string $sqlType SQL Type definition + * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException + * @return string Phinx type + */ + public function getPhinxType($sqlType) + { + switch ($sqlType) { + case 'nvarchar': + case 'varchar': + return static::PHINX_TYPE_STRING; + case 'char': + case 'nchar': + return static::PHINX_TYPE_CHAR; + case 'text': + case 'ntext': + return static::PHINX_TYPE_TEXT; + case 'int': + case 'integer': + return static::PHINX_TYPE_INTEGER; + case 'decimal': + case 'numeric': + case 'money': + return static::PHINX_TYPE_DECIMAL; + case 'tinyint': + return static::PHINX_TYPE_TINY_INTEGER; + case 'smallint': + return static::PHINX_TYPE_SMALL_INTEGER; + case 'bigint': + return static::PHINX_TYPE_BIG_INTEGER; + case 'real': + case 'float': + return static::PHINX_TYPE_FLOAT; + case 'binary': + case 'image': + case 'varbinary': + return static::PHINX_TYPE_BINARY; + case 'time': + return static::PHINX_TYPE_TIME; + case 'date': + return static::PHINX_TYPE_DATE; + case 'datetime': + case 'timestamp': + return static::PHINX_TYPE_DATETIME; + case 'bit': + return static::PHINX_TYPE_BOOLEAN; + case 'uniqueidentifier': + return static::PHINX_TYPE_UUID; + case 'filestream': + return static::PHINX_TYPE_FILESTREAM; + default: + throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by SqlServer.'); + } + } + + /** + * @inheritDoc + */ + public function createDatabase($name, $options = []) + { + if (isset($options['collation'])) { + $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation'])); + } else { + $this->execute(sprintf('CREATE DATABASE [%s]', $name)); + } + $this->execute(sprintf('USE [%s]', $name)); + } + + /** + * @inheritDoc + */ + public function hasDatabase($name) + { + $result = $this->fetchRow( + sprintf( + "SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = '%s'", + $name + ) + ); + + return $result['count'] > 0; + } + + /** + * @inheritDoc + */ + public function dropDatabase($name) + { + $sql = <<execute($sql); + $this->createdTables = []; + } + + /** + * Gets the SqlServer Column Definition for a Column object. + * + * @param \Phinx\Db\Table\Column $column Column + * @param bool $create Create column flag + * @return string + */ + protected function getColumnSqlDefinition(Column $column, $create = true) + { + $buffer = []; + if ($column->getType() instanceof Literal) { + $buffer[] = (string)$column->getType(); + } else { + $sqlType = $this->getSqlType($column->getType()); + $buffer[] = strtoupper($sqlType['name']); + // integers cant have limits in SQlServer + $noLimits = [ + 'bigint', + 'int', + 'tinyint', + 'smallint', + ]; + if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && $column->getPrecision() && $column->getScale()) { + $buffer[] = sprintf( + '(%s, %s)', + $column->getPrecision() ?: $sqlType['precision'], + $column->getScale() ?: $sqlType['scale'] + ); + } elseif (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) { + $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']); + } + } + + $properties = $column->getProperties(); + $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : ''; + $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : ''; + + $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL'; + + if ($create === true) { + if ($column->getDefault() === null && $column->isNull()) { + $buffer[] = ' DEFAULT NULL'; + } else { + $buffer[] = $this->getDefaultValueDefinition($column->getDefault()); + } + } + + if ($column->isIdentity()) { + $seed = $column->getSeed() ?: 1; + $increment = $column->getIncrement() ?: 1; + $buffer[] = sprintf('IDENTITY(%d,%d)', $seed, $increment); + } + + return implode(' ', $buffer); + } + + /** + * Gets the SqlServer Index Definition for an Index object. + * + * @param \Phinx\Db\Table\Index $index Index + * @param string $tableName Table name + * @return string + */ + protected function getIndexSqlDefinition(Index $index, $tableName) + { + $columnNames = $index->getColumns(); + if (is_string($index->getName())) { + $indexName = $index->getName(); + } else { + $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames)); + } + $order = $index->getOrder() ?? []; + $columnNames = array_map(function ($columnName) use ($order) { + $ret = '[' . $columnName . ']'; + if (isset($order[$columnName])) { + $ret .= ' ' . $order[$columnName]; + } + + return $ret; + }, $columnNames); + + $includedColumns = $index->getInclude() ? sprintf('INCLUDE ([%s])', implode('],[', $index->getInclude())) : ''; + + return sprintf( + 'CREATE %s INDEX %s ON %s (%s) %s;', + ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''), + $indexName, + $this->quoteTableName($tableName), + implode(',', $columnNames), + $includedColumns + ); + } + + /** + * Gets the SqlServer Foreign Key Definition for an ForeignKey object. + * + * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key + * @param string $tableName Table name + * @return string + */ + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName) + { + $constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns()); + $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName); + $def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")'; + $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")'; + if ($foreignKey->getOnDelete()) { + $def .= " ON DELETE {$foreignKey->getOnDelete()}"; + } + if ($foreignKey->getOnUpdate()) { + $def .= " ON UPDATE {$foreignKey->getOnUpdate()}"; + } + + return $def; + } + + /** + * @inheritDoc + */ + public function getColumnTypes() + { + return array_merge(parent::getColumnTypes(), static::$specificColumnTypes); + } + + /** + * Records a migration being run. + * + * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param string $direction Direction + * @param string $startTime Start Time + * @param string $endTime End Time + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime) + { + $startTime = str_replace(' ', 'T', $startTime); + $endTime = str_replace(' ', 'T', $endTime); + + return parent::migrated($migration, $direction, $startTime, $endTime); + } + + /** + * @inheritDoc + */ + public function getDecoratedConnection() + { + $options = $this->getOptions(); + $options = [ + 'username' => $options['user'] ?? null, + 'password' => $options['pass'] ?? null, + 'database' => $options['name'], + 'quoteIdentifiers' => true, + ] + $options; + + $driver = new SqlServerDriver($options); + $driver->setConnection($this->connection); + + return new Connection(['driver' => $driver] + $options); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/TablePrefixAdapter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/TablePrefixAdapter.php new file mode 100644 index 0000000..f0f366e --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/TablePrefixAdapter.php @@ -0,0 +1,482 @@ + + */ +class TablePrefixAdapter extends AdapterWrapper implements DirectActionInterface +{ + /** + * @inheritDoc + */ + public function getAdapterType() + { + return 'TablePrefixAdapter'; + } + + /** + * @inheritDoc + */ + public function hasTable($tableName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + + return parent::hasTable($adapterTableName); + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []) + { + $adapterTable = new Table( + $this->getAdapterTableName($table->getName()), + $table->getOptions() + ); + parent::createTable($adapterTable, $columns, $indexes); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changePrimaryKey(Table $table, $newColumns) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + + $adapterTable = new Table( + $this->getAdapterTableName($table->getName()), + $table->getOptions() + ); + $adapter->changePrimaryKey($adapterTable, $newColumns); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changeComment(Table $table, $newComment) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + + $adapterTable = new Table( + $this->getAdapterTableName($table->getName()), + $table->getOptions() + ); + $adapter->changeComment($adapterTable, $newComment); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function renameTable($tableName, $newTableName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + + $adapterTableName = $this->getAdapterTableName($tableName); + $adapterNewTableName = $this->getAdapterTableName($newTableName); + $adapter->renameTable($adapterTableName, $adapterNewTableName); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropTable($tableName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($tableName); + $adapter->dropTable($adapterTableName); + } + + /** + * @inheritDoc + */ + public function truncateTable($tableName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + parent::truncateTable($adapterTableName); + } + + /** + * @inheritDoc + */ + public function getColumns($tableName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + + return parent::getColumns($adapterTableName); + } + + /** + * @inheritDoc + */ + public function hasColumn($tableName, $columnName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + + return parent::hasColumn($adapterTableName, $columnName); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addColumn(Table $table, Column $column) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable = new Table($adapterTableName, $table->getOptions()); + $adapter->addColumn($adapterTable, $column); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($tableName); + $adapter->renameColumn($adapterTableName, $columnName, $newColumnName); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($tableName); + $adapter->changeColumn($adapterTableName, $columnName, $newColumn); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropColumn($tableName, $columnName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($tableName); + $adapter->dropColumn($adapterTableName, $columnName); + } + + /** + * @inheritDoc + */ + public function hasIndex($tableName, $columns) + { + $adapterTableName = $this->getAdapterTableName($tableName); + + return parent::hasIndex($adapterTableName, $columns); + } + + /** + * @inheritDoc + */ + public function hasIndexByName($tableName, $indexName) + { + $adapterTableName = $this->getAdapterTableName($tableName); + + return parent::hasIndexByName($adapterTableName, $indexName); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addIndex(Table $table, Index $index) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTable = new Table($table->getName(), $table->getOptions()); + $adapter->addIndex($adapterTable, $index); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropIndex($tableName, $columns) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($tableName); + $adapter->dropIndex($adapterTableName, $columns); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropIndexByName($tableName, $indexName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($tableName); + $adapter->dropIndexByName($adapterTableName, $indexName); + } + + /** + * @inheritDoc + */ + public function hasPrimaryKey($tableName, $columns, $constraint = null) + { + $adapterTableName = $this->getAdapterTableName($tableName); + + return parent::hasPrimaryKey($adapterTableName, $columns, $constraint); + } + + /** + * @inheritDoc + */ + public function hasForeignKey($tableName, $columns, $constraint = null) + { + $adapterTableName = $this->getAdapterTableName($tableName); + + return parent::hasForeignKey($adapterTableName, $columns, $constraint); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable = new Table($adapterTableName, $table->getOptions()); + $adapter->addForeignKey($adapterTable, $foreignKey); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The underlying adapter does not implement DirectActionInterface'); + } + $adapterTableName = $this->getAdapterTableName($tableName); + $adapter->dropForeignKey($adapterTableName, $columns, $constraint); + } + + /** + * @inheritDoc + */ + public function insert(Table $table, $row) + { + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable = new Table($adapterTableName, $table->getOptions()); + parent::insert($adapterTable, $row); + } + + /** + * @inheritDoc + */ + public function bulkinsert(Table $table, $rows) + { + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable = new Table($adapterTableName, $table->getOptions()); + parent::bulkinsert($adapterTable, $rows); + } + + /** + * Gets the table prefix. + * + * @return string + */ + public function getPrefix() + { + return (string)$this->getOption('table_prefix'); + } + + /** + * Gets the table suffix. + * + * @return string + */ + public function getSuffix() + { + return (string)$this->getOption('table_suffix'); + } + + /** + * Applies the prefix and suffix to the table name. + * + * @param string $tableName Table name + * @return string + */ + public function getAdapterTableName($tableName) + { + return $this->getPrefix() . $tableName . $this->getSuffix(); + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException + * @return void + */ + public function executeActions(Table $table, array $actions) + { + $adapterTableName = $this->getAdapterTableName($table->getName()); + $adapterTable = new Table($adapterTableName, $table->getOptions()); + + foreach ($actions as $k => $action) { + switch (true) { + case $action instanceof AddColumn: + $actions[$k] = new AddColumn($adapterTable, $action->getColumn()); + break; + + case $action instanceof AddIndex: + $actions[$k] = new AddIndex($adapterTable, $action->getIndex()); + break; + + case $action instanceof AddForeignKey: + $foreignKey = clone $action->getForeignKey(); + $refTable = $foreignKey->getReferencedTable(); + $refTableName = $this->getAdapterTableName($refTable->getName()); + $foreignKey->setReferencedTable(new Table($refTableName, $refTable->getOptions())); + $actions[$k] = new AddForeignKey($adapterTable, $foreignKey); + break; + + case $action instanceof ChangeColumn: + $actions[$k] = new ChangeColumn($adapterTable, $action->getColumnName(), $action->getColumn()); + break; + + case $action instanceof DropForeignKey: + $actions[$k] = new DropForeignKey($adapterTable, $action->getForeignKey()); + break; + + case $action instanceof DropIndex: + $actions[$k] = new DropIndex($adapterTable, $action->getIndex()); + break; + + case $action instanceof DropTable: + $actions[$k] = new DropTable($adapterTable); + break; + + case $action instanceof RemoveColumn: + $actions[$k] = new RemoveColumn($adapterTable, $action->getColumn()); + break; + + case $action instanceof RenameColumn: + $actions[$k] = new RenameColumn($adapterTable, $action->getColumn(), $action->getNewName()); + break; + + case $action instanceof RenameTable: + $actions[$k] = new RenameTable($adapterTable, $this->getAdapterTableName($action->getNewName())); + break; + + case $action instanceof ChangePrimaryKey: + $actions[$k] = new ChangePrimaryKey($adapterTable, $action->getNewColumns()); + break; + + case $action instanceof ChangeComment: + $actions[$k] = new ChangeComment($adapterTable, $action->getNewComment()); + break; + + default: + throw new InvalidArgumentException( + sprintf("Forgot to implement table prefixing for action: '%s'", get_class($action)) + ); + } + } + + parent::executeActions($adapterTable, $actions); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/TimedOutputAdapter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/TimedOutputAdapter.php new file mode 100644 index 0000000..dab3ac1 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/TimedOutputAdapter.php @@ -0,0 +1,423 @@ +getAdapter()->getAdapterType(); + } + + /** + * Start timing a command. + * + * @return callable A function that is to be called when the command finishes + */ + public function startCommandTimer() + { + $started = microtime(true); + + return function () use ($started) { + $end = microtime(true); + if (OutputInterface::VERBOSITY_VERBOSE <= $this->getOutput()->getVerbosity()) { + $this->getOutput()->writeln(' -> ' . sprintf('%.4fs', $end - $started)); + } + }; + } + + /** + * Write a Phinx command to the output. + * + * @param string $command Command Name + * @param array $args Command Args + * @return void + */ + public function writeCommand($command, $args = []) + { + if (OutputInterface::VERBOSITY_VERBOSE > $this->getOutput()->getVerbosity()) { + return; + } + + if (count($args)) { + $outArr = []; + foreach ($args as $arg) { + if (is_array($arg)) { + $arg = array_map( + function ($value) { + return '\'' . $value . '\''; + }, + $arg + ); + $outArr[] = '[' . implode(', ', $arg) . ']'; + continue; + } + + $outArr[] = '\'' . $arg . '\''; + } + $this->getOutput()->writeln(' -- ' . $command . '(' . implode(', ', $outArr) . ')'); + + return; + } + + $this->getOutput()->writeln(' -- ' . $command); + } + + /** + * @inheritDoc + */ + public function insert(Table $table, $row) + { + $end = $this->startCommandTimer(); + $this->writeCommand('insert', [$table->getName()]); + parent::insert($table, $row); + $end(); + } + + /** + * @inheritDoc + */ + public function bulkinsert(Table $table, $rows) + { + $end = $this->startCommandTimer(); + $this->writeCommand('bulkinsert', [$table->getName()]); + parent::bulkinsert($table, $rows); + $end(); + } + + /** + * @inheritDoc + */ + public function createTable(Table $table, array $columns = [], array $indexes = []) + { + $end = $this->startCommandTimer(); + $this->writeCommand('createTable', [$table->getName()]); + parent::createTable($table, $columns, $indexes); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changePrimaryKey(Table $table, $newColumns) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('changePrimaryKey', [$table->getName()]); + $adapter->changePrimaryKey($table, $newColumns); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changeComment(Table $table, $newComment) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('changeComment', [$table->getName()]); + $adapter->changeComment($table, $newComment); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function renameTable($tableName, $newTableName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('renameTable', [$tableName, $newTableName]); + $adapter->renameTable($tableName, $newTableName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropTable($tableName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropTable', [$tableName]); + $adapter->dropTable($tableName); + $end(); + } + + /** + * @inheritDoc + */ + public function truncateTable($tableName) + { + $end = $this->startCommandTimer(); + $this->writeCommand('truncateTable', [$tableName]); + parent::truncateTable($tableName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addColumn(Table $table, Column $column) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand( + 'addColumn', + [ + $table->getName(), + $column->getName(), + $column->getType(), + ] + ); + $adapter->addColumn($table, $column); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function renameColumn($tableName, $columnName, $newColumnName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('renameColumn', [$tableName, $columnName, $newColumnName]); + $adapter->renameColumn($tableName, $columnName, $newColumnName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function changeColumn($tableName, $columnName, Column $newColumn) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('changeColumn', [$tableName, $columnName, $newColumn->getType()]); + $adapter->changeColumn($tableName, $columnName, $newColumn); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropColumn($tableName, $columnName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropColumn', [$tableName, $columnName]); + $adapter->dropColumn($tableName, $columnName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addIndex(Table $table, Index $index) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('addIndex', [$table->getName(), $index->getColumns()]); + $adapter->addIndex($table, $index); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropIndex($tableName, $columns) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropIndex', [$tableName, $columns]); + $adapter->dropIndex($tableName, $columns); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropIndexByName($tableName, $indexName) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropIndexByName', [$tableName, $indexName]); + $adapter->dropIndexByName($tableName, $indexName); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function addForeignKey(Table $table, ForeignKey $foreignKey) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('addForeignKey', [$table->getName(), $foreignKey->getColumns()]); + $adapter->addForeignKey($table, $foreignKey); + $end(); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException + * @return void + */ + public function dropForeignKey($tableName, $columns, $constraint = null) + { + $adapter = $this->getAdapter(); + if (!$adapter instanceof DirectActionInterface) { + throw new BadMethodCallException('The adapter needs to implement DirectActionInterface'); + } + $end = $this->startCommandTimer(); + $this->writeCommand('dropForeignKey', [$tableName, $columns]); + $adapter->dropForeignKey($tableName, $columns, $constraint); + $end(); + } + + /** + * @inheritDoc + */ + public function createDatabase($name, $options = []) + { + $end = $this->startCommandTimer(); + $this->writeCommand('createDatabase', [$name]); + parent::createDatabase($name, $options); + $end(); + } + + /** + * @inheritDoc + */ + public function dropDatabase($name) + { + $end = $this->startCommandTimer(); + $this->writeCommand('dropDatabase', [$name]); + parent::dropDatabase($name); + $end(); + } + + /** + * @inheritDoc + */ + public function createSchema($name = 'public') + { + $end = $this->startCommandTimer(); + $this->writeCommand('createSchema', [$name]); + parent::createSchema($name); + $end(); + } + + /** + * @inheritDoc + */ + public function dropSchema($name) + { + $end = $this->startCommandTimer(); + $this->writeCommand('dropSchema', [$name]); + parent::dropSchema($name); + $end(); + } + + /** + * @inheritDoc + */ + public function executeActions(Table $table, array $actions) + { + $end = $this->startCommandTimer(); + $this->writeCommand(sprintf('Altering table %s', $table->getName())); + parent::executeActions($table, $actions); + $end(); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/UnsupportedColumnTypeException.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/UnsupportedColumnTypeException.php new file mode 100644 index 0000000..5ef75e9 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/UnsupportedColumnTypeException.php @@ -0,0 +1,19 @@ + + */ +class UnsupportedColumnTypeException extends RuntimeException +{ +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/WrapperInterface.php b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/WrapperInterface.php new file mode 100644 index 0000000..5c3fa01 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/WrapperInterface.php @@ -0,0 +1,39 @@ + + */ +interface WrapperInterface +{ + /** + * Class constructor, must always wrap another adapter. + * + * @param \Phinx\Db\Adapter\AdapterInterface $adapter Adapter + */ + public function __construct(AdapterInterface $adapter); + + /** + * Sets the database adapter to proxy commands to. + * + * @param \Phinx\Db\Adapter\AdapterInterface $adapter Adapter + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function setAdapter(AdapterInterface $adapter); + + /** + * Gets the database adapter. + * + * @throws \RuntimeException if the adapter has not been set + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function getAdapter(); +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Plan/AlterTable.php b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/AlterTable.php new file mode 100644 index 0000000..a2d8bdf --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/AlterTable.php @@ -0,0 +1,72 @@ +table = $table; + } + + /** + * Adds another action to the collection + * + * @param \Phinx\Db\Action\Action $action The action to add + * @return void + */ + public function addAction(Action $action) + { + $this->actions[] = $action; + } + + /** + * Returns the table associated to this collection + * + * @return \Phinx\Db\Table\Table + */ + public function getTable() + { + return $this->table; + } + + /** + * Returns an array with all collected actions + * + * @return \Phinx\Db\Action\Action[] + */ + public function getActions() + { + return $this->actions; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Intent.php b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Intent.php new file mode 100644 index 0000000..5545d6e --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Intent.php @@ -0,0 +1,55 @@ +actions[] = $action; + } + + /** + * Returns the full list of actions + * + * @return \Phinx\Db\Action\Action[] + */ + public function getActions() + { + return $this->actions; + } + + /** + * Merges another Intent object with this one + * + * @param \Phinx\Db\Plan\Intent $another The other intent to merge in + * @return void + */ + public function merge(Intent $another) + { + $this->actions = array_merge($this->actions, $another->getActions()); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Plan/NewTable.php b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/NewTable.php new file mode 100644 index 0000000..ca24872 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/NewTable.php @@ -0,0 +1,101 @@ +table = $table; + } + + /** + * Adds a column to the collection + * + * @param \Phinx\Db\Table\Column $column The column description + * @return void + */ + public function addColumn(Column $column) + { + $this->columns[] = $column; + } + + /** + * Adds an index to the collection + * + * @param \Phinx\Db\Table\Index $index The index description + * @return void + */ + public function addIndex(Index $index) + { + $this->indexes[] = $index; + } + + /** + * Returns the table object associated to this collection + * + * @return \Phinx\Db\Table\Table + */ + public function getTable() + { + return $this->table; + } + + /** + * Returns the columns collection + * + * @return \Phinx\Db\Table\Column[] + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Returns the indexes collection + * + * @return \Phinx\Db\Table\Index[] + */ + public function getIndexes() + { + return $this->indexes; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Plan.php b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Plan.php new file mode 100644 index 0000000..820d42d --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Plan.php @@ -0,0 +1,505 @@ +createPlan($intent->getActions()); + } + + /** + * Parses the given Intent and creates the separate steps to execute + * + * @param \Phinx\Db\Action\Action[] $actions The actions to use for the plan + * @return void + */ + protected function createPlan($actions) + { + $this->gatherCreates($actions); + $this->gatherUpdates($actions); + $this->gatherTableMoves($actions); + $this->gatherIndexes($actions); + $this->gatherConstraints($actions); + $this->resolveConflicts(); + } + + /** + * Returns a nested list of all the steps to execute + * + * @return \Phinx\Db\Plan\AlterTable[][] + */ + protected function updatesSequence() + { + return [ + $this->tableUpdates, + $this->constraints, + $this->indexes, + $this->columnRemoves, + $this->tableMoves, + ]; + } + + /** + * Returns a nested list of all the steps to execute in inverse order + * + * @return \Phinx\Db\Plan\AlterTable[][] + */ + protected function inverseUpdatesSequence() + { + return [ + $this->constraints, + $this->tableMoves, + $this->indexes, + $this->columnRemoves, + $this->tableUpdates, + ]; + } + + /** + * Executes this plan using the given AdapterInterface + * + * @param \Phinx\Db\Adapter\AdapterInterface $executor The executor object for the plan + * @return void + */ + public function execute(AdapterInterface $executor) + { + foreach ($this->tableCreates as $newTable) { + $executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes()); + } + + foreach ($this->updatesSequence() as $updates) { + foreach ($updates as $update) { + $executor->executeActions($update->getTable(), $update->getActions()); + } + } + } + + /** + * Executes the inverse plan (rollback the actions) with the given AdapterInterface:w + * + * @param \Phinx\Db\Adapter\AdapterInterface $executor The executor object for the plan + * @return void + */ + public function executeInverse(AdapterInterface $executor) + { + foreach ($this->inverseUpdatesSequence() as $updates) { + foreach ($updates as $update) { + $executor->executeActions($update->getTable(), $update->getActions()); + } + } + + foreach ($this->tableCreates as $newTable) { + $executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes()); + } + } + + /** + * Deletes certain actions from the plan if they are found to be conflicting or redundant. + * + * @return void + */ + protected function resolveConflicts() + { + foreach ($this->tableMoves as $alterTable) { + foreach ($alterTable->getActions() as $action) { + if ($action instanceof DropTable) { + $this->tableUpdates = $this->forgetTable($action->getTable(), $this->tableUpdates); + $this->constraints = $this->forgetTable($action->getTable(), $this->constraints); + $this->indexes = $this->forgetTable($action->getTable(), $this->indexes); + $this->columnRemoves = $this->forgetTable($action->getTable(), $this->columnRemoves); + } + } + } + + // Columns that are dropped will automatically cause the indexes to be dropped as well + foreach ($this->columnRemoves as $columnRemove) { + foreach ($columnRemove->getActions() as $action) { + if ($action instanceof RemoveColumn) { + [$this->indexes] = $this->forgetDropIndex( + $action->getTable(), + [$action->getColumn()->getName()], + $this->indexes + ); + } + } + } + + // Renaming a column and then changing the renamed column is something people do, + // but it is a conflicting action. Luckily solving the conflict can be done by moving + // the ChangeColumn action to another AlterTable. + $splitter = new ActionSplitter( + RenameColumn::class, + ChangeColumn::class, + function (RenameColumn $a, ChangeColumn $b) { + return $a->getNewName() === $b->getColumnName(); + } + ); + $tableUpdates = []; + foreach ($this->tableUpdates as $update) { + $tableUpdates = array_merge($tableUpdates, $splitter($update)); + } + $this->tableUpdates = $tableUpdates; + + // Dropping indexes used by foreign keys is a conflict, but one we can resolve + // if the foreign key is also scheduled to be dropped. If we can find such a a case, + // we force the execution of the index drop after the foreign key is dropped. + // Changing constraint properties sometimes require dropping it and then + // creating it again with the new stuff. Unfortunately, we have already bundled + // everything together in as few AlterTable statements as we could, so we need to + // resolve this conflict manually. + $splitter = new ActionSplitter( + DropForeignKey::class, + AddForeignKey::class, + function (DropForeignKey $a, AddForeignKey $b) { + return $a->getForeignKey()->getColumns() === $b->getForeignKey()->getColumns(); + } + ); + $constraints = []; + foreach ($this->constraints as $constraint) { + $constraints = array_merge( + $constraints, + $splitter($this->remapContraintAndIndexConflicts($constraint)) + ); + } + $this->constraints = $constraints; + } + + /** + * Deletes all actions related to the given table and keeps the + * rest + * + * @param \Phinx\Db\Table\Table $table The table to find in the list of actions + * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform + * @return \Phinx\Db\Plan\AlterTable[] The list of actions without actions for the given table + */ + protected function forgetTable(Table $table, $actions) + { + $result = []; + foreach ($actions as $action) { + if ($action->getTable()->getName() === $table->getName()) { + continue; + } + $result[] = $action; + } + + return $result; + } + + /** + * Finds all DropForeignKey actions in an AlterTable and moves + * all conflicting DropIndex action in `$this->indexes` into the + * given AlterTable. + * + * @param \Phinx\Db\Plan\AlterTable $alter The collection of actions to inspect + * @return \Phinx\Db\Plan\AlterTable The updated AlterTable object. This function + * has the side effect of changing the `$this->indexes` property. + */ + protected function remapContraintAndIndexConflicts(AlterTable $alter) + { + $newAlter = new AlterTable($alter->getTable()); + + foreach ($alter->getActions() as $action) { + $newAlter->addAction($action); + if ($action instanceof DropForeignKey) { + [$this->indexes, $dropIndexActions] = $this->forgetDropIndex( + $action->getTable(), + $action->getForeignKey()->getColumns(), + $this->indexes + ); + foreach ($dropIndexActions as $dropIndexAction) { + $newAlter->addAction($dropIndexAction); + } + } + } + + return $newAlter; + } + + /** + * Deletes any DropIndex actions for the given table and exact columns + * + * @param \Phinx\Db\Table\Table $table The table to find in the list of actions + * @param string[] $columns The column names to match + * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform + * @return array A tuple containing the list of actions without actions for dropping the index + * and a list of drop index actions that were removed. + */ + protected function forgetDropIndex(Table $table, array $columns, array $actions) + { + $dropIndexActions = new ArrayObject(); + $indexes = array_map(function ($alter) use ($table, $columns, $dropIndexActions) { + if ($alter->getTable()->getName() !== $table->getName()) { + return $alter; + } + + $newAlter = new AlterTable($table); + foreach ($alter->getActions() as $action) { + if ($action instanceof DropIndex && $action->getIndex()->getColumns() === $columns) { + $dropIndexActions->append($action); + } else { + $newAlter->addAction($action); + } + } + + return $newAlter; + }, $actions); + + return [$indexes, $dropIndexActions->getArrayCopy()]; + } + + /** + * Deletes any RemoveColumn actions for the given table and exact columns + * + * @param \Phinx\Db\Table\Table $table The table to find in the list of actions + * @param string[] $columns The column names to match + * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform + * @return array A tuple containing the list of actions without actions for removing the column + * and a list of remove column actions that were removed. + */ + protected function forgetRemoveColumn(Table $table, array $columns, array $actions) + { + $removeColumnActions = new ArrayObject(); + $indexes = array_map(function ($alter) use ($table, $columns, $removeColumnActions) { + if ($alter->getTable()->getName() !== $table->getName()) { + return $alter; + } + + $newAlter = new AlterTable($table); + foreach ($alter->getActions() as $action) { + if ($action instanceof RemoveColumn && in_array($action->getColumn()->getName(), $columns, true)) { + $removeColumnActions->append($action); + } else { + $newAlter->addAction($action); + } + } + + return $newAlter; + }, $actions); + + return [$indexes, $removeColumnActions->getArrayCopy()]; + } + + /** + * Collects all table creation actions from the given intent + * + * @param \Phinx\Db\Action\Action[] $actions The actions to parse + * @return void + */ + protected function gatherCreates($actions) + { + foreach ($actions as $action) { + if ($action instanceof CreateTable) { + $this->tableCreates[$action->getTable()->getName()] = new NewTable($action->getTable()); + } + } + + foreach ($actions as $action) { + if ( + ($action instanceof AddColumn || $action instanceof AddIndex) + && isset($this->tableCreates[$action->getTable()->getName()]) + ) { + $table = $action->getTable(); + + if ($action instanceof AddColumn) { + $this->tableCreates[$table->getName()]->addColumn($action->getColumn()); + } + + if ($action instanceof AddIndex) { + $this->tableCreates[$table->getName()]->addIndex($action->getIndex()); + } + } + } + } + + /** + * Collects all alter table actions from the given intent + * + * @param \Phinx\Db\Action\Action[] $actions The actions to parse + * @return void + */ + protected function gatherUpdates($actions) + { + foreach ($actions as $action) { + if ( + !($action instanceof AddColumn) + && !($action instanceof ChangeColumn) + && !($action instanceof RemoveColumn) + && !($action instanceof RenameColumn) + ) { + continue; + } elseif (isset($this->tableCreates[$action->getTable()->getName()])) { + continue; + } + $table = $action->getTable(); + $name = $table->getName(); + + if ($action instanceof RemoveColumn) { + if (!isset($this->columnRemoves[$name])) { + $this->columnRemoves[$name] = new AlterTable($table); + } + $this->columnRemoves[$name]->addAction($action); + } else { + if (!isset($this->tableUpdates[$name])) { + $this->tableUpdates[$name] = new AlterTable($table); + } + $this->tableUpdates[$name]->addAction($action); + } + } + } + + /** + * Collects all alter table drop and renames from the given intent + * + * @param \Phinx\Db\Action\Action[] $actions The actions to parse + * @return void + */ + protected function gatherTableMoves($actions) + { + foreach ($actions as $action) { + if ( + !($action instanceof DropTable) + && !($action instanceof RenameTable) + && !($action instanceof ChangePrimaryKey) + && !($action instanceof ChangeComment) + ) { + continue; + } + $table = $action->getTable(); + $name = $table->getName(); + + if (!isset($this->tableMoves[$name])) { + $this->tableMoves[$name] = new AlterTable($table); + } + + $this->tableMoves[$name]->addAction($action); + } + } + + /** + * Collects all index creation and drops from the given intent + * + * @param \Phinx\Db\Action\Action[] $actions The actions to parse + * @return void + */ + protected function gatherIndexes($actions) + { + foreach ($actions as $action) { + if (!($action instanceof AddIndex) && !($action instanceof DropIndex)) { + continue; + } elseif (isset($this->tableCreates[$action->getTable()->getName()])) { + continue; + } + + $table = $action->getTable(); + $name = $table->getName(); + + if (!isset($this->indexes[$name])) { + $this->indexes[$name] = new AlterTable($table); + } + + $this->indexes[$name]->addAction($action); + } + } + + /** + * Collects all foreign key creation and drops from the given intent + * + * @param \Phinx\Db\Action\Action[] $actions The actions to parse + * @return void + */ + protected function gatherConstraints($actions) + { + foreach ($actions as $action) { + if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) { + continue; + } + $table = $action->getTable(); + $name = $table->getName(); + + if (!isset($this->constraints[$name])) { + $this->constraints[$name] = new AlterTable($table); + } + + $this->constraints[$name]->addAction($action); + } + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Solver/ActionSplitter.php b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Solver/ActionSplitter.php new file mode 100644 index 0000000..6617c8a --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Plan/Solver/ActionSplitter.php @@ -0,0 +1,103 @@ +conflictClass = $conflictClass; + $this->conflictClassDual = $conflictClassDual; + $this->conflictFilter = $conflictFilter; + } + + /** + * Returs a sequence of AlterTable instructions that are non conflicting + * based on the constructor parameters. + * + * @param \Phinx\Db\Plan\AlterTable $alter The collection of actions to inspect + * @return \Phinx\Db\Plan\AlterTable[] A list of AlterTable that can be executed without + * this type of conflict + */ + public function __invoke(AlterTable $alter) + { + $conflictActions = array_filter($alter->getActions(), function ($action) { + return $action instanceof $this->conflictClass; + }); + + $originalAlter = new AlterTable($alter->getTable()); + $newAlter = new AlterTable($alter->getTable()); + + foreach ($alter->getActions() as $action) { + if (!$action instanceof $this->conflictClassDual) { + $originalAlter->addAction($action); + continue; + } + + $found = false; + $matches = $this->conflictFilter; + foreach ($conflictActions as $ca) { + if ($matches($ca, $action)) { + $found = true; + break; + } + } + + if ($found) { + $newAlter->addAction($action); + } else { + $originalAlter->addAction($action); + } + } + + return [$originalAlter, $newAlter]; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Table.php b/vendor/robmorgan/phinx/src/Phinx/Db/Table.php new file mode 100644 index 0000000..256a4c2 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Table.php @@ -0,0 +1,717 @@ +table = new TableValue($name, $options); + $this->actions = new Intent(); + + if ($adapter !== null) { + $this->setAdapter($adapter); + } + } + + /** + * Gets the table name. + * + * @return string|null + */ + public function getName() + { + return $this->table->getName(); + } + + /** + * Gets the table options. + * + * @return array + */ + public function getOptions() + { + return $this->table->getOptions(); + } + + /** + * Gets the table name and options as an object + * + * @return \Phinx\Db\Table\Table + */ + public function getTable() + { + return $this->table; + } + + /** + * Sets the database adapter. + * + * @param \Phinx\Db\Adapter\AdapterInterface $adapter Database Adapter + * @return $this + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * Gets the database adapter. + * + * @throws \RuntimeException + * @return \Phinx\Db\Adapter\AdapterInterface|null + */ + public function getAdapter() + { + if (!$this->adapter) { + throw new RuntimeException('There is no database adapter set yet, cannot proceed'); + } + + return $this->adapter; + } + + /** + * Does the table have pending actions? + * + * @return bool + */ + public function hasPendingActions() + { + return count($this->actions->getActions()) > 0 || count($this->data) > 0; + } + + /** + * Does the table exist? + * + * @return bool + */ + public function exists() + { + return $this->getAdapter()->hasTable($this->getName()); + } + + /** + * Drops the database table. + * + * @return $this + */ + public function drop() + { + $this->actions->addAction(new DropTable($this->table)); + + return $this; + } + + /** + * Renames the database table. + * + * @param string $newTableName New Table Name + * @return $this + */ + public function rename($newTableName) + { + $this->actions->addAction(new RenameTable($this->table, $newTableName)); + + return $this; + } + + /** + * Changes the primary key of the database table. + * + * @param string|string[]|null $columns Column name(s) to belong to the primary key, or null to drop the key + * @return $this + */ + public function changePrimaryKey($columns) + { + $this->actions->addAction(new ChangePrimaryKey($this->table, $columns)); + + return $this; + } + + /** + * Checks to see if a primary key exists. + * + * @param string|string[] $columns Column(s) + * @param string|null $constraint Constraint names + * @return bool + */ + public function hasPrimaryKey($columns, $constraint = null) + { + return $this->getAdapter()->hasPrimaryKey($this->getName(), $columns, $constraint); + } + + /** + * Changes the comment of the database table. + * + * @param string|null $comment New comment string, or null to drop the comment + * @return $this + */ + public function changeComment($comment) + { + $this->actions->addAction(new ChangeComment($this->table, $comment)); + + return $this; + } + + /** + * Gets an array of the table columns. + * + * @return \Phinx\Db\Table\Column[] + */ + public function getColumns() + { + return $this->getAdapter()->getColumns($this->getName()); + } + + /** + * Gets a table column if it exists. + * + * @param string $name Column name + * @return \Phinx\Db\Table\Column|null + */ + public function getColumn($name) + { + $columns = array_filter( + $this->getColumns(), + function ($column) use ($name) { + return $column->getName() === $name; + } + ); + + return array_pop($columns); + } + + /** + * Sets an array of data to be inserted. + * + * @param array $data Data + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } + + /** + * Gets the data waiting to be inserted. + * + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * Resets all of the pending data to be inserted + * + * @return void + */ + public function resetData() + { + $this->setData([]); + } + + /** + * Resets all of the pending table changes. + * + * @return void + */ + public function reset() + { + $this->actions = new Intent(); + $this->resetData(); + } + + /** + * Add a table column. + * + * Type can be: string, text, integer, float, decimal, datetime, timestamp, + * time, date, binary, boolean. + * + * Valid options can be: limit, default, null, precision or scale. + * + * @param string|\Phinx\Db\Table\Column $columnName Column Name + * @param string|\Phinx\Util\Literal|null $type Column Type + * @param array $options Column Options + * @throws \InvalidArgumentException + * @return $this + */ + public function addColumn($columnName, $type = null, $options = []) + { + if ($columnName instanceof Column) { + $action = new AddColumn($this->table, $columnName); + } else { + $action = AddColumn::build($this->table, $columnName, $type, $options); + } + + // Delegate to Adapters to check column type + if (!$this->getAdapter()->isValidColumnType($action->getColumn())) { + throw new InvalidArgumentException(sprintf( + 'An invalid column type "%s" was specified for column "%s".', + $type, + $action->getColumn()->getName() + )); + } + + $this->actions->addAction($action); + + return $this; + } + + /** + * Remove a table column. + * + * @param string $columnName Column Name + * @return $this + */ + public function removeColumn($columnName) + { + $action = RemoveColumn::build($this->table, $columnName); + $this->actions->addAction($action); + + return $this; + } + + /** + * Rename a table column. + * + * @param string $oldName Old Column Name + * @param string $newName New Column Name + * @return $this + */ + public function renameColumn($oldName, $newName) + { + $action = RenameColumn::build($this->table, $oldName, $newName); + $this->actions->addAction($action); + + return $this; + } + + /** + * Change a table column type. + * + * @param string $columnName Column Name + * @param string|\Phinx\Db\Table\Column|\Phinx\Util\Literal $newColumnType New Column Type + * @param array $options Options + * @return $this + */ + public function changeColumn($columnName, $newColumnType, array $options = []) + { + if ($newColumnType instanceof Column) { + $action = new ChangeColumn($this->table, $columnName, $newColumnType); + } else { + $action = ChangeColumn::build($this->table, $columnName, $newColumnType, $options); + } + $this->actions->addAction($action); + + return $this; + } + + /** + * Checks to see if a column exists. + * + * @param string $columnName Column Name + * @return bool + */ + public function hasColumn($columnName) + { + return $this->getAdapter()->hasColumn($this->getName(), $columnName); + } + + /** + * Add an index to a database table. + * + * In $options you can specific unique = true/false or name (index name). + * + * @param string|array|\Phinx\Db\Table\Index $columns Table Column(s) + * @param array $options Index Options + * @return $this + */ + public function addIndex($columns, array $options = []) + { + $action = AddIndex::build($this->table, $columns, $options); + $this->actions->addAction($action); + + return $this; + } + + /** + * Removes the given index from a table. + * + * @param string|string[] $columns Columns + * @return $this + */ + public function removeIndex($columns) + { + $action = DropIndex::build($this->table, is_string($columns) ? [$columns] : $columns); + $this->actions->addAction($action); + + return $this; + } + + /** + * Removes the given index identified by its name from a table. + * + * @param string $name Index name + * @return $this + */ + public function removeIndexByName($name) + { + $action = DropIndex::buildFromName($this->table, $name); + $this->actions->addAction($action); + + return $this; + } + + /** + * Checks to see if an index exists. + * + * @param string|string[] $columns Columns + * @return bool + */ + public function hasIndex($columns) + { + return $this->getAdapter()->hasIndex($this->getName(), $columns); + } + + /** + * Checks to see if an index specified by name exists. + * + * @param string $indexName Index name + * @return bool + */ + public function hasIndexByName($indexName) + { + return $this->getAdapter()->hasIndexByName($this->getName(), $indexName); + } + + /** + * Add a foreign key to a database table. + * + * In $options you can specify on_delete|on_delete = cascade|no_action .., + * on_update, constraint = constraint name. + * + * @param string|string[] $columns Columns + * @param string|\Phinx\Db\Table\Table $referencedTable Referenced Table + * @param string|string[] $referencedColumns Referenced Columns + * @param array $options Options + * @return $this + */ + public function addForeignKey($columns, $referencedTable, $referencedColumns = ['id'], $options = []) + { + $action = AddForeignKey::build($this->table, $columns, $referencedTable, $referencedColumns, $options); + $this->actions->addAction($action); + + return $this; + } + + /** + * Add a foreign key to a database table with a given name. + * + * In $options you can specify on_delete|on_delete = cascade|no_action .., + * on_update, constraint = constraint name. + * + * @param string $name The constraint name + * @param string|string[] $columns Columns + * @param string|\Phinx\Db\Table\Table $referencedTable Referenced Table + * @param string|string[] $referencedColumns Referenced Columns + * @param array $options Options + * @return $this + */ + public function addForeignKeyWithName($name, $columns, $referencedTable, $referencedColumns = ['id'], $options = []) + { + $action = AddForeignKey::build( + $this->table, + $columns, + $referencedTable, + $referencedColumns, + $options, + $name + ); + $this->actions->addAction($action); + + return $this; + } + + /** + * Removes the given foreign key from the table. + * + * @param string|string[] $columns Column(s) + * @param string|null $constraint Constraint names + * @return $this + */ + public function dropForeignKey($columns, $constraint = null) + { + $action = DropForeignKey::build($this->table, $columns, $constraint); + $this->actions->addAction($action); + + return $this; + } + + /** + * Checks to see if a foreign key exists. + * + * @param string|string[] $columns Column(s) + * @param string|null $constraint Constraint names + * @return bool + */ + public function hasForeignKey($columns, $constraint = null) + { + return $this->getAdapter()->hasForeignKey($this->getName(), $columns, $constraint); + } + + /** + * Add timestamp columns created_at and updated_at to the table. + * + * @param string|false|null $createdAt Alternate name for the created_at column + * @param string|false|null $updatedAt Alternate name for the updated_at column + * @param bool $withTimezone Whether to set the timezone option on the added columns + * @return $this + */ + public function addTimestamps($createdAt = 'created_at', $updatedAt = 'updated_at', $withTimezone = false) + { + $createdAt = $createdAt ?? 'created_at'; + $updatedAt = $updatedAt ?? 'updated_at'; + + if (!$createdAt && !$updatedAt) { + throw new \RuntimeException('Cannot set both created_at and updated_at columns to false'); + } + + if ($createdAt) { + $this->addColumn($createdAt, 'timestamp', [ + 'default' => 'CURRENT_TIMESTAMP', + 'update' => '', + 'timezone' => $withTimezone, + ]); + } + if ($updatedAt) { + $this->addColumn($updatedAt, 'timestamp', [ + 'null' => true, + 'default' => null, + 'update' => 'CURRENT_TIMESTAMP', + 'timezone' => $withTimezone, + ]); + } + + return $this; + } + + /** + * Alias that always sets $withTimezone to true + * + * @see addTimestamps + * @param string|null $createdAt Alternate name for the created_at column + * @param string|null $updatedAt Alternate name for the updated_at column + * @return $this + */ + public function addTimestampsWithTimezone($createdAt = null, $updatedAt = null) + { + $this->addTimestamps($createdAt, $updatedAt, true); + + return $this; + } + + /** + * Insert data into the table. + * + * @param array $data array of data in the form: + * array( + * array("col1" => "value1", "col2" => "anotherValue1"), + * array("col2" => "value2", "col2" => "anotherValue2"), + * ) + * or array("col1" => "value1", "col2" => "anotherValue1") + * @return $this + */ + public function insert($data) + { + // handle array of array situations + $keys = array_keys($data); + $firstKey = array_shift($keys); + if ($firstKey !== null && is_array($data[$firstKey])) { + foreach ($data as $row) { + $this->data[] = $row; + } + + return $this; + } + + if (count($data) > 0) { + $this->data[] = $data; + } + + return $this; + } + + /** + * Creates a table from the object instance. + * + * @return void + */ + public function create() + { + $this->executeActions(false); + $this->saveData(); + $this->reset(); // reset pending changes + } + + /** + * Updates a table from the object instance. + * + * @return void + */ + public function update() + { + $this->executeActions(true); + $this->saveData(); + $this->reset(); // reset pending changes + } + + /** + * Commit the pending data waiting for insertion. + * + * @return void + */ + public function saveData() + { + $rows = $this->getData(); + if (empty($rows)) { + return; + } + + $bulk = true; + $row = current($rows); + $c = array_keys($row); + foreach ($this->getData() as $row) { + $k = array_keys($row); + if ($k != $c) { + $bulk = false; + break; + } + } + + if ($bulk) { + $this->getAdapter()->bulkinsert($this->table, $this->getData()); + } else { + foreach ($this->getData() as $row) { + $this->getAdapter()->insert($this->table, $row); + } + } + + $this->resetData(); + } + + /** + * Immediately truncates the table. This operation cannot be undone + * + * @return void + */ + public function truncate() + { + $this->getAdapter()->truncateTable($this->getName()); + } + + /** + * Commits the table changes. + * + * If the table doesn't exist it is created otherwise it is updated. + * + * @return void + */ + public function save() + { + if ($this->exists()) { + $this->update(); // update the table + } else { + $this->create(); // create the table + } + } + + /** + * Executes all the pending actions for this table + * + * @param bool $exists Whether or not the table existed prior to executing this method + * @return void + */ + protected function executeActions($exists) + { + // Renaming a table is tricky, specially when running a reversible migration + // down. We will just assume the table already exists if the user commands a + // table rename. + if (!$exists) { + foreach ($this->actions->getActions() as $action) { + if ($action instanceof RenameTable) { + $exists = true; + break; + } + } + } + + // If the table does not exist, the last command in the chain needs to be + // a CreateTable action. + if (!$exists) { + $this->actions->addAction(new CreateTable($this->table)); + } + + $plan = new Plan($this->actions); + $plan->execute($this->getAdapter()); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Table/Column.php b/vendor/robmorgan/phinx/src/Phinx/Db/Table/Column.php new file mode 100644 index 0000000..542ce25 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Table/Column.php @@ -0,0 +1,756 @@ +name = $name; + + return $this; + } + + /** + * Gets the column name. + * + * @return string|null + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the column type. + * + * @param string|\Phinx\Util\Literal $type Column type + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Gets the column type. + * + * @return string|\Phinx\Util\Literal + */ + public function getType() + { + return $this->type; + } + + /** + * Sets the column limit. + * + * @param int $limit Limit + * @return $this + */ + public function setLimit($limit) + { + $this->limit = $limit; + + return $this; + } + + /** + * Gets the column limit. + * + * @return int + */ + public function getLimit() + { + return $this->limit; + } + + /** + * Sets whether the column allows nulls. + * + * @param bool $null Null + * @return $this + */ + public function setNull($null) + { + $this->null = (bool)$null; + + return $this; + } + + /** + * Gets whether the column allows nulls. + * + * @return bool + */ + public function getNull() + { + return $this->null; + } + + /** + * Does the column allow nulls? + * + * @return bool + */ + public function isNull() + { + return $this->getNull(); + } + + /** + * Sets the default column value. + * + * @param mixed $default Default + * @return $this + */ + public function setDefault($default) + { + $this->default = $default; + + return $this; + } + + /** + * Gets the default column value. + * + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * Sets whether or not the column is an identity column. + * + * @param bool $identity Identity + * @return $this + */ + public function setIdentity($identity) + { + $this->identity = $identity; + + return $this; + } + + /** + * Gets whether or not the column is an identity column. + * + * @return bool + */ + public function getIdentity() + { + return $this->identity; + } + + /** + * Is the column an identity column? + * + * @return bool + */ + public function isIdentity() + { + return $this->getIdentity(); + } + + /** + * Sets the name of the column to add this column after. + * + * @param string $after After + * @return $this + */ + public function setAfter($after) + { + $this->after = $after; + + return $this; + } + + /** + * Returns the name of the column to add this column after. + * + * @return string + */ + public function getAfter() + { + return $this->after; + } + + /** + * Sets the 'ON UPDATE' mysql column function. + * + * @param string $update On Update function + * @return $this + */ + public function setUpdate($update) + { + $this->update = $update; + + return $this; + } + + /** + * Returns the value of the ON UPDATE column function. + * + * @return string + */ + public function getUpdate() + { + return $this->update; + } + + /** + * Sets the number precision for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @param int $precision Number precision + * @return $this + */ + public function setPrecision($precision) + { + $this->setLimit($precision); + + return $this; + } + + /** + * Gets the number precision for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @return int + */ + public function getPrecision() + { + return $this->limit; + } + + /** + * Gets the column identity seed. + * + * @return int + */ + public function getSeed() + { + return $this->seed; + } + + /** + * Gets the column identity increment. + * + * @return int + */ + public function getIncrement() + { + return $this->increment; + } + + /** + * Sets the column identity seed. + * + * @param int $seed Number seed + * @return $this + */ + public function setSeed($seed) + { + $this->seed = $seed; + + return $this; + } + + /** + * Sets the column identity increment. + * + * @param int $increment Number increment + * @return $this + */ + public function setIncrement($increment) + { + $this->increment = $increment; + + return $this; + } + + /** + * Sets the number scale for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @param int $scale Number scale + * @return $this + */ + public function setScale($scale) + { + $this->scale = $scale; + + return $this; + } + + /** + * Gets the number scale for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @return int + */ + public function getScale() + { + return $this->scale; + } + + /** + * Sets the number precision and scale for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @param int $precision Number precision + * @param int $scale Number scale + * @return $this + */ + public function setPrecisionAndScale($precision, $scale) + { + $this->setLimit($precision); + $this->scale = $scale; + + return $this; + } + + /** + * Sets the column comment. + * + * @param string $comment Comment + * @return $this + */ + public function setComment($comment) + { + $this->comment = $comment; + + return $this; + } + + /** + * Gets the column comment. + * + * @return string + */ + public function getComment() + { + return $this->comment; + } + + /** + * Sets whether field should be signed. + * + * @param bool $signed Signed + * @return $this + */ + public function setSigned($signed) + { + $this->signed = (bool)$signed; + + return $this; + } + + /** + * Gets whether field should be signed. + * + * @return bool + */ + public function getSigned() + { + return $this->signed; + } + + /** + * Should the column be signed? + * + * @return bool + */ + public function isSigned() + { + return $this->getSigned(); + } + + /** + * Sets whether the field should have a timezone identifier. + * Used for date/time columns only! + * + * @param bool $timezone Timezone + * @return $this + */ + public function setTimezone($timezone) + { + $this->timezone = (bool)$timezone; + + return $this; + } + + /** + * Gets whether field has a timezone identifier. + * + * @return bool + */ + public function getTimezone() + { + return $this->timezone; + } + + /** + * Should the column have a timezone? + * + * @return bool + */ + public function isTimezone() + { + return $this->getTimezone(); + } + + /** + * Sets field properties. + * + * @param array $properties Properties + * @return $this + */ + public function setProperties($properties) + { + $this->properties = $properties; + + return $this; + } + + /** + * Gets field properties + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Sets field values. + * + * @param string[]|string $values Value(s) + * @return $this + */ + public function setValues($values) + { + if (!is_array($values)) { + $values = preg_split('/,\s*/', $values); + } + $this->values = $values; + + return $this; + } + + /** + * Gets field values + * + * @return array + */ + public function getValues() + { + return $this->values; + } + + /** + * Sets the column collation. + * + * @param string $collation Collation + * @return $this + */ + public function setCollation($collation) + { + $this->collation = $collation; + + return $this; + } + + /** + * Gets the column collation. + * + * @return string + */ + public function getCollation() + { + return $this->collation; + } + + /** + * Sets the column character set. + * + * @param string $encoding Encoding + * @return $this + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + + return $this; + } + + /** + * Gets the column character set. + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Sets the column SRID. + * + * @param int $srid SRID + * @return \Phinx\Db\Table\Column + */ + public function setSrid($srid) + { + $this->srid = $srid; + + return $this; + } + + /** + * Gets the column SRID. + * + * @return int|null + */ + public function getSrid() + { + return $this->srid; + } + + /** + * Gets all allowed options. Each option must have a corresponding `setFoo` method. + * + * @return array + */ + protected function getValidOptions() + { + return [ + 'limit', + 'default', + 'null', + 'identity', + 'scale', + 'after', + 'update', + 'comment', + 'signed', + 'timezone', + 'properties', + 'values', + 'collation', + 'encoding', + 'srid', + 'seed', + 'increment', + ]; + } + + /** + * Gets all aliased options. Each alias must reference a valid option. + * + * @return array + */ + protected function getAliasedOptions() + { + return [ + 'length' => 'limit', + 'precision' => 'limit', + ]; + } + + /** + * Utility method that maps an array of column options to this objects methods. + * + * @param array $options Options + * @throws \RuntimeException + * @return $this + */ + public function setOptions($options) + { + $validOptions = $this->getValidOptions(); + $aliasOptions = $this->getAliasedOptions(); + + foreach ($options as $option => $value) { + if (isset($aliasOptions[$option])) { + // proxy alias -> option + $option = $aliasOptions[$option]; + } + + if (!in_array($option, $validOptions, true)) { + throw new RuntimeException(sprintf('"%s" is not a valid column option.', $option)); + } + + $method = 'set' . ucfirst($option); + $this->$method($value); + } + + return $this; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Table/ForeignKey.php b/vendor/robmorgan/phinx/src/Phinx/Db/Table/ForeignKey.php new file mode 100644 index 0000000..4705f3c --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Table/ForeignKey.php @@ -0,0 +1,234 @@ +columns = is_string($columns) ? [$columns] : $columns; + + return $this; + } + + /** + * Gets the foreign key columns. + * + * @return string[] + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Sets the foreign key referenced table. + * + * @param \Phinx\Db\Table\Table $table The table this KEY is pointing to + * @return $this + */ + public function setReferencedTable(Table $table) + { + $this->referencedTable = $table; + + return $this; + } + + /** + * Gets the foreign key referenced table. + * + * @return \Phinx\Db\Table\Table + */ + public function getReferencedTable() + { + return $this->referencedTable; + } + + /** + * Sets the foreign key referenced columns. + * + * @param string[] $referencedColumns Referenced columns + * @return $this + */ + public function setReferencedColumns(array $referencedColumns) + { + $this->referencedColumns = $referencedColumns; + + return $this; + } + + /** + * Gets the foreign key referenced columns. + * + * @return string[] + */ + public function getReferencedColumns() + { + return $this->referencedColumns; + } + + /** + * Sets ON DELETE action for the foreign key. + * + * @param string $onDelete On Delete + * @return $this + */ + public function setOnDelete($onDelete) + { + $this->onDelete = $this->normalizeAction($onDelete); + + return $this; + } + + /** + * Gets ON DELETE action for the foreign key. + * + * @return string + */ + public function getOnDelete() + { + return $this->onDelete; + } + + /** + * Gets ON UPDATE action for the foreign key. + * + * @return string + */ + public function getOnUpdate() + { + return $this->onUpdate; + } + + /** + * Sets ON UPDATE action for the foreign key. + * + * @param string $onUpdate On Update + * @return $this + */ + public function setOnUpdate($onUpdate) + { + $this->onUpdate = $this->normalizeAction($onUpdate); + + return $this; + } + + /** + * Sets constraint for the foreign key. + * + * @param string $constraint Constraint + * @return $this + */ + public function setConstraint($constraint) + { + $this->constraint = $constraint; + + return $this; + } + + /** + * Gets constraint name for the foreign key. + * + * @return string|null + */ + public function getConstraint() + { + return $this->constraint; + } + + /** + * Utility method that maps an array of index options to this objects methods. + * + * @param array $options Options + * @throws \RuntimeException + * @return $this + */ + public function setOptions($options) + { + // Valid Options + $validOptions = ['delete', 'update', 'constraint']; + foreach ($options as $option => $value) { + if (!in_array($option, $validOptions, true)) { + throw new RuntimeException(sprintf('"%s" is not a valid foreign key option.', $option)); + } + + // handle $options['delete'] as $options['update'] + if ($option === 'delete') { + $this->setOnDelete($value); + } elseif ($option === 'update') { + $this->setOnUpdate($value); + } else { + $method = 'set' . ucfirst($option); + $this->$method($value); + } + } + + return $this; + } + + /** + * From passed value checks if it's correct and fixes if needed + * + * @param string $action Action + * @throws \InvalidArgumentException + * @return string + */ + protected function normalizeAction($action) + { + $constantName = 'static::' . str_replace(' ', '_', strtoupper(trim($action))); + if (!defined($constantName)) { + throw new InvalidArgumentException('Unknown action passed: ' . $action); + } + + return constant($constantName); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Table/Index.php b/vendor/robmorgan/phinx/src/Phinx/Db/Table/Index.php new file mode 100644 index 0000000..7753e36 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Table/Index.php @@ -0,0 +1,227 @@ +columns = $columns; + + return $this; + } + + /** + * Gets the index columns. + * + * @return string[] + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Sets the index type. + * + * @param string $type Type + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Gets the index type. + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Sets the index name. + * + * @param string $name Name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Gets the index name. + * + * @return string|null + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the index limit. + * + * @param int|array $limit limit value or array of limit value + * @return $this + */ + public function setLimit($limit) + { + $this->limit = $limit; + + return $this; + } + + /** + * Gets the index limit. + * + * @return int|array + */ + public function getLimit() + { + return $this->limit; + } + + /** + * Sets the index columns sort order. + * + * @param string[] $order column name sort order key value pair + * @return $this + */ + public function setOrder($order) + { + $this->order = $order; + + return $this; + } + + /** + * Gets the index columns sort order. + * + * @return string[] + */ + public function getOrder() + { + return $this->order; + } + + /** + * Sets the index included columns. + * + * @param string[] $includedColumns Columns + * @return $this + */ + public function setInclude($includedColumns) + { + $this->includedColumns = $includedColumns; + + return $this; + } + + /** + * Gets the index included columns. + * + * @return string[] + */ + public function getInclude() + { + return $this->includedColumns; + } + + /** + * Utility method that maps an array of index options to this objects methods. + * + * @param array $options Options + * @throws \RuntimeException + * @return $this + */ + public function setOptions($options) + { + // Valid Options + $validOptions = ['type', 'unique', 'name', 'limit', 'order', 'include']; + foreach ($options as $option => $value) { + if (!in_array($option, $validOptions, true)) { + throw new RuntimeException(sprintf('"%s" is not a valid index option.', $option)); + } + + // handle $options['unique'] + if (strcasecmp($option, self::UNIQUE) === 0) { + if ((bool)$value) { + $this->setType(self::UNIQUE); + } + continue; + } + + $method = 'set' . ucfirst($option); + $this->$method($value); + } + + return $this; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Table/Table.php b/vendor/robmorgan/phinx/src/Phinx/Db/Table/Table.php new file mode 100644 index 0000000..328abd2 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Table/Table.php @@ -0,0 +1,82 @@ +name = $name; + $this->options = $options; + } + + /** + * Sets the table name. + * + * @param string $name The name of the table + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Gets the table name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Gets the table options + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Sets the table options + * + * @param array $options The options for the table creation + * @return void + */ + public function setOptions(array $options) + { + $this->options = $options; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Db/Util/AlterInstructions.php b/vendor/robmorgan/phinx/src/Phinx/Db/Util/AlterInstructions.php new file mode 100644 index 0000000..94497e7 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Db/Util/AlterInstructions.php @@ -0,0 +1,122 @@ +alterParts = $alterParts; + $this->postSteps = $postSteps; + } + + /** + * Adds another part to the single ALTER instruction + * + * @param string $part The SQL snipped to add as part of the ALTER instruction + * @return void + */ + public function addAlter($part) + { + $this->alterParts[] = $part; + } + + /** + * Adds a SQL command to be executed after the ALTER instruction. + * This method allows a callable, with will get an empty array as state + * for the first time and will pass the return value of the callable to + * the next callable, if present. + * + * This allows to keep a single state across callbacks. + * + * @param string|callable $sql The SQL to run after, or a callable to execute + * @return void + */ + public function addPostStep($sql) + { + $this->postSteps[] = $sql; + } + + /** + * Returns the alter SQL snippets + * + * @return string[] + */ + public function getAlterParts() + { + return $this->alterParts; + } + + /** + * Returns the SQL commands to run after the ALTER instruction + * + * @return (string|callable)[] + */ + public function getPostSteps() + { + return $this->postSteps; + } + + /** + * Merges another AlterInstructions object to this one + * + * @param \Phinx\Db\Util\AlterInstructions $other The other collection of instructions to merge in + * @return void + */ + public function merge(AlterInstructions $other) + { + $this->alterParts = array_merge($this->alterParts, $other->getAlterParts()); + $this->postSteps = array_merge($this->postSteps, $other->getPostSteps()); + } + + /** + * Executes the ALTER instruction and all of the post steps. + * + * @param string $alterTemplate The template for the alter instruction + * @param callable $executor The function to be used to execute all instructions + * @return void + */ + public function execute($alterTemplate, callable $executor) + { + if ($this->alterParts) { + $alter = sprintf($alterTemplate, implode(', ', $this->alterParts)); + $executor($alter); + } + + $state = []; + + foreach ($this->postSteps as $instruction) { + if (is_callable($instruction)) { + $state = $instruction($state); + continue; + } + + $executor($instruction); + } + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Migration/AbstractMigration.php b/vendor/robmorgan/phinx/src/Phinx/Migration/AbstractMigration.php new file mode 100644 index 0000000..76367e5 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Migration/AbstractMigration.php @@ -0,0 +1,335 @@ + + */ +abstract class AbstractMigration implements MigrationInterface +{ + /** + * @var string + */ + protected $environment; + + /** + * @var int + */ + protected $version; + + /** + * @var \Phinx\Db\Adapter\AdapterInterface + */ + protected $adapter; + + /** + * @var \Symfony\Component\Console\Output\OutputInterface + */ + protected $output; + + /** + * @var \Symfony\Component\Console\Input\InputInterface + */ + protected $input; + + /** + * Whether this migration is being applied or reverted + * + * @var bool + */ + protected $isMigratingUp = true; + + /** + * List of all the table objects created by this migration + * + * @var array + */ + protected $tables = []; + + /** + * @param string $environment Environment Detected + * @param int $version Migration Version + * @param \Symfony\Component\Console\Input\InputInterface|null $input Input + * @param \Symfony\Component\Console\Output\OutputInterface|null $output Output + */ + final public function __construct($environment, $version, ?InputInterface $input = null, ?OutputInterface $output = null) + { + $this->environment = $environment; + $this->version = $version; + + if ($input !== null) { + $this->setInput($input); + } + + if ($output !== null) { + $this->setOutput($output); + } + } + + /** + * @inheritDoc + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * @inheritDoc + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * @inheritDoc + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + + return $this; + } + + /** + * @inheritDoc + */ + public function getInput() + { + return $this->input; + } + + /** + * @inheritDoc + */ + public function setOutput(OutputInterface $output) + { + $this->output = $output; + + return $this; + } + + /** + * @inheritDoc + */ + public function getOutput() + { + return $this->output; + } + + /** + * @inheritDoc + */ + public function getName() + { + return static::class; + } + + /** + * @inheritDoc + */ + public function getEnvironment() + { + return $this->environment; + } + + /** + * @inheritDoc + */ + public function setVersion($version) + { + $this->version = $version; + + return $this; + } + + /** + * @inheritDoc + */ + public function getVersion() + { + return $this->version; + } + + /** + * @inheritDoc + */ + public function setMigratingUp($isMigratingUp) + { + $this->isMigratingUp = $isMigratingUp; + + return $this; + } + + /** + * @inheritDoc + */ + public function isMigratingUp() + { + return $this->isMigratingUp; + } + + /** + * @inheritDoc + */ + public function execute($sql) + { + return $this->getAdapter()->execute($sql); + } + + /** + * @inheritDoc + */ + public function query($sql) + { + return $this->getAdapter()->query($sql); + } + + /** + * @inheritDoc + */ + public function getQueryBuilder() + { + return $this->getAdapter()->getQueryBuilder(); + } + + /** + * @inheritDoc + */ + public function fetchRow($sql) + { + return $this->getAdapter()->fetchRow($sql); + } + + /** + * @inheritDoc + */ + public function fetchAll($sql) + { + return $this->getAdapter()->fetchAll($sql); + } + + /** + * @inheritDoc + */ + public function insert($table, $data) + { + trigger_error('insert() is deprecated since 0.10.0. Use $this->table($tableName)->insert($data)->save() instead.', E_USER_DEPRECATED); + // convert to table object + if (is_string($table)) { + $table = new Table($table, [], $this->getAdapter()); + } + $table->insert($data)->save(); + } + + /** + * @inheritDoc + */ + public function createDatabase($name, $options) + { + $this->getAdapter()->createDatabase($name, $options); + } + + /** + * @inheritDoc + */ + public function dropDatabase($name) + { + $this->getAdapter()->dropDatabase($name); + } + + /** + * @inheritDoc + */ + public function hasTable($tableName) + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * @inheritDoc + */ + public function table($tableName, $options = []) + { + $table = new Table($tableName, $options, $this->getAdapter()); + $this->tables[] = $table; + + return $table; + } + + /** + * A short-hand method to drop the given database table. + * + * @deprecated since 0.10.0. Use $this->table($tableName)->drop()->save() instead. + * @param string $tableName Table name + * @return void + */ + public function dropTable($tableName) + { + trigger_error('dropTable() is deprecated since 0.10.0. Use $this->table($tableName)->drop()->save() instead.', E_USER_DEPRECATED); + $this->table($tableName)->drop()->save(); + } + + /** + * Perform checks on the migration, print a warning + * if there are potential problems. + * + * Right now, the only check is if there is both a `change()` and + * an `up()` or a `down()` method. + * + * @param string|null $direction Direction + * @return void + */ + public function preFlightCheck($direction = null) + { + if (method_exists($this, MigrationInterface::CHANGE)) { + if ( + method_exists($this, MigrationInterface::UP) || + method_exists($this, MigrationInterface::DOWN) + ) { + $this->output->writeln(sprintf( + 'warning Migration contains both change() and/or up()/down() methods. Ignoring up() and down().' + )); + } + } + } + + /** + * Perform checks on the migration after completion + * + * Right now, the only check is whether all changes were committed + * + * @param string|null $direction direction of migration + * @throws \RuntimeException + * @return void + */ + public function postFlightCheck($direction = null) + { + foreach ($this->tables as $table) { + if ($table->hasPendingActions()) { + throw new RuntimeException('Migration has pending actions after execution!'); + } + } + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Migration/AbstractTemplateCreation.php b/vendor/robmorgan/phinx/src/Phinx/Migration/AbstractTemplateCreation.php new file mode 100644 index 0000000..213e635 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Migration/AbstractTemplateCreation.php @@ -0,0 +1,74 @@ +setInput($input); + } + if ($output !== null) { + $this->setOutput($output); + } + } + + /** + * @inheritDoc + */ + public function getInput() + { + return $this->input; + } + + /** + * @inheritDoc + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + + return $this; + } + + /** + * @inheritDoc + */ + public function getOutput() + { + return $this->output; + } + + /** + * @inheritDoc + */ + public function setOutput(OutputInterface $output) + { + $this->output = $output; + + return $this; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Migration/CreationInterface.php b/vendor/robmorgan/phinx/src/Phinx/Migration/CreationInterface.php new file mode 100644 index 0000000..a7c4ef3 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Migration/CreationInterface.php @@ -0,0 +1,69 @@ + + */ +interface CreationInterface +{ + /** + * @param \Symfony\Component\Console\Input\InputInterface|null $input Input + * @param \Symfony\Component\Console\Output\OutputInterface|null $output Output + */ + public function __construct(?InputInterface $input = null, ?OutputInterface $output = null); + + /** + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @return \Phinx\Migration\CreationInterface + */ + public function setInput(InputInterface $input); + + /** + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return \Phinx\Migration\CreationInterface + */ + public function setOutput(OutputInterface $output); + + /** + * @return \Symfony\Component\Console\Input\InputInterface + */ + public function getInput(); + + /** + * @return \Symfony\Component\Console\Output\OutputInterface + */ + public function getOutput(); + + /** + * Get the migration template. + * + * This will be the content that Phinx will amend to generate the migration file. + * + * @return string The content of the template for Phinx to amend. + */ + public function getMigrationTemplate(); + + /** + * Post Migration Creation. + * + * Once the migration file has been created, this method will be called, allowing any additional + * processing, specific to the template to be performed. + * + * @param string $migrationFilename The name of the newly created migration. + * @param string $className The class name. + * @param string $baseClassName The name of the base class. + * @return void + */ + public function postMigrationCreation($migrationFilename, $className, $baseClassName); +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Migration/IrreversibleMigrationException.php b/vendor/robmorgan/phinx/src/Phinx/Migration/IrreversibleMigrationException.php new file mode 100644 index 0000000..376bcdc --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Migration/IrreversibleMigrationException.php @@ -0,0 +1,20 @@ + + */ +class IrreversibleMigrationException extends Exception +{ +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Migration/Manager.php b/vendor/robmorgan/phinx/src/Phinx/Migration/Manager.php new file mode 100644 index 0000000..5a24cb6 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Migration/Manager.php @@ -0,0 +1,1063 @@ +setConfig($config); + $this->setInput($input); + $this->setOutput($output); + } + + /** + * Prints the specified environment's migration status. + * + * @param string $environment environment to print status of + * @param string|null $format format to print status in (either text, json, or null) + * @throws \RuntimeException + * @return array array indicating if there are any missing or down migrations + */ + public function printStatus($environment, $format = null) + { + $output = $this->getOutput(); + $hasDownMigration = false; + $hasMissingMigration = false; + $migrations = $this->getMigrations($environment); + $migrationCount = 0; + $missingCount = 0; + $pendingMigrationCount = 0; + $finalMigrations = []; + $verbosity = $output->getVerbosity(); + if ($format === 'json') { + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + } + if (count($migrations)) { + // rewrite using Symfony Table Helper as we already have this library + // included and it will fix formatting issues (e.g drawing the lines) + $output->writeln(''); + + switch ($this->getConfig()->getVersionOrder()) { + case Config::VERSION_ORDER_CREATION_TIME: + $migrationIdAndStartedHeader = '[Migration ID] Started '; + break; + case Config::VERSION_ORDER_EXECUTION_TIME: + $migrationIdAndStartedHeader = 'Migration ID [Started ]'; + break; + default: + throw new RuntimeException('Invalid version_order configuration option'); + } + + $output->writeln(" Status $migrationIdAndStartedHeader Finished Migration Name "); + $output->writeln('----------------------------------------------------------------------------------'); + + $env = $this->getEnvironment($environment); + $versions = $env->getVersionLog(); + + $maxNameLength = $versions ? max(array_map(function ($version) { + return strlen($version['migration_name']); + }, $versions)) : 0; + + $missingVersions = array_diff_key($versions, $migrations); + $missingCount = count($missingVersions); + + $hasMissingMigration = !empty($missingVersions); + + // get the migrations sorted in the same way as the versions + /** @var \Phinx\Migration\AbstractMigration[] $sortedMigrations */ + $sortedMigrations = []; + + foreach ($versions as $versionCreationTime => $version) { + if (isset($migrations[$versionCreationTime])) { + array_push($sortedMigrations, $migrations[$versionCreationTime]); + unset($migrations[$versionCreationTime]); + } + } + + if (empty($sortedMigrations) && !empty($missingVersions)) { + // this means we have no up migrations, so we write all the missing versions already so they show up + // before any possible down migration + foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) { + $this->printMissingVersion($missingVersion, $maxNameLength); + + unset($missingVersions[$missingVersionCreationTime]); + } + } + + // any migration left in the migrations (ie. not unset when sorting the migrations by the version order) is + // a migration that is down, so we add them to the end of the sorted migrations list + if (!empty($migrations)) { + $sortedMigrations = array_merge($sortedMigrations, $migrations); + } + + $migrationCount = count($sortedMigrations); + foreach ($sortedMigrations as $migration) { + $version = array_key_exists($migration->getVersion(), $versions) ? $versions[$migration->getVersion()] : false; + if ($version) { + // check if there are missing versions before this version + foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) { + if ($this->getConfig()->isVersionOrderCreationTime()) { + if ($missingVersion['version'] > $version['version']) { + break; + } + } else { + if ($missingVersion['start_time'] > $version['start_time']) { + break; + } elseif ( + $missingVersion['start_time'] == $version['start_time'] && + $missingVersion['version'] > $version['version'] + ) { + break; + } + } + + $this->printMissingVersion($missingVersion, $maxNameLength); + + unset($missingVersions[$missingVersionCreationTime]); + } + + $status = ' up '; + } else { + $pendingMigrationCount++; + $hasDownMigration = true; + $status = ' down '; + } + $maxNameLength = max($maxNameLength, strlen($migration->getName())); + + $output->writeln(sprintf( + '%s %14.0f %19s %19s %s', + $status, + $migration->getVersion(), + ($version ? $version['start_time'] : ''), + ($version ? $version['end_time'] : ''), + $migration->getName() + )); + + if ($version && $version['breakpoint']) { + $output->writeln(' BREAKPOINT SET'); + } + + $finalMigrations[] = ['migration_status' => trim(strip_tags($status)), 'migration_id' => sprintf('%14.0f', $migration->getVersion()), 'migration_name' => $migration->getName()]; + unset($versions[$migration->getVersion()]); + } + + // and finally add any possibly-remaining missing migrations + foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) { + $this->printMissingVersion($missingVersion, $maxNameLength); + + unset($missingVersions[$missingVersionCreationTime]); + } + } else { + // there are no migrations + $output->writeln(''); + $output->writeln('There are no available migrations. Try creating one using the create command.'); + } + + // write an empty line + $output->writeln(''); + + if ($format !== null) { + switch ($format) { + case AbstractCommand::FORMAT_JSON: + $output->setVerbosity($verbosity); + $output->writeln(json_encode( + [ + 'pending_count' => $pendingMigrationCount, + 'missing_count' => $missingCount, + 'total_count' => $migrationCount + $missingCount, + 'migrations' => $finalMigrations, + ] + )); + break; + default: + $output->writeln('Unsupported format: ' . $format . ''); + } + } + + return [ + 'hasMissingMigration' => $hasMissingMigration, + 'hasDownMigration' => $hasDownMigration, + ]; + } + + /** + * Print Missing Version + * + * @param array $version The missing version to print (in the format returned by Environment.getVersionLog). + * @param int $maxNameLength The maximum migration name length. + * @return void + */ + protected function printMissingVersion($version, $maxNameLength) + { + $this->getOutput()->writeln(sprintf( + ' up %14.0f %19s %19s %s ** MISSING MIGRATION FILE **', + $version['version'], + $version['start_time'], + $version['end_time'], + str_pad($version['migration_name'], $maxNameLength, ' ') + )); + + if ($version && $version['breakpoint']) { + $this->getOutput()->writeln(' BREAKPOINT SET'); + } + } + + /** + * Migrate to the version of the database on a given date. + * + * @param string $environment Environment + * @param \DateTime $dateTime Date to migrate to + * @param bool $fake flag that if true, we just record running the migration, but not actually do the + * migration + * @return void + */ + public function migrateToDateTime($environment, DateTime $dateTime, $fake = false) + { + $versions = array_keys($this->getMigrations($environment)); + $dateString = $dateTime->format('YmdHis'); + + $outstandingMigrations = array_filter($versions, function ($version) use ($dateString) { + return $version <= $dateString; + }); + + if (count($outstandingMigrations) > 0) { + $migration = max($outstandingMigrations); + $this->getOutput()->writeln('Migrating to version ' . $migration); + $this->migrate($environment, $migration, $fake); + } + } + + /** + * Migrate an environment to the specified version. + * + * @param string $environment Environment + * @param int|null $version version to migrate to + * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration + * @return void + */ + public function migrate($environment, $version = null, $fake = false) + { + $migrations = $this->getMigrations($environment); + $env = $this->getEnvironment($environment); + $versions = $env->getVersions(); + $current = $env->getCurrentVersion(); + + if (empty($versions) && empty($migrations)) { + return; + } + + if ($version === null) { + $version = max(array_merge($versions, array_keys($migrations))); + } else { + if ($version != 0 && !isset($migrations[$version])) { + $this->output->writeln(sprintf( + 'warning %s is not a valid version', + $version + )); + + return; + } + } + + // are we migrating up or down? + $direction = $version > $current ? MigrationInterface::UP : MigrationInterface::DOWN; + + if ($direction === MigrationInterface::DOWN) { + // run downs first + krsort($migrations); + foreach ($migrations as $migration) { + if ($migration->getVersion() <= $version) { + break; + } + + if (in_array($migration->getVersion(), $versions)) { + $this->executeMigration($environment, $migration, MigrationInterface::DOWN, $fake); + } + } + } + + ksort($migrations); + foreach ($migrations as $migration) { + if ($migration->getVersion() > $version) { + break; + } + + if (!in_array($migration->getVersion(), $versions)) { + $this->executeMigration($environment, $migration, MigrationInterface::UP, $fake); + } + } + } + + /** + * Execute a migration against the specified environment. + * + * @param string $name Environment Name + * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param string $direction Direction + * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration + * @return void + */ + public function executeMigration($name, MigrationInterface $migration, $direction = MigrationInterface::UP, $fake = false) + { + $this->getOutput()->writeln(''); + $this->getOutput()->writeln( + ' ==' . + ' ' . $migration->getVersion() . ' ' . $migration->getName() . ':' . + ' ' . ($direction === MigrationInterface::UP ? 'migrating' : 'reverting') . '' + ); + + // Execute the migration and log the time elapsed. + $start = microtime(true); + $this->getEnvironment($name)->executeMigration($migration, $direction, $fake); + $end = microtime(true); + + $this->getOutput()->writeln( + ' ==' . + ' ' . $migration->getVersion() . ' ' . $migration->getName() . ':' . + ' ' . ($direction === MigrationInterface::UP ? 'migrated' : 'reverted') . + ' ' . sprintf('%.4fs', $end - $start) . '' + ); + } + + /** + * Execute a seeder against the specified environment. + * + * @param string $name Environment Name + * @param \Phinx\Seed\SeedInterface $seed Seed + * @return void + */ + public function executeSeed($name, SeedInterface $seed) + { + $this->getOutput()->writeln(''); + $this->getOutput()->writeln( + ' ==' . + ' ' . $seed->getName() . ':' . + ' seeding' + ); + + // Execute the seeder and log the time elapsed. + $start = microtime(true); + $this->getEnvironment($name)->executeSeed($seed); + $end = microtime(true); + + $this->getOutput()->writeln( + ' ==' . + ' ' . $seed->getName() . ':' . + ' seeded' . + ' ' . sprintf('%.4fs', $end - $start) . '' + ); + } + + /** + * Rollback an environment to the specified version. + * + * @param string $environment Environment + * @param int|string|null $target Target + * @param bool $force Force + * @param bool $targetMustMatchVersion Target must match version + * @param bool $fake Flag that if true, we just record running the migration, but not actually do the migration + * @return void + */ + public function rollback($environment, $target = null, $force = false, $targetMustMatchVersion = true, $fake = false) + { + // note that the migrations are indexed by name (aka creation time) in ascending order + $migrations = $this->getMigrations($environment); + + // note that the version log are also indexed by name with the proper ascending order according to the version order + $executedVersions = $this->getEnvironment($environment)->getVersionLog(); + + // get a list of migrations sorted in the opposite way of the executed versions + $sortedMigrations = []; + + foreach ($executedVersions as $versionCreationTime => &$executedVersion) { + // if we have a date (ie. the target must not match a version) and we are sorting by execution time, we + // convert the version start time so we can compare directly with the target date + if (!$this->getConfig()->isVersionOrderCreationTime() && !$targetMustMatchVersion) { + $dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $executedVersion['start_time']); + $executedVersion['start_time'] = $dateTime->format('YmdHis'); + } + + if (isset($migrations[$versionCreationTime])) { + array_unshift($sortedMigrations, $migrations[$versionCreationTime]); + } else { + // this means the version is missing so we unset it so that we don't consider it when rolling back + // migrations (or choosing the last up version as target) + unset($executedVersions[$versionCreationTime]); + } + } + + if ($target === 'all' || $target === '0') { + $target = 0; + } elseif (!is_numeric($target) && $target !== null) { // try to find a target version based on name + // search through the migrations using the name + $migrationNames = array_map(function ($item) { + return $item['migration_name']; + }, $executedVersions); + $found = array_search($target, $migrationNames, true); + + // check on was found + if ($found !== false) { + $target = (string)$found; + } else { + $this->getOutput()->writeln("No migration found with name ($target)"); + + return; + } + } + + // Check we have at least 1 migration to revert + $executedVersionCreationTimes = array_keys($executedVersions); + if (empty($executedVersionCreationTimes) || $target == end($executedVersionCreationTimes)) { + $this->getOutput()->writeln('No migrations to rollback'); + + return; + } + + // If no target was supplied, revert the last migration + if ($target === null) { + // Get the migration before the last run migration + $prev = count($executedVersionCreationTimes) - 2; + $target = $prev >= 0 ? $executedVersionCreationTimes[$prev] : 0; + } + + // If the target must match a version, check the target version exists + if ($targetMustMatchVersion && $target !== 0 && !isset($migrations[$target])) { + $this->getOutput()->writeln("Target version ($target) not found"); + + return; + } + + // Rollback all versions until we find the wanted rollback target + $rollbacked = false; + + foreach ($sortedMigrations as $migration) { + if ($targetMustMatchVersion && $migration->getVersion() == $target) { + break; + } + + if (in_array($migration->getVersion(), $executedVersionCreationTimes)) { + $executedVersion = $executedVersions[$migration->getVersion()]; + + if (!$targetMustMatchVersion) { + if ( + ($this->getConfig()->isVersionOrderCreationTime() && $executedVersion['version'] <= $target) || + (!$this->getConfig()->isVersionOrderCreationTime() && $executedVersion['start_time'] <= $target) + ) { + break; + } + } + + if ($executedVersion['breakpoint'] != 0 && !$force) { + $this->getOutput()->writeln('Breakpoint reached. Further rollbacks inhibited.'); + break; + } + $this->executeMigration($environment, $migration, MigrationInterface::DOWN, $fake); + $rollbacked = true; + } + } + + if (!$rollbacked) { + $this->getOutput()->writeln('No migrations to rollback'); + } + } + + /** + * Run database seeders against an environment. + * + * @param string $environment Environment + * @param string|null $seed Seeder + * @throws \InvalidArgumentException + * @return void + */ + public function seed($environment, $seed = null) + { + $seeds = $this->getSeeds(); + + if ($seed === null) { + // run all seeders + foreach ($seeds as $seeder) { + if (array_key_exists($seeder->getName(), $seeds)) { + $this->executeSeed($environment, $seeder); + } + } + } else { + // run only one seeder + if (array_key_exists($seed, $seeds)) { + $this->executeSeed($environment, $seeds[$seed]); + } else { + throw new InvalidArgumentException(sprintf('The seed class "%s" does not exist', $seed)); + } + } + } + + /** + * Sets the environments. + * + * @param \Phinx\Migration\Manager\Environment[] $environments Environments + * @return $this + */ + public function setEnvironments($environments = []) + { + $this->environments = $environments; + + return $this; + } + + /** + * Gets the manager class for the given environment. + * + * @param string $name Environment Name + * @throws \InvalidArgumentException + * @return \Phinx\Migration\Manager\Environment + */ + public function getEnvironment($name) + { + if (isset($this->environments[$name])) { + return $this->environments[$name]; + } + + // check the environment exists + if (!$this->getConfig()->hasEnvironment($name)) { + throw new InvalidArgumentException(sprintf( + 'The environment "%s" does not exist', + $name + )); + } + + // create an environment instance and cache it + $envOptions = $this->getConfig()->getEnvironment($name); + $envOptions['version_order'] = $this->getConfig()->getVersionOrder(); + $envOptions['data_domain'] = $this->getConfig()->getDataDomain(); + + $environment = new Environment($name, $envOptions); + $this->environments[$name] = $environment; + $environment->setInput($this->getInput()); + $environment->setOutput($this->getOutput()); + + return $environment; + } + + /** + * Sets the user defined PSR-11 container + * + * @param \Psr\Container\ContainerInterface $container Container + * @return void + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * Sets the console input. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @return $this + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + + return $this; + } + + /** + * Gets the console input. + * + * @return \Symfony\Component\Console\Input\InputInterface + */ + public function getInput() + { + return $this->input; + } + + /** + * Sets the console output. + * + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return $this + */ + public function setOutput(OutputInterface $output) + { + $this->output = $output; + + return $this; + } + + /** + * Gets the console output. + * + * @return \Symfony\Component\Console\Output\OutputInterface + */ + public function getOutput() + { + return $this->output; + } + + /** + * Sets the database migrations. + * + * @param \Phinx\Migration\AbstractMigration[] $migrations Migrations + * @return $this + */ + public function setMigrations(array $migrations) + { + $this->migrations = $migrations; + + return $this; + } + + /** + * Gets an array of the database migrations, indexed by migration name (aka creation time) and sorted in ascending + * order + * + * @param string $environment Environment + * @throws \InvalidArgumentException + * @return \Phinx\Migration\AbstractMigration[] + */ + public function getMigrations($environment) + { + if ($this->migrations === null) { + $phpFiles = $this->getMigrationFiles(); + + if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) { + $this->getOutput()->writeln('Migration file'); + $this->getOutput()->writeln( + array_map( + function ($phpFile) { + return " {$phpFile}"; + }, + $phpFiles + ) + ); + } + + // filter the files to only get the ones that match our naming scheme + $fileNames = []; + /** @var \Phinx\Migration\AbstractMigration[] $versions */ + $versions = []; + + foreach ($phpFiles as $filePath) { + if (Util::isValidMigrationFileName(basename($filePath))) { + if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) { + $this->getOutput()->writeln("Valid migration file {$filePath}."); + } + + $version = Util::getVersionFromFileName(basename($filePath)); + + if (isset($versions[$version])) { + throw new InvalidArgumentException(sprintf('Duplicate migration - "%s" has the same version as "%s"', $filePath, $versions[$version]->getVersion())); + } + + $config = $this->getConfig(); + $namespace = $config instanceof NamespaceAwareInterface ? $config->getMigrationNamespaceByPath(dirname($filePath)) : null; + + // convert the filename to a class name + $class = ($namespace === null ? '' : $namespace . '\\') . Util::mapFileNameToClassName(basename($filePath)); + + if (isset($fileNames[$class])) { + throw new InvalidArgumentException(sprintf( + 'Migration "%s" has the same name as "%s"', + basename($filePath), + $fileNames[$class] + )); + } + + $fileNames[$class] = basename($filePath); + + if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) { + $this->getOutput()->writeln("Loading class $class from $filePath."); + } + + // load the migration file + $orig_display_errors_setting = ini_get('display_errors'); + ini_set('display_errors', 'On'); + /** @noinspection PhpIncludeInspection */ + require_once $filePath; + ini_set('display_errors', $orig_display_errors_setting); + if (!class_exists($class)) { + throw new InvalidArgumentException(sprintf( + 'Could not find class "%s" in file "%s"', + $class, + $filePath + )); + } + + if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) { + $this->getOutput()->writeln("Running $class."); + } + + // instantiate it + $migration = new $class($environment, $version, $this->getInput(), $this->getOutput()); + + if (!($migration instanceof AbstractMigration)) { + throw new InvalidArgumentException(sprintf( + 'The class "%s" in file "%s" must extend \Phinx\Migration\AbstractMigration', + $class, + $filePath + )); + } + + $versions[$version] = $migration; + } else { + if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) { + $this->getOutput()->writeln("Invalid migration file {$filePath}."); + } + } + } + + ksort($versions); + $this->setMigrations($versions); + } + + return $this->migrations; + } + + /** + * Returns a list of migration files found in the provided migration paths. + * + * @return string[] + */ + protected function getMigrationFiles() + { + return Util::getFiles($this->getConfig()->getMigrationPaths()); + } + + /** + * Sets the database seeders. + * + * @param \Phinx\Seed\AbstractSeed[] $seeds Seeders + * @return $this + */ + public function setSeeds(array $seeds) + { + $this->seeds = $seeds; + + return $this; + } + + /** + * Get seed dependencies instances from seed dependency array + * + * @param \Phinx\Seed\AbstractSeed $seed Seed + * @return \Phinx\Seed\AbstractSeed[] + */ + protected function getSeedDependenciesInstances(AbstractSeed $seed) + { + $dependenciesInstances = []; + $dependencies = $seed->getDependencies(); + if (!empty($dependencies)) { + foreach ($dependencies as $dependency) { + foreach ($this->seeds as $seed) { + if (get_class($seed) === $dependency) { + $dependenciesInstances[get_class($seed)] = $seed; + } + } + } + } + + return $dependenciesInstances; + } + + /** + * Order seeds by dependencies + * + * @param \Phinx\Seed\AbstractSeed[] $seeds Seeds + * @return \Phinx\Seed\AbstractSeed[] + */ + protected function orderSeedsByDependencies(array $seeds) + { + $orderedSeeds = []; + foreach ($seeds as $seed) { + $key = get_class($seed); + $dependencies = $this->getSeedDependenciesInstances($seed); + if (!empty($dependencies)) { + $orderedSeeds[$key] = $seed; + $orderedSeeds = array_merge($this->orderSeedsByDependencies($dependencies), $orderedSeeds); + } else { + $orderedSeeds[$key] = $seed; + } + } + + return $orderedSeeds; + } + + /** + * Gets an array of database seeders. + * + * @throws \InvalidArgumentException + * @return \Phinx\Seed\AbstractSeed[] + */ + public function getSeeds() + { + if ($this->seeds === null) { + $phpFiles = $this->getSeedFiles(); + + // filter the files to only get the ones that match our naming scheme + $fileNames = []; + /** @var \Phinx\Seed\AbstractSeed[] $seeds */ + $seeds = []; + + foreach ($phpFiles as $filePath) { + if (Util::isValidSeedFileName(basename($filePath))) { + $config = $this->getConfig(); + $namespace = $config instanceof NamespaceAwareInterface ? $config->getSeedNamespaceByPath(dirname($filePath)) : null; + + // convert the filename to a class name + $class = ($namespace === null ? '' : $namespace . '\\') . pathinfo($filePath, PATHINFO_FILENAME); + $fileNames[$class] = basename($filePath); + + // load the seed file + /** @noinspection PhpIncludeInspection */ + require_once $filePath; + if (!class_exists($class)) { + throw new InvalidArgumentException(sprintf( + 'Could not find class "%s" in file "%s"', + $class, + $filePath + )); + } + + // instantiate it + /** @var \Phinx\Seed\AbstractSeed $seed */ + if ($this->container !== null) { + $seed = $this->container->get($class); + } else { + $seed = new $class(); + } + $input = $this->getInput(); + if ($input !== null) { + $seed->setInput($input); + } + $output = $this->getOutput(); + if ($output !== null) { + $seed->setOutput($output); + } + + if (!($seed instanceof AbstractSeed)) { + throw new InvalidArgumentException(sprintf( + 'The class "%s" in file "%s" must extend \Phinx\Seed\AbstractSeed', + $class, + $filePath + )); + } + + $seeds[$class] = $seed; + } + } + + ksort($seeds); + $this->setSeeds($seeds); + } + + $this->seeds = $this->orderSeedsByDependencies($this->seeds); + + return $this->seeds; + } + + /** + * Returns a list of seed files found in the provided seed paths. + * + * @return string[] + */ + protected function getSeedFiles() + { + return Util::getFiles($this->getConfig()->getSeedPaths()); + } + + /** + * Sets the config. + * + * @param \Phinx\Config\ConfigInterface $config Configuration Object + * @return $this + */ + public function setConfig(ConfigInterface $config) + { + $this->config = $config; + + return $this; + } + + /** + * Gets the config. + * + * @return \Phinx\Config\ConfigInterface + */ + public function getConfig() + { + return $this->config; + } + + /** + * Toggles the breakpoint for a specific version. + * + * @param string $environment Environment name + * @param int|null $version Version + * @return void + */ + public function toggleBreakpoint($environment, $version) + { + $this->markBreakpoint($environment, $version, self::BREAKPOINT_TOGGLE); + } + + /** + * Updates the breakpoint for a specific version. + * + * @param string $environment The required environment + * @param int|null $version The version of the target migration + * @param int $mark The state of the breakpoint as defined by self::BREAKPOINT_xxxx constants. + * @return void + */ + protected function markBreakpoint($environment, $version, $mark) + { + $migrations = $this->getMigrations($environment); + $this->getMigrations($environment); + $env = $this->getEnvironment($environment); + $versions = $env->getVersionLog(); + + if (empty($versions) || empty($migrations)) { + return; + } + + if ($version === null) { + $lastVersion = end($versions); + $version = $lastVersion['version']; + } + + if ($version != 0 && (!isset($versions[$version]) || !isset($migrations[$version]))) { + $this->output->writeln(sprintf( + 'warning %s is not a valid version', + $version + )); + + return; + } + + switch ($mark) { + case self::BREAKPOINT_TOGGLE: + $env->getAdapter()->toggleBreakpoint($migrations[$version]); + break; + case self::BREAKPOINT_SET: + if ($versions[$version]['breakpoint'] == 0) { + $env->getAdapter()->setBreakpoint($migrations[$version]); + } + break; + case self::BREAKPOINT_UNSET: + if ($versions[$version]['breakpoint'] == 1) { + $env->getAdapter()->unsetBreakpoint($migrations[$version]); + } + break; + } + + $versions = $env->getVersionLog(); + + $this->getOutput()->writeln( + ' Breakpoint ' . ($versions[$version]['breakpoint'] ? 'set' : 'cleared') . + ' for ' . $version . '' . + ' ' . $migrations[$version]->getName() . '' + ); + } + + /** + * Remove all breakpoints + * + * @param string $environment The required environment + * @return void + */ + public function removeBreakpoints($environment) + { + $this->getOutput()->writeln(sprintf( + ' %d breakpoints cleared.', + $this->getEnvironment($environment)->getAdapter()->resetAllBreakpoints() + )); + } + + /** + * Set the breakpoint for a specific version. + * + * @param string $environment The required environment + * @param int|null $version The version of the target migration + * @return void + */ + public function setBreakpoint($environment, $version) + { + $this->markBreakpoint($environment, $version, self::BREAKPOINT_SET); + } + + /** + * Unset the breakpoint for a specific version. + * + * @param string $environment The required environment + * @param int|null $version The version of the target migration + * @return void + */ + public function unsetBreakpoint($environment, $version) + { + $this->markBreakpoint($environment, $version, self::BREAKPOINT_UNSET); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Migration/Manager/Environment.php b/vendor/robmorgan/phinx/src/Phinx/Migration/Manager/Environment.php new file mode 100644 index 0000000..d21817a --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Migration/Manager/Environment.php @@ -0,0 +1,398 @@ +name = $name; + $this->options = $options; + } + + /** + * Executes the specified migration on this environment. + * + * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param string $direction Direction + * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration + * @return void + */ + public function executeMigration(MigrationInterface $migration, $direction = MigrationInterface::UP, $fake = false) + { + $direction = $direction === MigrationInterface::UP ? MigrationInterface::UP : MigrationInterface::DOWN; + $migration->setMigratingUp($direction === MigrationInterface::UP); + + $startTime = time(); + $migration->setAdapter($this->getAdapter()); + + $migration->preFlightCheck($direction); + + if (method_exists($migration, MigrationInterface::INIT)) { + $migration->{MigrationInterface::INIT}(); + } + + if (!$fake) { + // begin the transaction if the adapter supports it + if ($this->getAdapter()->hasTransactions()) { + $this->getAdapter()->beginTransaction(); + } + + // Run the migration + if (method_exists($migration, MigrationInterface::CHANGE)) { + if ($direction === MigrationInterface::DOWN) { + // Create an instance of the ProxyAdapter so we can record all + // of the migration commands for reverse playback + + /** @var \Phinx\Db\Adapter\ProxyAdapter $proxyAdapter */ + $proxyAdapter = AdapterFactory::instance() + ->getWrapper('proxy', $this->getAdapter()); + $migration->setAdapter($proxyAdapter); + $migration->{MigrationInterface::CHANGE}(); + $proxyAdapter->executeInvertedCommands(); + $migration->setAdapter($this->getAdapter()); + } else { + $migration->{MigrationInterface::CHANGE}(); + } + } else { + $migration->{$direction}(); + } + + // commit the transaction if the adapter supports it + if ($this->getAdapter()->hasTransactions()) { + $this->getAdapter()->commitTransaction(); + } + } + + $migration->postFlightCheck($direction); + + // Record it in the database + $this->getAdapter()->migrated($migration, $direction, date('Y-m-d H:i:s', $startTime), date('Y-m-d H:i:s', time())); + } + + /** + * Executes the specified seeder on this environment. + * + * @param \Phinx\Seed\SeedInterface $seed Seed + * @return void + */ + public function executeSeed(SeedInterface $seed) + { + $seed->setAdapter($this->getAdapter()); + if (method_exists($seed, SeedInterface::INIT)) { + $seed->{SeedInterface::INIT}(); + } + + // begin the transaction if the adapter supports it + if ($this->getAdapter()->hasTransactions()) { + $this->getAdapter()->beginTransaction(); + } + + // Run the seeder + if (method_exists($seed, SeedInterface::RUN)) { + $seed->{SeedInterface::RUN}(); + } + + // commit the transaction if the adapter supports it + if ($this->getAdapter()->hasTransactions()) { + $this->getAdapter()->commitTransaction(); + } + } + + /** + * Sets the environment's name. + * + * @param string $name Environment Name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Gets the environment name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the environment's options. + * + * @param array $options Environment Options + * @return $this + */ + public function setOptions($options) + { + $this->options = $options; + + return $this; + } + + /** + * Gets the environment's options. + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Sets the console input. + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @return $this + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + + return $this; + } + + /** + * Gets the console input. + * + * @return \Symfony\Component\Console\Input\InputInterface + */ + public function getInput() + { + return $this->input; + } + + /** + * Sets the console output. + * + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return $this + */ + public function setOutput(OutputInterface $output) + { + $this->output = $output; + + return $this; + } + + /** + * Gets the console output. + * + * @return \Symfony\Component\Console\Output\OutputInterface + */ + public function getOutput() + { + return $this->output; + } + + /** + * Gets all migrated version numbers. + * + * @return array + */ + public function getVersions() + { + return $this->getAdapter()->getVersions(); + } + + /** + * Get all migration log entries, indexed by version creation time and sorted in ascending order by the configuration's + * version_order option + * + * @return array + */ + public function getVersionLog() + { + return $this->getAdapter()->getVersionLog(); + } + + /** + * Sets the current version of the environment. + * + * @param int $version Environment Version + * @return $this + */ + public function setCurrentVersion($version) + { + $this->currentVersion = $version; + + return $this; + } + + /** + * Gets the current version of the environment. + * + * @return int + */ + public function getCurrentVersion() + { + // We don't cache this code as the current version is pretty volatile. + // that means they're no point in a setter then? + // maybe we should cache and call a reset() method every time a migration is run + $versions = $this->getVersions(); + $version = 0; + + if (!empty($versions)) { + $version = end($versions); + } + + $this->setCurrentVersion($version); + + return $this->currentVersion; + } + + /** + * Sets the database adapter. + * + * @param \Phinx\Db\Adapter\AdapterInterface $adapter Database Adapter + * @return $this + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * Gets the database adapter. + * + * @throws \RuntimeException + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function getAdapter() + { + if (isset($this->adapter)) { + return $this->adapter; + } + + $options = $this->getOptions(); + if (isset($options['connection'])) { + if (!($options['connection'] instanceof PDO)) { + throw new RuntimeException('The specified connection is not a PDO instance'); + } + + $options['connection']->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $options['adapter'] = $options['connection']->getAttribute(PDO::ATTR_DRIVER_NAME); + } + if (!isset($options['adapter'])) { + throw new RuntimeException('No adapter was specified for environment: ' . $this->getName()); + } + + $factory = AdapterFactory::instance(); + $adapter = $factory + ->getAdapter($options['adapter'], $options); + + // Automatically time the executed commands + $adapter = $factory->getWrapper('timed', $adapter); + + if (isset($options['wrapper'])) { + $adapter = $factory + ->getWrapper($options['wrapper'], $adapter); + } + + /** @var \Symfony\Component\Console\Input\InputInterface|null $input */ + $input = $this->getInput(); + if ($input) { + $adapter->setInput($this->getInput()); + } + + /** @var \Symfony\Component\Console\Output\OutputInterface|null $output */ + $output = $this->getOutput(); + if ($output) { + $adapter->setOutput($this->getOutput()); + } + + // Use the TablePrefixAdapter if table prefix/suffixes are in use + if ($adapter->hasOption('table_prefix') || $adapter->hasOption('table_suffix')) { + $adapter = AdapterFactory::instance() + ->getWrapper('prefix', $adapter); + } + + $this->setAdapter($adapter); + + return $adapter; + } + + /** + * Sets the schema table name. + * + * @param string $schemaTableName Schema Table Name + * @return $this + */ + public function setSchemaTableName($schemaTableName) + { + $this->schemaTableName = $schemaTableName; + + return $this; + } + + /** + * Gets the schema table name. + * + * @return string + */ + public function getSchemaTableName() + { + return $this->schemaTableName; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Migration/Migration.template.php.dist b/vendor/robmorgan/phinx/src/Phinx/Migration/Migration.template.php.dist new file mode 100644 index 0000000..12eb6fb --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Migration/Migration.template.php.dist @@ -0,0 +1,23 @@ + + */ +interface MigrationInterface +{ + /** + * @var string + */ + public const CHANGE = 'change'; + + /** + * @var string + */ + public const UP = 'up'; + + /** + * @var string + */ + public const DOWN = 'down'; + + /** + * @var string + */ + public const INIT = 'init'; + + /** + * Sets the database adapter. + * + * @param \Phinx\Db\Adapter\AdapterInterface $adapter Database Adapter + * @return \Phinx\Migration\MigrationInterface + */ + public function setAdapter(AdapterInterface $adapter); + + /** + * Gets the database adapter. + * + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function getAdapter(); + + /** + * Sets the input object to be used in migration object + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @return \Phinx\Migration\MigrationInterface + */ + public function setInput(InputInterface $input); + + /** + * Gets the input object to be used in migration object + * + * @return \Symfony\Component\Console\Input\InputInterface + */ + public function getInput(); + + /** + * Sets the output object to be used in migration object + * + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return \Phinx\Migration\MigrationInterface + */ + public function setOutput(OutputInterface $output); + + /** + * Gets the output object to be used in migration object + * + * @return \Symfony\Component\Console\Output\OutputInterface + */ + public function getOutput(); + + /** + * Gets the name. + * + * @return string + */ + public function getName(); + + /** + * Gets the detected environment + * + * @return string + */ + public function getEnvironment(); + + /** + * Sets the migration version number. + * + * @param int $version Version + * @return \Phinx\Migration\MigrationInterface + */ + public function setVersion($version); + + /** + * Gets the migration version number. + * + * @return int + */ + public function getVersion(); + + /** + * Sets whether this migration is being applied or reverted + * + * @param bool $isMigratingUp True if the migration is being applied + * @return \Phinx\Migration\MigrationInterface + */ + public function setMigratingUp($isMigratingUp); + + /** + * Gets whether this migration is being applied or reverted. + * True means that the migration is being applied. + * + * @return bool + */ + public function isMigratingUp(); + + /** + * Executes a SQL statement and returns the number of affected rows. + * + * @param string $sql SQL + * @return int + */ + public function execute($sql); + + /** + * Executes a SQL statement. + * + * The return type depends on the underlying adapter being used. To improve + * IDE auto-completion possibility, you can overwrite the query method + * phpDoc in your (typically custom abstract parent) migration class, where + * you can set the return type by the adapter in your current use. + * + * @param string $sql SQL + * @return mixed + */ + public function query($sql); + + /** + * Returns a new Query object that can be used to build complex SELECT, UPDATE, INSERT or DELETE + * queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @see https://api.cakephp.org/3.6/class-Cake.Database.Query.html + * @return \Cake\Database\Query + */ + public function getQueryBuilder(); + + /** + * Executes a query and returns only one row as an array. + * + * @param string $sql SQL + * @return array|false + */ + public function fetchRow($sql); + + /** + * Executes a query and returns an array of rows. + * + * @param string $sql SQL + * @return array + */ + public function fetchAll($sql); + + /** + * Insert data into a table. + * + * @deprecated since 0.10.0. Use $this->table($tableName)->insert($data)->save() instead. + * @param string $tableName Table name + * @param array $data Data + * @return void + */ + public function insert($tableName, $data); + + /** + * Create a new database. + * + * @param string $name Database Name + * @param array $options Options + * @return void + */ + public function createDatabase($name, $options); + + /** + * Drop a database. + * + * @param string $name Database Name + * @return void + */ + public function dropDatabase($name); + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table name + * @return bool + */ + public function hasTable($tableName); + + /** + * Returns an instance of the \Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Phinx\Db\Table + */ + public function table($tableName, $options); + + /** + * Perform checks on the migration, print a warning + * if there are potential problems. + * + * @param string|null $direction Direction + * @return void + */ + public function preFlightCheck($direction = null); + + /** + * Perform checks on the migration after completion + * + * Right now, the only check is whether all changes were committed + * + * @param string|null $direction direction of migration + * @return void + */ + public function postFlightCheck($direction = null); +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Seed/AbstractSeed.php b/vendor/robmorgan/phinx/src/Phinx/Seed/AbstractSeed.php new file mode 100644 index 0000000..ee259f4 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Seed/AbstractSeed.php @@ -0,0 +1,187 @@ + + */ +abstract class AbstractSeed implements SeedInterface +{ + /** + * @var \Phinx\Db\Adapter\AdapterInterface + */ + protected $adapter; + + /** + * @var \Symfony\Component\Console\Input\InputInterface + */ + protected $input; + + /** + * @var \Symfony\Component\Console\Output\OutputInterface + */ + protected $output; + + /** + * Override to specify dependencies for dependency injection from the configured PSR-11 container + */ + public function __construct() + { + } + + /** + * @inheritDoc + */ + public function run() + { + } + + /** + * Return seeds dependencies. + * + * @return array + */ + public function getDependencies() + { + return []; + } + + /** + * @inheritDoc + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * @inheritDoc + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * @inheritDoc + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + + return $this; + } + + /** + * @inheritDoc + */ + public function getInput() + { + return $this->input; + } + + /** + * @inheritDoc + */ + public function setOutput(OutputInterface $output) + { + $this->output = $output; + + return $this; + } + + /** + * @inheritDoc + */ + public function getOutput() + { + return $this->output; + } + + /** + * @inheritDoc + */ + public function getName() + { + return static::class; + } + + /** + * @inheritDoc + */ + public function execute($sql) + { + return $this->getAdapter()->execute($sql); + } + + /** + * @inheritDoc + */ + public function query($sql) + { + return $this->getAdapter()->query($sql); + } + + /** + * @inheritDoc + */ + public function fetchRow($sql) + { + return $this->getAdapter()->fetchRow($sql); + } + + /** + * @inheritDoc + */ + public function fetchAll($sql) + { + return $this->getAdapter()->fetchAll($sql); + } + + /** + * @inheritDoc + */ + public function insert($table, $data) + { + // convert to table object + if (is_string($table)) { + $table = new Table($table, [], $this->getAdapter()); + } + $table->insert($data)->save(); + } + + /** + * @inheritDoc + */ + public function hasTable($tableName) + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * @inheritDoc + */ + public function table($tableName, $options = []) + { + return new Table($tableName, $options, $this->getAdapter()); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Seed/Seed.template.php.dist b/vendor/robmorgan/phinx/src/Phinx/Seed/Seed.template.php.dist new file mode 100644 index 0000000..4b24906 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Seed/Seed.template.php.dist @@ -0,0 +1,20 @@ + + */ +interface SeedInterface +{ + /** + * @var string + */ + public const RUN = 'run'; + + /** + * @var string + */ + public const INIT = 'init'; + + /** + * Run the seeder. + * + * @return void + */ + public function run(); + + /** + * Sets the database adapter. + * + * @param \Phinx\Db\Adapter\AdapterInterface $adapter Database Adapter + * @return \Phinx\Seed\SeedInterface + */ + public function setAdapter(AdapterInterface $adapter); + + /** + * Gets the database adapter. + * + * @return \Phinx\Db\Adapter\AdapterInterface + */ + public function getAdapter(); + + /** + * Sets the input object to be used in migration object + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input + * @return \Phinx\Seed\SeedInterface + */ + public function setInput(InputInterface $input); + + /** + * Gets the input object to be used in migration object + * + * @return \Symfony\Component\Console\Input\InputInterface + */ + public function getInput(); + + /** + * Sets the output object to be used in migration object + * + * @param \Symfony\Component\Console\Output\OutputInterface $output Output + * @return \Phinx\Seed\SeedInterface + */ + public function setOutput(OutputInterface $output); + + /** + * Gets the output object to be used in migration object + * + * @return \Symfony\Component\Console\Output\OutputInterface + */ + public function getOutput(); + + /** + * Gets the name. + * + * @return string + */ + public function getName(); + + /** + * Executes a SQL statement and returns the number of affected rows. + * + * @param string $sql SQL + * @return int + */ + public function execute($sql); + + /** + * Executes a SQL statement. + * + * The return type depends on the underlying adapter being used. To improve + * IDE auto-completion possibility, you can overwrite the query method + * phpDoc in your (typically custom abstract parent) seed class, where + * you can set the return type by the adapter in your current use. + * + * @param string $sql SQL + * @return mixed + */ + public function query($sql); + + /** + * Executes a query and returns only one row as an array. + * + * @param string $sql SQL + * @return array|false + */ + public function fetchRow($sql); + + /** + * Executes a query and returns an array of rows. + * + * @param string $sql SQL + * @return array + */ + public function fetchAll($sql); + + /** + * Insert data into a table. + * + * @param string $tableName Table name + * @param array $data Data + * @return void + */ + public function insert($tableName, $data); + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table name + * @return bool + */ + public function hasTable($tableName); + + /** + * Returns an instance of the \Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Phinx\Db\Table + */ + public function table($tableName, $options); +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Util/Expression.php b/vendor/robmorgan/phinx/src/Phinx/Util/Expression.php new file mode 100644 index 0000000..352dab4 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Util/Expression.php @@ -0,0 +1,41 @@ +value = $value; + } + + /** + * @return string Returns the expression + */ + public function __toString() + { + return $this->value; + } + + /** + * @param string $value The expression + * @return self + */ + public static function from($value) + { + return new self($value); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Util/Literal.php b/vendor/robmorgan/phinx/src/Phinx/Util/Literal.php new file mode 100644 index 0000000..6ffbe59 --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Util/Literal.php @@ -0,0 +1,41 @@ +value = $value; + } + + /** + * @return string Returns the literal's value + */ + public function __toString() + { + return $this->value; + } + + /** + * @param string $value The literal's value + * @return self + */ + public static function from($value) + { + return new self($value); + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Util/Util.php b/vendor/robmorgan/phinx/src/Phinx/Util/Util.php new file mode 100644 index 0000000..6ac7a7b --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Util/Util.php @@ -0,0 +1,356 @@ +format(static::DATE_FORMAT); + } + + /** + * Gets an array of all the existing migration class names. + * + * @param string $path Path + * @return string[] + */ + public static function getExistingMigrationClassNames($path) + { + $classNames = []; + + if (!is_dir($path)) { + return $classNames; + } + + // filter the files to only get the ones that match our naming scheme + $phpFiles = static::getFiles($path); + + foreach ($phpFiles as $filePath) { + $fileName = basename($filePath); + if (preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName)) { + $classNames[] = static::mapFileNameToClassName($fileName); + } + } + + return $classNames; + } + + /** + * Get the version from the beginning of a file name. + * + * @param string $fileName File Name + * @return string + */ + public static function getVersionFromFileName($fileName) + { + $matches = []; + preg_match('/^[0-9]+/', basename($fileName), $matches); + + return $matches[0]; + } + + /** + * Turn migration names like 'CreateUserTable' into file names like + * '12345678901234_create_user_table.php' or 'LimitResourceNamesTo30Chars' into + * '12345678901234_limit_resource_names_to_30_chars.php'. + * + * @param string $className Class Name + * @return string + */ + public static function mapClassNameToFileName($className) + { + $snake = function ($matches) { + return '_' . strtolower($matches[0]); + }; + $fileName = preg_replace_callback('/\d+|[A-Z]/', $snake, $className); + $fileName = static::getCurrentTimestamp() . "$fileName.php"; + + return $fileName; + } + + /** + * Turn file names like '12345678901234_create_user_table.php' into class + * names like 'CreateUserTable'. + * + * @param string $fileName File Name + * @return string + */ + public static function mapFileNameToClassName(string $fileName): string + { + $matches = []; + if (preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName, $matches)) { + $fileName = $matches[1]; + } elseif (preg_match(static::MIGRATION_FILE_NAME_NO_NAME_PATTERN, $fileName)) { + return 'V' . substr($fileName, 0, strlen($fileName) - 4); + } + + $className = str_replace('_', '', ucwords($fileName, '_')); + + return $className; + } + + /** + * Check if a migration class name is unique regardless of the + * timestamp. + * + * This method takes a class name and a path to a migrations directory. + * + * Migration class names must be in PascalCase format but consecutive + * capitals are allowed. + * e.g: AddIndexToPostsTable or CustomHTMLTitle. + * + * @param string $className Class Name + * @param string $path Path + * @return bool + */ + public static function isUniqueMigrationClassName($className, $path) + { + $existingClassNames = static::getExistingMigrationClassNames($path); + + return !in_array($className, $existingClassNames, true); + } + + /** + * Check if a migration/seed class name is valid. + * + * Migration & Seed class names must be in CamelCase format. + * e.g: CreateUserTable, AddIndexToPostsTable or UserSeeder. + * + * Single words are not allowed on their own. + * + * @param string $className Class Name + * @return bool + */ + public static function isValidPhinxClassName($className) + { + return (bool)preg_match(static::CLASS_NAME_PATTERN, $className); + } + + /** + * Check if a migration file name is valid. + * + * @param string $fileName File Name + * @return bool + */ + public static function isValidMigrationFileName(string $fileName): bool + { + return (bool)preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName) + || (bool)preg_match(static::MIGRATION_FILE_NAME_NO_NAME_PATTERN, $fileName); + } + + /** + * Check if a seed file name is valid. + * + * @param string $fileName File Name + * @return bool + */ + public static function isValidSeedFileName($fileName) + { + return (bool)preg_match(static::SEED_FILE_NAME_PATTERN, $fileName); + } + + /** + * Expands a set of paths with curly braces (if supported by the OS). + * + * @param string[] $paths Paths + * @return string[] + */ + public static function globAll(array $paths) + { + $result = []; + + foreach ($paths as $path) { + $result = array_merge($result, static::glob($path)); + } + + return $result; + } + + /** + * Expands a path with curly braces (if supported by the OS). + * + * @param string $path Path + * @return string[] + */ + public static function glob($path) + { + return glob($path, defined('GLOB_BRACE') ? GLOB_BRACE : 0); + } + + /** + * Takes the path to a php file and attempts to include it if readable + * + * @param string $filename Filename + * @param \Symfony\Component\Console\Input\InputInterface|null $input Input + * @param \Symfony\Component\Console\Output\OutputInterface|null $output Output + * @param \Phinx\Console\Command\AbstractCommand|mixed|null $context Context + * @throws \Exception + * @return string + */ + public static function loadPhpFile($filename, ?InputInterface $input = null, ?OutputInterface $output = null, $context = null) + { + $filePath = realpath($filename); + if (!file_exists($filePath)) { + throw new Exception(sprintf("File does not exist: %s \n", $filename)); + } + + /** + * I lifed this from phpunits FileLoader class + * + * @see https://github.com/sebastianbergmann/phpunit/pull/2751 + */ + $isReadable = @fopen($filePath, 'r') !== false; + + if (!$isReadable) { + throw new Exception(sprintf("Cannot open file %s \n", $filename)); + } + + // prevent this to be propagated to the included file + unset($isReadable); + + include_once $filePath; + + return $filePath; + } + + /** + * Given an array of paths, return all unique PHP files that are in them + * + * @param string|string[] $paths Path or array of paths to get .php files. + * @return string[] + */ + public static function getFiles($paths) + { + $files = static::globAll(array_map(function ($path) { + return $path . DIRECTORY_SEPARATOR . '*.php'; + }, (array)$paths)); + // glob() can return the same file multiple times + // This will cause the migration to fail with a + // false assumption of duplicate migrations + // http://php.net/manual/en/function.glob.php#110340 + $files = array_unique($files); + + return $files; + } + + /** + * Attempt to remove the current working directory from a path for output. + * + * @param string $path Path to remove cwd prefix from + * @return string + */ + public static function relativePath($path) + { + $realpath = realpath($path); + if ($realpath !== false) { + $path = $realpath; + } + + $cwd = getcwd(); + if ($cwd !== false) { + $cwd .= DIRECTORY_SEPARATOR; + $cwdLen = strlen($cwd); + + if (substr($path, 0, $cwdLen) === $cwd) { + $path = substr($path, $cwdLen); + } + } + + return $path; + } + + /** + * Parses DSN string into db config array. + * + * @param string $dsn DSN string + * @return array + */ + public static function parseDsn(string $dsn): array + { + $pattern = <<<'REGEXP' +{ + ^ + (?: + (?P[\w\\\\]+):// + ) + (?: + (?P.*?) + (?: + :(?P.*?) + )? + @ + )? + (?: + (?P[^?#/:@]+) + (?: + :(?P\d+) + )? + )? + (?: + /(?P[^?#]*) + )? + (?: + \?(?P[^#]*) + )? + $ +}x +REGEXP; + + if (!preg_match($pattern, $dsn, $parsed)) { + return []; + } + + // filter out everything except the matched groups + $config = array_intersect_key($parsed, array_flip(['adapter', 'user', 'pass', 'host', 'port', 'name'])); + $config = array_filter($config); + + parse_str($parsed['query'] ?? '', $query); + $config = array_merge($query, $config); + + return $config; + } +} diff --git a/vendor/robmorgan/phinx/src/Phinx/Wrapper/TextWrapper.php b/vendor/robmorgan/phinx/src/Phinx/Wrapper/TextWrapper.php new file mode 100644 index 0000000..afa137d --- /dev/null +++ b/vendor/robmorgan/phinx/src/Phinx/Wrapper/TextWrapper.php @@ -0,0 +1,240 @@ + + */ +class TextWrapper +{ + /** + * @var \Phinx\Console\PhinxApplication + */ + protected $app; + + /** + * @var array + */ + protected $options; + + /** + * @var int + */ + protected $exitCode; + + /** + * @param \Phinx\Console\PhinxApplication $app Application + * @param array $options Options + */ + public function __construct(PhinxApplication $app, array $options = []) + { + $this->app = $app; + $this->options = $options; + } + + /** + * Get the application instance. + * + * @return \Phinx\Console\PhinxApplication + */ + public function getApp() + { + return $this->app; + } + + /** + * Returns the exit code from the last run command. + * + * @return int + */ + public function getExitCode() + { + return $this->exitCode; + } + + /** + * Returns the output from running the "status" command. + * + * @param string|null $env environment name (optional) + * @return string + */ + public function getStatus($env = null) + { + $command = ['status']; + if ($env ?: $this->hasOption('environment')) { + $command += ['-e' => $env ?: $this->getOption('environment')]; + } + if ($this->hasOption('configuration')) { + $command += ['-c' => $this->getOption('configuration')]; + } + if ($this->hasOption('parser')) { + $command += ['-p' => $this->getOption('parser')]; + } + if ($this->hasOption('format')) { + $command += ['-f' => $this->getOption('format')]; + } + + return $this->executeRun($command); + } + + /** + * Returns the output from running the "migrate" command. + * + * @param string|null $env environment name (optional) + * @param string|null $target target version (optional) + * @return string + */ + public function getMigrate($env = null, $target = null) + { + $command = ['migrate']; + if ($env ?: $this->hasOption('environment')) { + $command += ['-e' => $env ?: $this->getOption('environment')]; + } + if ($this->hasOption('configuration')) { + $command += ['-c' => $this->getOption('configuration')]; + } + if ($this->hasOption('parser')) { + $command += ['-p' => $this->getOption('parser')]; + } + if ($target) { + $command += ['-t' => $target]; + } + + return $this->executeRun($command); + } + + /** + * Returns the output from running the "seed:run" command. + * + * @param string|null $env Environment name + * @param string|null $target Target version + * @param string[]|string|null $seed Array of seed names or seed name + * @return string + */ + public function getSeed($env = null, $target = null, $seed = null) + { + $command = ['seed:run']; + if ($env ?: $this->hasOption('environment')) { + $command += ['-e' => $env ?: $this->getOption('environment')]; + } + if ($this->hasOption('configuration')) { + $command += ['-c' => $this->getOption('configuration')]; + } + if ($this->hasOption('parser')) { + $command += ['-p' => $this->getOption('parser')]; + } + if ($target) { + $command += ['-t' => $target]; + } + if ($seed) { + $seed = (array)$seed; + $command += ['-s' => $seed]; + } + + return $this->executeRun($command); + } + + /** + * Returns the output from running the "rollback" command. + * + * @param string|null $env Environment name (optional) + * @param mixed $target Target version, or 0 (zero) fully revert (optional) + * @return string + */ + public function getRollback($env = null, $target = null) + { + $command = ['rollback']; + if ($env ?: $this->hasOption('environment')) { + $command += ['-e' => $env ?: $this->getOption('environment')]; + } + if ($this->hasOption('configuration')) { + $command += ['-c' => $this->getOption('configuration')]; + } + if ($this->hasOption('parser')) { + $command += ['-p' => $this->getOption('parser')]; + } + if (isset($target)) { + // Need to use isset() with rollback, because -t0 is a valid option! + // See https://book.cakephp.org/phinx/0/en/commands.html#the-rollback-command + $command += ['-t' => $target]; + } + + return $this->executeRun($command); + } + + /** + * Check option from options array + * + * @param string $key Key + * @return bool + */ + protected function hasOption($key) + { + return isset($this->options[$key]); + } + + /** + * Get option from options array + * + * @param string $key Key + * @return string|null + */ + protected function getOption($key) + { + if (!isset($this->options[$key])) { + return null; + } + + return $this->options[$key]; + } + + /** + * Set option in options array + * + * @param string $key Key + * @param string $value Value + * @return $this + */ + public function setOption($key, $value) + { + $this->options[$key] = $value; + + return $this; + } + + /** + * Execute a command, capturing output and storing the exit code. + * + * @param array $command Command + * @return string + */ + protected function executeRun(array $command) + { + // Output will be written to a temporary stream, so that it can be + // collected after running the command. + $stream = fopen('php://temp', 'w+'); + + // Execute the command, capturing the output in the temporary stream + // and storing the exit code for debugging purposes. + $this->exitCode = $this->app->doRun(new ArrayInput($command), new StreamOutput($stream)); + + // Get the output of the command and close the stream, which will + // destroy the temporary file. + $result = stream_get_contents($stream, -1, 0); + fclose($stream); + + return $result; + } +} diff --git a/vendor/robmorgan/phinx/src/composer_autoloader.php b/vendor/robmorgan/phinx/src/composer_autoloader.php new file mode 100644 index 0000000..2cde666 --- /dev/null +++ b/vendor/robmorgan/phinx/src/composer_autoloader.php @@ -0,0 +1,23 @@ +container = $container; + } + + /** + * Get container + * + * @return ContainerInterface + */ + public function getContainer() + { + return $this->container; + } + + /** + * Add middleware + * + * This method prepends new middleware to the app's middleware stack. + * + * @param callable|string $callable The callback routine + * + * @return static + */ + public function add($callable) + { + return $this->addMiddleware(new DeferredCallable($callable, $this->container)); + } + + /** + * Calling a non-existent method on App checks to see if there's an item + * in the container that is callable and if so, calls it. + * + * @param string $method + * @param array $args + * + * @return mixed + * + * @throws BadMethodCallException + */ + public function __call($method, $args) + { + if ($this->container->has($method)) { + $obj = $this->container->get($method); + if (is_callable($obj)) { + return call_user_func_array($obj, $args); + } + } + + throw new BadMethodCallException("Method $method is not a valid method"); + } + + /** + * Add GET route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function get($pattern, $callable) + { + return $this->map(['GET'], $pattern, $callable); + } + + /** + * Add POST route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function post($pattern, $callable) + { + return $this->map(['POST'], $pattern, $callable); + } + + /** + * Add PUT route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function put($pattern, $callable) + { + return $this->map(['PUT'], $pattern, $callable); + } + + /** + * Add PATCH route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function patch($pattern, $callable) + { + return $this->map(['PATCH'], $pattern, $callable); + } + + /** + * Add DELETE route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function delete($pattern, $callable) + { + return $this->map(['DELETE'], $pattern, $callable); + } + + /** + * Add OPTIONS route + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function options($pattern, $callable) + { + return $this->map(['OPTIONS'], $pattern, $callable); + } + + /** + * Add route for any HTTP method + * + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function any($pattern, $callable) + { + return $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $pattern, $callable); + } + + /** + * Add route with multiple methods + * + * @param string[] $methods Numeric array of HTTP method names + * @param string $pattern The route URI pattern + * @param callable|string $callable The route callback routine + * + * @return RouteInterface + */ + public function map(array $methods, $pattern, $callable) + { + if ($callable instanceof Closure) { + $callable = $callable->bindTo($this->container); + } + + $route = $this->container->get('router')->map($methods, $pattern, $callable); + if (is_callable([$route, 'setContainer'])) { + $route->setContainer($this->container); + } + + if (is_callable([$route, 'setOutputBuffering'])) { + $route->setOutputBuffering($this->container->get('settings')['outputBuffering']); + } + + return $route; + } + + /** + * Add a route that sends an HTTP redirect + * + * @param string $from + * @param string|UriInterface $to + * @param int $status + * + * @return RouteInterface + */ + public function redirect($from, $to, $status = 302) + { + $handler = function ($request, ResponseInterface $response) use ($to, $status) { + return $response->withHeader('Location', (string)$to)->withStatus($status); + }; + + return $this->get($from, $handler); + } + + /** + * Add a route group + * + * This method accepts a route pattern and a callback. All route + * declarations in the callback will be prepended by the group(s) + * that it is in. + * + * @param string $pattern + * @param callable|Closure $callable + * + * @return RouteGroupInterface + */ + public function group($pattern, $callable) + { + /** @var RouterInterface $router */ + $router = $this->container->get('router'); + + /** @var RouteGroup $group */ + $group = $router->pushGroup($pattern, $callable); + $group->setContainer($this->container); + $group($this); + + $router->popGroup(); + return $group; + } + + /** + * Run application + * + * This method traverses the application middleware stack and then sends the + * resultant Response object to the HTTP client. + * + * @param bool|false $silent + * + * @return ResponseInterface + * + * @throws Exception + * @throws Throwable + */ + public function run($silent = false) + { + $response = $this->container->get('response'); + + try { + ob_start(); + $response = $this->process($this->container->get('request'), $response); + } catch (InvalidMethodException $e) { + $response = $this->processInvalidMethod($e->getRequest(), $response); + } finally { + $output = ob_get_clean(); + } + + if (!empty($output) && $response->getBody()->isWritable()) { + $outputBuffering = $this->container->get('settings')['outputBuffering']; + if ($outputBuffering === 'prepend') { + // prepend output buffer content + $body = new Http\Body(fopen('php://temp', 'r+')); + $body->write($output . $response->getBody()); + $response = $response->withBody($body); + } elseif ($outputBuffering === 'append') { + // append output buffer content + $response->getBody()->write($output); + } + } + + $response = $this->finalize($response); + + if (!$silent) { + $this->respond($response); + } + + return $response; + } + + /** + * Pull route info for a request with a bad method to decide whether to + * return a not-found error (default) or a bad-method error, then run + * the handler for that error, returning the resulting response. + * + * Used for cases where an incoming request has an unrecognized method, + * rather than throwing an exception and not catching it all the way up. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + * + * @throws ContainerException + */ + protected function processInvalidMethod(ServerRequestInterface $request, ResponseInterface $response) + { + $router = $this->container->get('router'); + if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) { + $router->setBasePath($request->getUri()->getBasePath()); + } + + $request = $this->dispatchRouterAndPrepareRoute($request, $router); + $routeInfo = $request->getAttribute('routeInfo', [RouterInterface::DISPATCH_STATUS => Dispatcher::NOT_FOUND]); + + if ($routeInfo[RouterInterface::DISPATCH_STATUS] === Dispatcher::METHOD_NOT_ALLOWED) { + return $this->handleException( + new MethodNotAllowedException($request, $response, $routeInfo[RouterInterface::ALLOWED_METHODS]), + $request, + $response + ); + } + + return $this->handleException(new NotFoundException($request, $response), $request, $response); + } + + /** + * Process a request + * + * This method traverses the application middleware stack and then returns the + * resultant Response object. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + * + * @throws Exception + * @throws Throwable + */ + public function process(ServerRequestInterface $request, ResponseInterface $response) + { + // Ensure basePath is set + $router = $this->container->get('router'); + if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) { + $router->setBasePath($request->getUri()->getBasePath()); + } + + // Dispatch the Router first if the setting for this is on + if ($this->container->get('settings')['determineRouteBeforeAppMiddleware'] === true) { + // Dispatch router (note: you won't be able to alter routes after this) + $request = $this->dispatchRouterAndPrepareRoute($request, $router); + } + + // Traverse middleware stack + try { + $response = $this->callMiddlewareStack($request, $response); + } catch (Exception $e) { + $response = $this->handleException($e, $request, $response); + } catch (Throwable $e) { + $response = $this->handlePhpError($e, $request, $response); + } + + return $response; + } + + /** + * Send the response to the client + * + * @param ResponseInterface $response + */ + public function respond(ResponseInterface $response) + { + // Send response + if (!headers_sent()) { + // Headers + foreach ($response->getHeaders() as $name => $values) { + $first = stripos($name, 'Set-Cookie') === 0 ? false : true; + foreach ($values as $value) { + header(sprintf('%s: %s', $name, $value), $first); + $first = false; + } + } + + // Set the status _after_ the headers, because of PHP's "helpful" behavior with location headers. + // See https://github.com/slimphp/Slim/issues/1730 + + // Status + header(sprintf( + 'HTTP/%s %s %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ), true, $response->getStatusCode()); + } + + // Body + $request = $this->container->get('request'); + if (!$this->isEmptyResponse($response) && !$this->isHeadRequest($request)) { + $body = $response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + $settings = $this->container->get('settings'); + $chunkSize = $settings['responseChunkSize']; + + $contentLength = $response->getHeaderLine('Content-Length'); + if (!$contentLength) { + $contentLength = $body->getSize(); + } + + + if (isset($contentLength)) { + $amountToRead = $contentLength; + while ($amountToRead > 0 && !$body->eof()) { + $data = $body->read(min((int)$chunkSize, (int)$amountToRead)); + echo $data; + + $amountToRead -= strlen($data); + + if (connection_status() != CONNECTION_NORMAL) { + break; + } + } + } else { + while (!$body->eof()) { + echo $body->read((int)$chunkSize); + if (connection_status() != CONNECTION_NORMAL) { + break; + } + } + } + } + } + + /** + * Invoke application + * + * This method implements the middleware interface. It receives + * Request and Response objects, and it returns a Response object + * after compiling the routes registered in the Router and dispatching + * the Request object to the appropriate Route callback routine. + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * + * @return ResponseInterface + * + * @throws MethodNotAllowedException + * @throws NotFoundException + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response) + { + // Get the route info + $routeInfo = $request->getAttribute('routeInfo'); + + /** @var RouterInterface $router */ + $router = $this->container->get('router'); + + // If router hasn't been dispatched or the URI changed then dispatch + if (null === $routeInfo || ($routeInfo['request'] !== [$request->getMethod(), (string) $request->getUri()])) { + $request = $this->dispatchRouterAndPrepareRoute($request, $router); + $routeInfo = $request->getAttribute('routeInfo'); + } + + if ($routeInfo[0] === Dispatcher::FOUND) { + $route = $router->lookupRoute($routeInfo[1]); + return $route->run($request, $response); + } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { + if (!$this->container->has('notAllowedHandler')) { + throw new MethodNotAllowedException($request, $response, $routeInfo[1]); + } + /** @var callable $notAllowedHandler */ + $notAllowedHandler = $this->container->get('notAllowedHandler'); + return $notAllowedHandler($request, $response, $routeInfo[1]); + } + + if (!$this->container->has('notFoundHandler')) { + throw new NotFoundException($request, $response); + } + /** @var callable $notFoundHandler */ + $notFoundHandler = $this->container->get('notFoundHandler'); + return $notFoundHandler($request, $response); + } + + /** + * Perform a sub-request from within an application route + * + * This method allows you to prepare and initiate a sub-request, run within + * the context of the current request. This WILL NOT issue a remote HTTP + * request. Instead, it will route the provided URL, method, headers, + * cookies, body, and server variables against the set of registered + * application routes. The result response object is returned. + * + * @param string $method The request method (e.g., GET, POST, PUT, etc.) + * @param string $path The request URI path + * @param string $query The request URI query string + * @param array $headers The request headers (key-value array) + * @param array $cookies The request cookies (key-value array) + * @param string $bodyContent The request body + * @param ResponseInterface $response The response object (optional) + * + * @return ResponseInterface + * + * @throws MethodNotAllowedException + * @throws NotFoundException + */ + public function subRequest( + $method, + $path, + $query = '', + array $headers = [], + array $cookies = [], + $bodyContent = '', + ResponseInterface $response = null + ) { + $env = $this->container->get('environment'); + $uri = Uri::createFromEnvironment($env)->withPath($path)->withQuery($query); + $headers = new Headers($headers); + $serverParams = $env->all(); + $body = new Body(fopen('php://temp', 'r+')); + $body->write($bodyContent); + $body->rewind(); + $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); + + if (!$response) { + $response = $this->container->get('response'); + } + + return $this($request, $response); + } + + /** + * Dispatch the router to find the route. Prepare the route for use. + * + * @param ServerRequestInterface $request + * @param RouterInterface $router + * + * @return ServerRequestInterface + */ + protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request, RouterInterface $router) + { + $routeInfo = $router->dispatch($request); + + if ($routeInfo[0] === Dispatcher::FOUND) { + $routeArguments = []; + foreach ($routeInfo[2] as $k => $v) { + $routeArguments[$k] = urldecode($v); + } + + $route = $router->lookupRoute($routeInfo[1]); + $route->prepare($request, $routeArguments); + + // add route to the request's attributes in case a middleware or handler needs access to the route + $request = $request->withAttribute('route', $route); + } + + $routeInfo['request'] = [$request->getMethod(), (string) $request->getUri()]; + + return $request->withAttribute('routeInfo', $routeInfo); + } + + /** + * Finalize response + * + * @param ResponseInterface $response + * + * @return ResponseInterface + * + * @throws RuntimeException + */ + protected function finalize(ResponseInterface $response) + { + // stop PHP sending a Content-Type automatically + ini_set('default_mimetype', ''); + + $request = $this->container->get('request'); + if ($this->isEmptyResponse($response) && !$this->isHeadRequest($request)) { + return $response->withoutHeader('Content-Type')->withoutHeader('Content-Length'); + } + + // Add Content-Length header if `addContentLengthHeader` setting is set + if (isset($this->container->get('settings')['addContentLengthHeader']) && + $this->container->get('settings')['addContentLengthHeader'] == true) { + if (ob_get_length() > 0) { + throw new RuntimeException("Unexpected data in output buffer. " . + "Maybe you have characters before an opening getBody()->getSize(); + if ($size !== null && !$response->hasHeader('Content-Length')) { + $response = $response->withHeader('Content-Length', (string) $size); + } + } + + // clear the body if this is a HEAD request + if ($this->isHeadRequest($request)) { + return $response->withBody(new Body(fopen('php://temp', 'r+'))); + } + + return $response; + } + + /** + * Helper method, which returns true if the provided response must not output a body and false + * if the response could have a body. + * + * @see https://tools.ietf.org/html/rfc7231 + * + * @param ResponseInterface $response + * + * @return bool + */ + protected function isEmptyResponse(ResponseInterface $response) + { + if (method_exists($response, 'isEmpty')) { + return $response->isEmpty(); + } + + return in_array($response->getStatusCode(), [204, 205, 304]); + } + + /** + * Helper method to check if the current request is a HEAD request + * + * @param RequestInterface $request + * + * @return bool + */ + protected function isHeadRequest(RequestInterface $request) + { + return strtoupper($request->getMethod()) === 'HEAD'; + } + + /** + * Call relevant handler from the Container if needed. If it doesn't exist, + * then just re-throw. + * + * @param Exception $e + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + * + * @throws Exception If a handler is needed and not found + */ + protected function handleException(Exception $e, ServerRequestInterface $request, ResponseInterface $response) + { + if ($e instanceof MethodNotAllowedException) { + $handler = 'notAllowedHandler'; + $params = [$e->getRequest(), $e->getResponse(), $e->getAllowedMethods()]; + } elseif ($e instanceof NotFoundException) { + $handler = 'notFoundHandler'; + $params = [$e->getRequest(), $e->getResponse(), $e]; + } elseif ($e instanceof SlimException) { + // This is a Stop exception and contains the response + return $e->getResponse(); + } else { + // Other exception, use $request and $response params + $handler = 'errorHandler'; + $params = [$request, $response, $e]; + } + + if ($this->container->has($handler)) { + $callable = $this->container->get($handler); + // Call the registered handler + return call_user_func_array($callable, $params); + } + + // No handlers found, so just throw the exception + throw $e; + } + + /** + * Call relevant handler from the Container if needed. If it doesn't exist, + * then just re-throw. + * + * @param Throwable $e + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + * + * @throws Throwable + */ + protected function handlePhpError(Throwable $e, ServerRequestInterface $request, ResponseInterface $response) + { + $handler = 'phpErrorHandler'; + $params = [$request, $response, $e]; + + if ($this->container->has($handler)) { + $callable = $this->container->get($handler); + // Call the registered handler + return call_user_func_array($callable, $params); + } + + // No handlers found, so just throw the exception + throw $e; + } +} diff --git a/vendor/slim/slim/Slim/CallableResolver.php b/vendor/slim/slim/Slim/CallableResolver.php new file mode 100644 index 0000000..577e9df --- /dev/null +++ b/vendor/slim/slim/Slim/CallableResolver.php @@ -0,0 +1,119 @@ +container = $container; + } + + /** + * Resolve toResolve into a closure so that the router can dispatch. + * + * If toResolve is of the format 'class:method', then try to extract 'class' + * from the container otherwise instantiate it and then dispatch 'method'. + * + * @param callable|string $toResolve + * + * @return callable + * + * @throws RuntimeException If the callable does not exist + * @throws RuntimeException If the callable is not resolvable + */ + public function resolve($toResolve) + { + if (is_callable($toResolve)) { + return $toResolve; + } + + $resolved = $toResolve; + + if (is_string($toResolve)) { + list($class, $method) = $this->parseCallable($toResolve); + $resolved = $this->resolveCallable($class, $method); + } + + $this->assertCallable($resolved); + return $resolved; + } + + /** + * Extract class and method from toResolve + * + * @param string $toResolve + * + * @return array + */ + protected function parseCallable($toResolve) + { + if (preg_match(self::CALLABLE_PATTERN, $toResolve, $matches)) { + return [$matches[1], $matches[2]]; + } + + return [$toResolve, '__invoke']; + } + + /** + * Check if string is something in the DIC + * that's callable or is a class name which has an __invoke() method. + * + * @param string $class + * @param string $method + * + * @return callable + * + * @throws RuntimeException if the callable does not exist + */ + protected function resolveCallable($class, $method) + { + if ($this->container->has($class)) { + return [$this->container->get($class), $method]; + } + + if (!class_exists($class)) { + throw new RuntimeException(sprintf('Callable %s does not exist', $class)); + } + + return [new $class($this->container), $method]; + } + + /** + * @param Callable $callable + * + * @throws RuntimeException if the callable is not resolvable + */ + protected function assertCallable($callable) + { + if (!is_callable($callable)) { + throw new RuntimeException(sprintf( + '%s is not resolvable', + is_array($callable) || is_object($callable) ? json_encode($callable) : $callable + )); + } + } +} diff --git a/vendor/slim/slim/Slim/CallableResolverAwareTrait.php b/vendor/slim/slim/Slim/CallableResolverAwareTrait.php new file mode 100644 index 0000000..cdf7367 --- /dev/null +++ b/vendor/slim/slim/Slim/CallableResolverAwareTrait.php @@ -0,0 +1,43 @@ +container instanceof ContainerInterface) { + return $callable; + } + + /** @var CallableResolverInterface $resolver */ + $resolver = $this->container->get('callableResolver'); + + return $resolver->resolve($callable); + } +} diff --git a/vendor/slim/slim/Slim/Collection.php b/vendor/slim/slim/Slim/Collection.php new file mode 100644 index 0000000..2eaec44 --- /dev/null +++ b/vendor/slim/slim/Slim/Collection.php @@ -0,0 +1,169 @@ +replace($items); + } + + /** + * {@inheritdoc} + */ + public function set($key, $value) + { + $this->data[$key] = $value; + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + return $this->has($key) ? $this->data[$key] : $default; + } + + /** + * {@inheritdoc} + */ + public function replace(array $items) + { + foreach ($items as $key => $value) { + $this->set($key, $value); + } + } + + /** + * {@inheritdoc} + */ + public function all() + { + return $this->data; + } + + /** + * Get collection keys + * + * @return array The collection's source data keys + */ + public function keys() + { + return array_keys($this->data); + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + return array_key_exists($key, $this->data); + } + + /** + * {@inheritdoc} + */ + public function remove($key) + { + unset($this->data[$key]); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->data = []; + } + + /** + * Does this collection have a given key? + * + * @param string $key The data key + * + * @return bool + */ + public function offsetExists($key) + { + return $this->has($key); + } + + /** + * Get collection item for key + * + * @param string $key The data key + * + * @return mixed The key's value, or the default value + */ + public function offsetGet($key) + { + return $this->get($key); + } + + /** + * Set collection item + * + * @param string $key The data key + * @param mixed $value The data value + */ + public function offsetSet($key, $value) + { + $this->set($key, $value); + } + + /** + * Remove item from collection + * + * @param string $key The data key + */ + public function offsetUnset($key) + { + $this->remove($key); + } + + /** + * Get number of items in collection + * + * @return int + */ + public function count() + { + return count($this->data); + } + + /** + * Get collection iterator + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->data); + } +} diff --git a/vendor/slim/slim/Slim/Container.php b/vendor/slim/slim/Slim/Container.php new file mode 100644 index 0000000..4641f68 --- /dev/null +++ b/vendor/slim/slim/Slim/Container.php @@ -0,0 +1,175 @@ + '1.1', + 'responseChunkSize' => 4096, + 'outputBuffering' => 'append', + 'determineRouteBeforeAppMiddleware' => false, + 'displayErrorDetails' => false, + 'addContentLengthHeader' => true, + 'routerCacheFile' => false, + ]; + + /** + * @param array $values The parameters or objects. + */ + public function __construct(array $values = []) + { + parent::__construct($values); + + $userSettings = isset($values['settings']) ? $values['settings'] : []; + $this->registerDefaultServices($userSettings); + } + + /** + * This function registers the default services that Slim needs to work. + * + * All services are shared, they are registered such that the + * same instance is returned on subsequent calls. + * + * @param array $userSettings Associative array of application settings + * + * @return void + */ + private function registerDefaultServices($userSettings) + { + $defaultSettings = $this->defaultSettings; + + /** + * This service MUST return an array or an instance of ArrayAccess. + * + * @return array|ArrayAccess + */ + $this['settings'] = function () use ($userSettings, $defaultSettings) { + return new Collection(array_merge($defaultSettings, $userSettings)); + }; + + $defaultProvider = new DefaultServicesProvider(); + $defaultProvider->register($this); + } + + /** + * Finds an entry of the container by its identifier and returns it. + * + * @param string $id Identifier of the entry to look for. + * + * @return mixed + * + * @throws InvalidArgumentException Thrown when an offset cannot be found in the Pimple container + * @throws SlimContainerException Thrown when an exception is + * not an instance of ContainerExceptionInterface + * @throws ContainerValueNotFoundException No entry was found for this identifier. + * @throws ContainerExceptionInterface Error while retrieving the entry. + */ + public function get($id) + { + if (!$this->offsetExists($id)) { + throw new ContainerValueNotFoundException(sprintf('Identifier "%s" is not defined.', $id)); + } + try { + return $this->offsetGet($id); + } catch (InvalidArgumentException $exception) { + if ($this->exceptionThrownByContainer($exception)) { + throw new SlimContainerException( + sprintf('Container error while retrieving "%s"', $id), + null, + $exception + ); + } else { + throw $exception; + } + } + } + + /** + * Tests whether an exception needs to be recast for compliance with psr/container. This will be if the + * exception was thrown by Pimple. + * + * @param InvalidArgumentException $exception + * + * @return bool + */ + private function exceptionThrownByContainer(InvalidArgumentException $exception) + { + $trace = $exception->getTrace()[0]; + + return $trace['class'] === PimpleContainer::class && $trace['function'] === 'offsetGet'; + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * @param string $id Identifier of the entry to look for. + * + * @return boolean + */ + public function has($id) + { + return $this->offsetExists($id); + } + + /** + * @param string $name + * + * @return mixed + * + * @throws InvalidArgumentException Thrown when an offset cannot be found in the Pimple container + * @throws SlimContainerException Thrown when an exception is not + * an instance of ContainerExceptionInterface + * @throws ContainerValueNotFoundException No entry was found for this identifier. + * @throws ContainerExceptionInterface Error while retrieving the entry. + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * @param string $name + * @return bool + */ + public function __isset($name) + { + return $this->has($name); + } +} diff --git a/vendor/slim/slim/Slim/DefaultServicesProvider.php b/vendor/slim/slim/Slim/DefaultServicesProvider.php new file mode 100644 index 0000000..48a8d6e --- /dev/null +++ b/vendor/slim/slim/Slim/DefaultServicesProvider.php @@ -0,0 +1,194 @@ +get('environment')); + }; + } + + if (!isset($container['response'])) { + /** + * PSR-7 Response object + * + * @param Container $container + * + * @return ResponseInterface + */ + $container['response'] = function ($container) { + $headers = new Headers(['Content-Type' => 'text/html; charset=UTF-8']); + $response = new Response(200, $headers); + + return $response->withProtocolVersion($container->get('settings')['httpVersion']); + }; + } + + if (!isset($container['router'])) { + /** + * This service MUST return a shared instance of \Slim\Interfaces\RouterInterface. + * + * @param Container $container + * + * @return RouterInterface + */ + $container['router'] = function ($container) { + $routerCacheFile = false; + if (isset($container->get('settings')['routerCacheFile'])) { + $routerCacheFile = $container->get('settings')['routerCacheFile']; + } + + $router = (new Router)->setCacheFile($routerCacheFile); + if (method_exists($router, 'setContainer')) { + $router->setContainer($container); + } + + return $router; + }; + } + + if (!isset($container['foundHandler'])) { + /** + * This service MUST return a SHARED instance InvocationStrategyInterface. + * + * @return InvocationStrategyInterface + */ + $container['foundHandler'] = function () { + return new RequestResponse; + }; + } + + if (!isset($container['phpErrorHandler'])) { + /** + * This service MUST return a callable that accepts three arguments: + * + * 1. Instance of ServerRequestInterface + * 2. Instance of ResponseInterface + * 3. Instance of Error + * + * The callable MUST return an instance of ResponseInterface. + * + * @param Container $container + * + * @return callable + */ + $container['phpErrorHandler'] = function ($container) { + return new PhpError($container->get('settings')['displayErrorDetails']); + }; + } + + if (!isset($container['errorHandler'])) { + /** + * This service MUST return a callable that accepts three arguments: + * + * 1. Instance of \Psr\Http\Message\ServerRequestInterface + * 2. Instance of \Psr\Http\Message\ResponseInterface + * 3. Instance of \Exception + * + * The callable MUST return an instance of ResponseInterface. + * + * @param Container $container + * + * @return callable + */ + $container['errorHandler'] = function ($container) { + return new Error( + $container->get('settings')['displayErrorDetails'] + ); + }; + } + + if (!isset($container['notFoundHandler'])) { + /** + * This service MUST return a callable that accepts two arguments: + * + * 1. Instance of ServerRequestInterface + * 2. Instance of ResponseInterface + * + * The callable MUST return an instance of ResponseInterface. + * + * @return callable + */ + $container['notFoundHandler'] = function () { + return new NotFound; + }; + } + + if (!isset($container['notAllowedHandler'])) { + /** + * This service MUST return a callable that accepts three arguments: + * + * 1. Instance of ServerRequestInterface + * 2. Instance of ResponseInterface + * 3. Array of allowed HTTP methods + * + * The callable MUST return an instance of ResponseInterface. + * + * @return callable + */ + $container['notAllowedHandler'] = function () { + return new NotAllowed; + }; + } + + if (!isset($container['callableResolver'])) { + /** + * Instance of CallableResolverInterface + * + * @param Container $container + * + * @return CallableResolverInterface + */ + $container['callableResolver'] = function ($container) { + return new CallableResolver($container); + }; + } + } +} diff --git a/vendor/slim/slim/Slim/DeferredCallable.php b/vendor/slim/slim/Slim/DeferredCallable.php new file mode 100644 index 0000000..5f33584 --- /dev/null +++ b/vendor/slim/slim/Slim/DeferredCallable.php @@ -0,0 +1,59 @@ +callable = $callable; + $this->container = $container; + } + + /** + * @return callable|string + */ + public function getCallable() + { + return $this->callable; + } + + /** + * @return mixed + */ + public function __invoke() + { + $callable = $this->resolveCallable($this->callable); + if ($callable instanceof Closure) { + $callable = $callable->bindTo($this->container); + } + + $args = func_get_args(); + + return call_user_func_array($callable, $args); + } +} diff --git a/vendor/slim/slim/Slim/Exception/ContainerException.php b/vendor/slim/slim/Slim/Exception/ContainerException.php new file mode 100644 index 0000000..fb01849 --- /dev/null +++ b/vendor/slim/slim/Slim/Exception/ContainerException.php @@ -0,0 +1,15 @@ +request = $request; + parent::__construct(sprintf('Unsupported HTTP method "%s" provided', $method)); + } + + /** + * @return ServerRequestInterface + */ + public function getRequest() + { + return $this->request; + } +} diff --git a/vendor/slim/slim/Slim/Exception/MethodNotAllowedException.php b/vendor/slim/slim/Slim/Exception/MethodNotAllowedException.php new file mode 100644 index 0000000..20d3824 --- /dev/null +++ b/vendor/slim/slim/Slim/Exception/MethodNotAllowedException.php @@ -0,0 +1,38 @@ +allowedMethods = $allowedMethods; + } + + /** + * @return string[] + */ + public function getAllowedMethods() + { + return $this->allowedMethods; + } +} diff --git a/vendor/slim/slim/Slim/Exception/NotFoundException.php b/vendor/slim/slim/Slim/Exception/NotFoundException.php new file mode 100644 index 0000000..5bbbbe6 --- /dev/null +++ b/vendor/slim/slim/Slim/Exception/NotFoundException.php @@ -0,0 +1,12 @@ +request = $request; + $this->response = $response; + } + + /** + * @return ServerRequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/AbstractError.php b/vendor/slim/slim/Slim/Handlers/AbstractError.php new file mode 100644 index 0000000..256062f --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/AbstractError.php @@ -0,0 +1,96 @@ +displayErrorDetails = (bool) $displayErrorDetails; + } + + /** + * Write to the error log if displayErrorDetails is false + * + * @param Exception|Throwable $throwable + * + * @return void + */ + protected function writeToErrorLog($throwable) + { + if ($this->displayErrorDetails) { + return; + } + + $message = 'Slim Application Error:' . PHP_EOL; + $message .= $this->renderThrowableAsText($throwable); + while ($throwable = $throwable->getPrevious()) { + $message .= PHP_EOL . 'Previous error:' . PHP_EOL; + $message .= $this->renderThrowableAsText($throwable); + } + + $message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + + $this->logError($message); + } + + /** + * Render error as Text. + * + * @param Exception|Throwable $throwable + * + * @return string + */ + protected function renderThrowableAsText($throwable) + { + $text = sprintf('Type: %s' . PHP_EOL, get_class($throwable)); + + if ($code = $throwable->getCode()) { + $text .= sprintf('Code: %s' . PHP_EOL, $code); + } + + if ($message = $throwable->getMessage()) { + $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); + } + + if ($file = $throwable->getFile()) { + $text .= sprintf('File: %s' . PHP_EOL, $file); + } + + if ($line = $throwable->getLine()) { + $text .= sprintf('Line: %s' . PHP_EOL, $line); + } + + if ($trace = $throwable->getTraceAsString()) { + $text .= sprintf('Trace: %s', $trace); + } + + return $text; + } + + /** + * Wraps the error_log function so that this can be easily tested + * + * @param string $message + */ + protected function logError($message) + { + error_log($message); + } +} diff --git a/vendor/slim/slim/Slim/Handlers/AbstractHandler.php b/vendor/slim/slim/Slim/Handlers/AbstractHandler.php new file mode 100644 index 0000000..5ace9a0 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/AbstractHandler.php @@ -0,0 +1,56 @@ +getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + + if (count($selectedContentTypes)) { + return current($selectedContentTypes); + } + + // handle +json and +xml specially + if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { + $mediaType = 'application/' . $matches[1]; + if (in_array($mediaType, $this->knownContentTypes)) { + return $mediaType; + } + } + + return 'text/html'; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/Error.php b/vendor/slim/slim/Slim/Handlers/Error.php new file mode 100644 index 0000000..50ece96 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/Error.php @@ -0,0 +1,221 @@ +determineContentType($request); + switch ($contentType) { + case 'application/json': + $output = $this->renderJsonErrorMessage($exception); + break; + + case 'text/xml': + case 'application/xml': + $output = $this->renderXmlErrorMessage($exception); + break; + + case 'text/html': + $output = $this->renderHtmlErrorMessage($exception); + break; + + default: + throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); + } + + $this->writeToErrorLog($exception); + + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + + return $response + ->withStatus(500) + ->withHeader('Content-type', $contentType) + ->withBody($body); + } + + /** + * Render HTML error page + * + * @param Exception $exception + * + * @return string + */ + protected function renderHtmlErrorMessage(Exception $exception) + { + $title = 'Slim Application Error'; + + if ($this->displayErrorDetails) { + $html = '

The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderHtmlException($exception); + + while ($exception = $exception->getPrevious()) { + $html .= '

Previous exception

'; + $html .= $this->renderHtmlExceptionOrError($exception); + } + } else { + $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; + } + + $output = sprintf( + "" . + "%s

%s

%s", + $title, + $title, + $html + ); + + return $output; + } + + /** + * Render exception as HTML. + * + * Provided for backwards compatibility; use renderHtmlExceptionOrError(). + * + * @param Exception $exception + * + * @return string + */ + protected function renderHtmlException(Exception $exception) + { + return $this->renderHtmlExceptionOrError($exception); + } + + /** + * Render exception or error as HTML. + * + * @param Exception|\Error $exception + * + * @return string + * + * @throws RuntimeException + */ + protected function renderHtmlExceptionOrError($exception) + { + if (!$exception instanceof Exception && !$exception instanceof \Error) { + throw new RuntimeException("Unexpected type. Expected Exception or Error."); + } + + $html = sprintf('
Type: %s
', get_class($exception)); + + if (($code = $exception->getCode())) { + $html .= sprintf('
Code: %s
', $code); + } + + if (($message = $exception->getMessage())) { + $html .= sprintf('
Message: %s
', htmlentities($message)); + } + + if (($file = $exception->getFile())) { + $html .= sprintf('
File: %s
', $file); + } + + if (($line = $exception->getLine())) { + $html .= sprintf('
Line: %s
', $line); + } + + if (($trace = $exception->getTraceAsString())) { + $html .= '

Trace

'; + $html .= sprintf('
%s
', htmlentities($trace)); + } + + return $html; + } + + /** + * Render JSON error + * + * @param Exception $exception + * + * @return string + */ + protected function renderJsonErrorMessage(Exception $exception) + { + $error = [ + 'message' => 'Slim Application Error', + ]; + + if ($this->displayErrorDetails) { + $error['exception'] = []; + + do { + $error['exception'][] = [ + 'type' => get_class($exception), + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => explode("\n", $exception->getTraceAsString()), + ]; + } while ($exception = $exception->getPrevious()); + } + + return json_encode($error, JSON_PRETTY_PRINT); + } + + /** + * Render XML error + * + * @param Exception $exception + * + * @return string + */ + protected function renderXmlErrorMessage(Exception $exception) + { + $xml = "\n Slim Application Error\n"; + if ($this->displayErrorDetails) { + do { + $xml .= " \n"; + $xml .= " " . get_class($exception) . "\n"; + $xml .= " " . $exception->getCode() . "\n"; + $xml .= " " . $this->createCdataSection($exception->getMessage()) . "\n"; + $xml .= " " . $exception->getFile() . "\n"; + $xml .= " " . $exception->getLine() . "\n"; + $xml .= " " . $this->createCdataSection($exception->getTraceAsString()) . "\n"; + $xml .= " \n"; + } while ($exception = $exception->getPrevious()); + } + $xml .= ""; + + return $xml; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * + * @return string + */ + private function createCdataSection($content) + { + return sprintf('', str_replace(']]>', ']]]]>', $content)); + } +} diff --git a/vendor/slim/slim/Slim/Handlers/NotAllowed.php b/vendor/slim/slim/Slim/Handlers/NotAllowed.php new file mode 100644 index 0000000..5eba80b --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/NotAllowed.php @@ -0,0 +1,143 @@ +getMethod() === 'OPTIONS') { + $status = 200; + $contentType = 'text/plain'; + $output = $this->renderPlainOptionsMessage($methods); + } else { + $status = 405; + $contentType = $this->determineContentType($request); + switch ($contentType) { + case 'application/json': + $output = $this->renderJsonNotAllowedMessage($methods); + break; + + case 'text/xml': + case 'application/xml': + $output = $this->renderXmlNotAllowedMessage($methods); + break; + + case 'text/html': + $output = $this->renderHtmlNotAllowedMessage($methods); + break; + default: + throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); + } + } + + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + $allow = implode(', ', $methods); + + return $response + ->withStatus($status) + ->withHeader('Content-type', $contentType) + ->withHeader('Allow', $allow) + ->withBody($body); + } + + /** + * Render plain message for OPTIONS response + * + * @param string[] $methods + * + * @return string + */ + protected function renderPlainOptionsMessage($methods) + { + $allow = implode(', ', $methods); + + return 'Allowed methods: ' . $allow; + } + + /** + * Render JSON not allowed message + * + * @param string[] $methods + * + * @return string + */ + protected function renderJsonNotAllowedMessage($methods) + { + $allow = implode(', ', $methods); + + return '{"message":"Method not allowed. Must be one of: ' . $allow . '"}'; + } + + /** + * Render XML not allowed message + * + * @param string[] $methods + * + * @return string + */ + protected function renderXmlNotAllowedMessage($methods) + { + $allow = implode(', ', $methods); + + return "Method not allowed. Must be one of: $allow"; + } + + /** + * Render HTML not allowed message + * + * @param string[] $methods + * + * @return string + */ + protected function renderHtmlNotAllowedMessage($methods) + { + $allow = implode(', ', $methods); + $output = << + + Method not allowed + + + +

Method not allowed

+

Method not allowed. Must be one of: $allow

+ + +END; + + return $output; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/NotFound.php b/vendor/slim/slim/Slim/Handlers/NotFound.php new file mode 100644 index 0000000..f0d013c --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/NotFound.php @@ -0,0 +1,133 @@ +getMethod() === 'OPTIONS') { + $contentType = 'text/plain'; + $output = $this->renderPlainNotFoundOutput(); + } else { + $contentType = $this->determineContentType($request); + switch ($contentType) { + case 'application/json': + $output = $this->renderJsonNotFoundOutput(); + break; + + case 'text/xml': + case 'application/xml': + $output = $this->renderXmlNotFoundOutput(); + break; + + case 'text/html': + $output = $this->renderHtmlNotFoundOutput($request); + break; + + default: + throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); + } + } + + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + + return $response->withStatus(404) + ->withHeader('Content-Type', $contentType) + ->withBody($body); + } + + /** + * Render plain not found message + * + * @return string + */ + protected function renderPlainNotFoundOutput() + { + return 'Not found'; + } + + /** + * Return a response for application/json content not found + * + * @return string + */ + protected function renderJsonNotFoundOutput() + { + return '{"message":"Not found"}'; + } + + /** + * Return a response for xml content not found + * + * @return string + */ + protected function renderXmlNotFoundOutput() + { + return 'Not found'; + } + + /** + * Return a response for text/html content not found + * + * @param ServerRequestInterface $request The most recent Request object + * + * @return string + */ + protected function renderHtmlNotFoundOutput(ServerRequestInterface $request) + { + $homeUrl = (string)($request->getUri()->withPath('')->withQuery('')->withFragment('')); + return << + + Page Not Found + + + +

Page Not Found

+

+ The page you are looking for could not be found. Check the address bar + to ensure your URL is spelled correctly. If all else fails, you can + visit our home page at the link below. +

+ Visit the Home Page + + +END; + } +} diff --git a/vendor/slim/slim/Slim/Handlers/PhpError.php b/vendor/slim/slim/Slim/Handlers/PhpError.php new file mode 100644 index 0000000..28c7416 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/PhpError.php @@ -0,0 +1,199 @@ +determineContentType($request); + switch ($contentType) { + case 'application/json': + $output = $this->renderJsonErrorMessage($error); + break; + + case 'text/xml': + case 'application/xml': + $output = $this->renderXmlErrorMessage($error); + break; + + case 'text/html': + $output = $this->renderHtmlErrorMessage($error); + break; + default: + throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); + } + + $this->writeToErrorLog($error); + + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + + return $response + ->withStatus(500) + ->withHeader('Content-type', $contentType) + ->withBody($body); + } + + /** + * Render HTML error page + * + * @param Throwable $error + * + * @return string + */ + protected function renderHtmlErrorMessage(Throwable $error) + { + $title = 'Slim Application Error'; + + if ($this->displayErrorDetails) { + $html = '

The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderHtmlError($error); + + while ($error = $error->getPrevious()) { + $html .= '

Previous error

'; + $html .= $this->renderHtmlError($error); + } + } else { + $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; + } + + $output = sprintf( + "" . + "%s

%s

%s", + $title, + $title, + $html + ); + + return $output; + } + + /** + * Render error as HTML. + * + * @param Throwable $error + * + * @return string + */ + protected function renderHtmlError(Throwable $error) + { + $html = sprintf('
Type: %s
', get_class($error)); + + if (($code = $error->getCode())) { + $html .= sprintf('
Code: %s
', $code); + } + + if (($message = $error->getMessage())) { + $html .= sprintf('
Message: %s
', htmlentities($message)); + } + + if (($file = $error->getFile())) { + $html .= sprintf('
File: %s
', $file); + } + + if (($line = $error->getLine())) { + $html .= sprintf('
Line: %s
', $line); + } + + if (($trace = $error->getTraceAsString())) { + $html .= '

Trace

'; + $html .= sprintf('
%s
', htmlentities($trace)); + } + + return $html; + } + + /** + * Render JSON error + * + * @param Throwable $error + * + * @return string + */ + protected function renderJsonErrorMessage(Throwable $error) + { + $json = [ + 'message' => 'Slim Application Error', + ]; + + if ($this->displayErrorDetails) { + $json['error'] = []; + + do { + $json['error'][] = [ + 'type' => get_class($error), + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + 'file' => $error->getFile(), + 'line' => $error->getLine(), + 'trace' => explode("\n", $error->getTraceAsString()), + ]; + } while ($error = $error->getPrevious()); + } + + return json_encode($json, JSON_PRETTY_PRINT); + } + + /** + * Render XML error + * + * @param Throwable $error + * + * @return string + */ + protected function renderXmlErrorMessage(Throwable $error) + { + $xml = "\n Slim Application Error\n"; + if ($this->displayErrorDetails) { + do { + $xml .= " \n"; + $xml .= " " . get_class($error) . "\n"; + $xml .= " " . $error->getCode() . "\n"; + $xml .= " " . $this->createCdataSection($error->getMessage()) . "\n"; + $xml .= " " . $error->getFile() . "\n"; + $xml .= " " . $error->getLine() . "\n"; + $xml .= " " . $this->createCdataSection($error->getTraceAsString()) . "\n"; + $xml .= " \n"; + } while ($error = $error->getPrevious()); + } + $xml .= ""; + + return $xml; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * + * @return string + */ + private function createCdataSection($content) + { + return sprintf('', str_replace(']]>', ']]]]>', $content)); + } +} diff --git a/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php b/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php new file mode 100644 index 0000000..d91b05a --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php @@ -0,0 +1,42 @@ + $v) { + $request = $request->withAttribute($k, $v); + } + + return call_user_func($callable, $request, $response, $routeArguments); + } +} diff --git a/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponseArgs.php b/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponseArgs.php new file mode 100644 index 0000000..d072914 --- /dev/null +++ b/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponseArgs.php @@ -0,0 +1,41 @@ + '', + 'domain' => null, + 'hostonly' => null, + 'path' => null, + 'expires' => null, + 'secure' => false, + 'httponly' => false, + 'samesite' => null + ]; + + /** + * @param array $cookies + */ + public function __construct(array $cookies = []) + { + $this->requestCookies = $cookies; + } + + /** + * Set default cookie properties + * + * @param array $settings + */ + public function setDefaults(array $settings) + { + $this->defaults = array_replace($this->defaults, $settings); + } + + /** + * {@inheritdoc} + */ + public function get($name, $default = null) + { + return isset($this->requestCookies[$name]) ? $this->requestCookies[$name] : $default; + } + + /** + * {@inheritdoc} + */ + public function set($name, $value) + { + if (!is_array($value)) { + $value = ['value' => (string)$value]; + } + $this->responseCookies[$name] = array_replace($this->defaults, $value); + } + + /** + * {@inheritdoc} + */ + public function toHeaders() + { + $headers = []; + foreach ($this->responseCookies as $name => $properties) { + $headers[] = $this->toHeader($name, $properties); + } + + return $headers; + } + + /** + * Convert to `Set-Cookie` header + * + * @param string $name Cookie name + * @param array $properties Cookie properties + * + * @return string + */ + protected function toHeader($name, array $properties) + { + $result = urlencode($name) . '=' . urlencode($properties['value']); + + if (isset($properties['domain'])) { + $result .= '; domain=' . $properties['domain']; + } + + if (isset($properties['path'])) { + $result .= '; path=' . $properties['path']; + } + + if (isset($properties['expires'])) { + if (is_string($properties['expires'])) { + $timestamp = strtotime($properties['expires']); + } else { + $timestamp = (int)$properties['expires']; + } + if ($timestamp !== 0) { + $result .= '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp); + } + } + + if (isset($properties['secure']) && $properties['secure']) { + $result .= '; secure'; + } + + if (isset($properties['hostonly']) && $properties['hostonly']) { + $result .= '; HostOnly'; + } + + if (isset($properties['httponly']) && $properties['httponly']) { + $result .= '; HttpOnly'; + } + + if (isset($properties['samesite']) && in_array(strtolower($properties['samesite']), ['lax', 'strict'], true)) { + // While strtolower is needed for correct comparison, the RFC doesn't care about case + $result .= '; SameSite=' . $properties['samesite']; + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public static function parseHeader($header) + { + if (is_array($header) === true) { + $header = isset($header[0]) ? $header[0] : ''; + } + + if (is_string($header) === false) { + throw new InvalidArgumentException('Cannot parse Cookie data. Header value must be a string.'); + } + + $header = rtrim($header, "\r\n"); + $pieces = preg_split('@[;]\s*@', $header); + $cookies = []; + + foreach ($pieces as $cookie) { + $cookie = explode('=', $cookie, 2); + + if (count($cookie) === 2) { + $key = urldecode($cookie[0]); + $value = urldecode($cookie[1]); + + if (!isset($cookies[$key])) { + $cookies[$key] = $value; + } + } + } + + return $cookies; + } +} diff --git a/vendor/slim/slim/Slim/Http/Environment.php b/vendor/slim/slim/Slim/Http/Environment.php new file mode 100644 index 0000000..11089d9 --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Environment.php @@ -0,0 +1,56 @@ + 'HTTP/1.1', + 'REQUEST_METHOD' => 'GET', + 'REQUEST_SCHEME' => $defscheme, + 'SCRIPT_NAME' => '', + 'REQUEST_URI' => '', + 'QUERY_STRING' => '', + 'SERVER_NAME' => 'localhost', + 'SERVER_PORT' => $defport, + 'HTTP_HOST' => 'localhost', + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8', + 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', + 'HTTP_USER_AGENT' => 'Slim Framework', + 'REMOTE_ADDR' => '127.0.0.1', + 'REQUEST_TIME' => time(), + 'REQUEST_TIME_FLOAT' => microtime(true), + ], $settings); + + return new static($data); + } +} diff --git a/vendor/slim/slim/Slim/Http/Headers.php b/vendor/slim/slim/Slim/Http/Headers.php new file mode 100644 index 0000000..f2eec7c --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Headers.php @@ -0,0 +1,206 @@ + 1, + 'CONTENT_LENGTH' => 1, + 'PHP_AUTH_USER' => 1, + 'PHP_AUTH_PW' => 1, + 'PHP_AUTH_DIGEST' => 1, + 'AUTH_TYPE' => 1, + ]; + + /** + * Create new headers collection with data extracted from the application Environment object + * + * @param Environment $environment The Slim application Environment + * + * @return self + */ + public static function createFromEnvironment(Environment $environment) + { + $data = []; + $environment = self::determineAuthorization($environment); + foreach ($environment as $key => $value) { + $key = strtoupper($key); + if (isset(static::$special[$key]) || strpos($key, 'HTTP_') === 0) { + if ($key !== 'HTTP_CONTENT_LENGTH') { + $data[$key] = $value; + } + } + } + + return new static($data); + } + + /** + * If HTTP_AUTHORIZATION does not exist tries to get it from getallheaders() when available. + * + * @param Environment $environment The Slim application Environment + * + * @return Environment + */ + + public static function determineAuthorization(Environment $environment) + { + $authorization = $environment->get('HTTP_AUTHORIZATION'); + if (!empty($authorization) || !is_callable('getallheaders')) { + return $environment; + } + + $headers = getallheaders(); + if (!is_array($headers)) { + return $environment; + } + + $headers = array_change_key_case($headers, CASE_LOWER); + if (isset($headers['authorization'])) { + $environment->set('HTTP_AUTHORIZATION', $headers['authorization']); + } + + return $environment; + } + + /** + * Return array of HTTP header names and values. + * This method returns the _original_ header name as specified by the end user. + * + * @return array + */ + public function all() + { + $all = parent::all(); + $out = []; + foreach ($all as $key => $props) { + $out[$props['originalKey']] = $props['value']; + } + + return $out; + } + + /** + * Set HTTP header value + * + * This method sets a header value. It replaces + * any values that may already exist for the header name. + * + * @param string $key The case-insensitive header name + * @param array|string $value The header value + */ + public function set($key, $value) + { + if (!is_array($value)) { + $value = [$value]; + } + parent::set($this->normalizeKey($key), [ + 'value' => $value, + 'originalKey' => $key + ]); + } + + /** + * Get HTTP header value + * + * @param string $key The case-insensitive header name + * @param mixed $default The default value if key does not exist + * + * @return string[] + */ + public function get($key, $default = null) + { + if ($this->has($key)) { + return parent::get($this->normalizeKey($key))['value']; + } + + return $default; + } + + /** + * Get HTTP header key as originally specified + * + * @param string $key The case-insensitive header name + * @param mixed $default The default value if key does not exist + * + * @return string + */ + public function getOriginalKey($key, $default = null) + { + if ($this->has($key)) { + return parent::get($this->normalizeKey($key))['originalKey']; + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function add($key, $value) + { + $oldValues = $this->get($key, []); + $newValues = is_array($value) ? $value : [$value]; + $this->set($key, array_merge($oldValues, array_values($newValues))); + } + + /** + * Does this collection have a given header? + * + * @param string $key The case-insensitive header name + * + * @return bool + */ + public function has($key) + { + return parent::has($this->normalizeKey($key)); + } + + /** + * Remove header from collection + * + * @param string $key The case-insensitive header name + */ + public function remove($key) + { + parent::remove($this->normalizeKey($key)); + } + + /** + * {@inheritdoc} + */ + public function normalizeKey($key) + { + $key = strtr(strtolower($key), '_', '-'); + if (strpos($key, 'http-') === 0) { + $key = substr($key, 5); + } + + return $key; + } +} diff --git a/vendor/slim/slim/Slim/Http/Message.php b/vendor/slim/slim/Slim/Http/Message.php new file mode 100644 index 0000000..0dc7b4d --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Message.php @@ -0,0 +1,295 @@ + true, + '1.1' => true, + '2.0' => true, + '2' => true, + ]; + + /** + * @var HeadersInterface + */ + protected $headers; + + /** + * @var StreamInterface + */ + protected $body; + + /** + * Disable magic setter to ensure immutability + */ + public function __set($name, $value) + { + // Do nothing + } + + /** + * Retrieves the HTTP protocol version as a string. + * + * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). + * + * @return string + */ + public function getProtocolVersion() + { + return $this->protocolVersion; + } + + /** + * Return an instance with the specified HTTP protocol version. + * + * The version string MUST contain only the HTTP version number (e.g., + * "1.1", "1.0"). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new protocol version. + * + * @param string $version HTTP protocol version + * + * @return static + * + * @throws InvalidArgumentException if the http version is an invalid number + */ + public function withProtocolVersion($version) + { + if (!isset(self::$validProtocolVersions[$version])) { + throw new InvalidArgumentException( + 'Invalid HTTP version. Must be one of: ' + . implode(', ', array_keys(self::$validProtocolVersions)) + ); + } + $clone = clone $this; + $clone->protocolVersion = $version; + + return $clone; + } + + /** + * Retrieves all message header values. + * + * Returns an associative array of the message's headers. + * Each key MUST be a header name, and each value MUST be an array of strings for that header. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return array + */ + public function getHeaders() + { + return $this->headers->all(); + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * Returns true if any header names match the given header name using a case-insensitive string comparison. + * Returns false if no matching header name is found in the message. + * + * @param string $name Case-insensitive header field name. + * + * @return bool + */ + public function hasHeader($name) + { + return $this->headers->has($name); + } + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * + * @return string[] + */ + public function getHeader($name) + { + return $this->headers->get($name, []); + } + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns a string of all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * + * @return string + */ + public function getHeaderLine($name) + { + return implode(',', $this->headers->get($name, [])); + } + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * + * @return static + */ + public function withHeader($name, $value) + { + $clone = clone $this; + $clone->headers->set($name, $value); + + return $clone; + } + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * + * @return static + */ + public function withAddedHeader($name, $value) + { + $clone = clone $this; + $clone->headers->add($name, $value); + + if ($this instanceof Response && $this->body instanceof NonBufferedBody) { + header(sprintf('%s: %s', $name, $clone->getHeaderLine($name))); + } + + return $clone; + } + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * + * @return static + */ + public function withoutHeader($name) + { + $clone = clone $this; + $clone->headers->remove($name); + + if ($this instanceof Response && $this->body instanceof NonBufferedBody) { + header_remove($name); + } + + return $clone; + } + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody() + { + return $this->body; + } + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * + * @return static + */ + public function withBody(StreamInterface $body) + { + $clone = clone $this; + $clone->body = $body; + + return $clone; + } +} diff --git a/vendor/slim/slim/Slim/Http/NonBufferedBody.php b/vendor/slim/slim/Slim/Http/NonBufferedBody.php new file mode 100644 index 0000000..352ff59 --- /dev/null +++ b/vendor/slim/slim/Slim/Http/NonBufferedBody.php @@ -0,0 +1,143 @@ + 1, + 'DELETE' => 1, + 'GET' => 1, + 'HEAD' => 1, + 'OPTIONS' => 1, + 'PATCH' => 1, + 'POST' => 1, + 'PUT' => 1, + 'TRACE' => 1, + ]; + + /** + * Create new HTTP request with data extracted from the application + * Environment object + * + * @param Environment $environment The Slim application Environment + * + * @return static + */ + public static function createFromEnvironment(Environment $environment) + { + $method = $environment['REQUEST_METHOD']; + $uri = Uri::createFromEnvironment($environment); + $headers = Headers::createFromEnvironment($environment); + $cookies = Cookies::parseHeader($headers->get('Cookie', [])); + $serverParams = $environment->all(); + $body = new RequestBody(); + $uploadedFiles = UploadedFile::createFromEnvironment($environment); + + $request = new static($method, $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles); + + if ($method === 'POST' && + in_array($request->getMediaType(), ['application/x-www-form-urlencoded', 'multipart/form-data']) + ) { + // parsed body must be $_POST + $request = $request->withParsedBody($_POST); + } + return $request; + } + + /** + * @param string $method The request method + * @param UriInterface $uri The request URI object + * @param HeadersInterface $headers The request headers collection + * @param array $cookies The request cookies collection + * @param array $serverParams The server environment variables + * @param StreamInterface $body The request body object + * @param array $uploadedFiles The request uploadedFiles collection + * + * @throws InvalidMethodException on invalid HTTP method + */ + public function __construct( + $method, + UriInterface $uri, + HeadersInterface $headers, + array $cookies, + array $serverParams, + StreamInterface $body, + array $uploadedFiles = [] + ) { + try { + $this->originalMethod = $this->filterMethod($method); + } catch (InvalidMethodException $e) { + $this->originalMethod = $method; + } + + $this->uri = $uri; + $this->headers = $headers; + $this->cookies = $cookies; + $this->serverParams = $serverParams; + $this->attributes = new Collection(); + $this->body = $body; + $this->uploadedFiles = $uploadedFiles; + + if (isset($serverParams['SERVER_PROTOCOL'])) { + $this->protocolVersion = str_replace('HTTP/', '', $serverParams['SERVER_PROTOCOL']); + } + + if (!$this->headers->has('Host') && $this->uri->getHost() !== '') { + $port = $this->uri->getPort() ? ":{$this->uri->getPort()}" : ''; + + $this->headers->set('Host', $this->uri->getHost() . $port); + } + + $this->registerMediaTypeParser('application/json', function ($input) { + $result = json_decode($input, true); + if (!is_array($result)) { + return null; + } + return $result; + }); + + $this->registerMediaTypeParser('application/xml', function ($input) { + $backup = libxml_disable_entity_loader(true); + $backup_errors = libxml_use_internal_errors(true); + $result = simplexml_load_string($input); + libxml_disable_entity_loader($backup); + libxml_clear_errors(); + libxml_use_internal_errors($backup_errors); + if ($result === false) { + return null; + } + return $result; + }); + + $this->registerMediaTypeParser('text/xml', function ($input) { + $backup = libxml_disable_entity_loader(true); + $backup_errors = libxml_use_internal_errors(true); + $result = simplexml_load_string($input); + libxml_disable_entity_loader($backup); + libxml_clear_errors(); + libxml_use_internal_errors($backup_errors); + if ($result === false) { + return null; + } + return $result; + }); + + $this->registerMediaTypeParser('application/x-www-form-urlencoded', function ($input) { + parse_str($input, $data); + return $data; + }); + + // if the request had an invalid method, we can throw it now + if (isset($e) && $e instanceof InvalidMethodException) { + throw $e; + } + } + + /** + * This method is applied to the cloned object after PHP performs an initial shallow-copy. + * This method completes a deep-copy by creating new objects for the cloned object's internal reference pointers. + */ + public function __clone() + { + $this->headers = clone $this->headers; + $this->attributes = clone $this->attributes; + $this->body = clone $this->body; + } + + /** + * Retrieves the HTTP method of the request. + * + * @return string + */ + public function getMethod() + { + if ($this->method === null) { + $this->method = $this->originalMethod; + $customMethod = $this->getHeaderLine('X-Http-Method-Override'); + + if ($customMethod) { + $this->method = $this->filterMethod($customMethod); + } elseif ($this->originalMethod === 'POST') { + $overrideMethod = $this->filterMethod($this->getParsedBodyParam('_METHOD')); + if ($overrideMethod !== null) { + $this->method = $overrideMethod; + } + + if ($this->getBody()->eof()) { + $this->getBody()->rewind(); + } + } + } + + return $this->method; + } + + /** + * Get the original HTTP method (ignore override). + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string + */ + public function getOriginalMethod() + { + return $this->originalMethod; + } + + /** + * Return an instance with the provided HTTP method. + * + * While HTTP method names are typically all uppercase characters, HTTP + * method names are case-sensitive and thus implementations SHOULD NOT + * modify the given string. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request method. + * + * @param string $method Case-sensitive method. + * + * @return static + * + * @throws InvalidArgumentException for invalid HTTP methods. + */ + public function withMethod($method) + { + $method = $this->filterMethod($method); + $clone = clone $this; + $clone->originalMethod = $method; + $clone->method = $method; + + return $clone; + } + + /** + * Validate the HTTP method + * + * @param null|string $method + * + * @return null|string + * + * @throws InvalidArgumentException on invalid HTTP method. + */ + protected function filterMethod($method) + { + if ($method === null) { + return $method; + } + + if (!is_string($method)) { + throw new InvalidArgumentException(sprintf( + 'Unsupported HTTP method; must be a string, received %s', + (is_object($method) ? get_class($method) : gettype($method)) + )); + } + + $method = strtoupper($method); + if (preg_match("/^[!#$%&'*+.^_`|~0-9a-z-]+$/i", $method) !== 1) { + throw new InvalidMethodException($this, $method); + } + + return $method; + } + + /** + * Does this request use a given method? + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $method HTTP method + * + * @return bool + */ + public function isMethod($method) + { + return $this->getMethod() === $method; + } + + /** + * Is this a GET request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isGet() + { + return $this->isMethod('GET'); + } + + /** + * Is this a POST request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPost() + { + return $this->isMethod('POST'); + } + + /** + * Is this a PUT request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPut() + { + return $this->isMethod('PUT'); + } + + /** + * Is this a PATCH request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPatch() + { + return $this->isMethod('PATCH'); + } + + /** + * Is this a DELETE request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isDelete() + { + return $this->isMethod('DELETE'); + } + + /** + * Is this a HEAD request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isHead() + { + return $this->isMethod('HEAD'); + } + + /** + * Is this a OPTIONS request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isOptions() + { + return $this->isMethod('OPTIONS'); + } + + /** + * Is this an XHR request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isXhr() + { + return $this->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; + } + + /** + * Retrieves the message's request target. + * + * Retrieves the message's request-target either as it will appear (for + * clients), as it appeared at request (for servers), or as it was + * specified for the instance (see withRequestTarget()). + * + * In most cases, this will be the origin-form of the composed URI, + * unless a value was provided to the concrete implementation (see + * withRequestTarget() below). + * + * If no URI is available, and no request-target has been specifically + * provided, this method MUST return the string "/". + * + * @return string + */ + public function getRequestTarget() + { + if ($this->requestTarget) { + return $this->requestTarget; + } + + if ($this->uri === null) { + return '/'; + } + + if ($this->uri instanceof Uri) { + $basePath = $this->uri->getBasePath(); + } else { + $basePath = ''; + } + $path = $this->uri->getPath(); + $path = $basePath . '/' . ltrim($path, '/'); + + $query = $this->uri->getQuery(); + if ($query) { + $path .= '?' . $query; + } + $this->requestTarget = $path; + + return $this->requestTarget; + } + + /** + * Return an instance with the specific request-target. + * + * If the request needs a non-origin-form request-target — e.g., for + * specifying an absolute-form, authority-form, or asterisk-form — + * this method may be used to create an instance with the specified + * request-target, verbatim. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request target. + * + * @link http://tools.ietf.org/html/rfc7230#section-2.7 + * (for the various request-target forms allowed in request messages) + * + * @param string $requestTarget + * + * @return static + * + * @throws InvalidArgumentException if the request target is invalid + */ + public function withRequestTarget($requestTarget) + { + if (preg_match('#\s#', $requestTarget)) { + throw new InvalidArgumentException( + 'Invalid request target provided; must be a string and cannot contain whitespace' + ); + } + $clone = clone $this; + $clone->requestTarget = $requestTarget; + + return $clone; + } + + /** + * Retrieves the URI instance. + * + * This method MUST return a UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * + * @return UriInterface + */ + public function getUri() + { + return $this->uri; + } + + /** + * Returns an instance with the provided URI. + * + * This method MUST update the Host header of the returned request by + * default if the URI contains a host component. If the URI does not + * contain a host component, any pre-existing Host header MUST be carried + * over to the returned request. + * + * You can opt-in to preserving the original state of the Host header by + * setting `$preserveHost` to `true`. When `$preserveHost` is set to + * `true`, this method interacts with the Host header in the following ways: + * + * - If the the Host header is missing or empty, and the new URI contains + * a host component, this method MUST update the Host header in the returned + * request. + * - If the Host header is missing or empty, and the new URI does not contain a + * host component, this method MUST NOT update the Host header in the returned + * request. + * - If a Host header is present and non-empty, this method MUST NOT update + * the Host header in the returned request. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * + * @param UriInterface $uri New request URI to use. + * @param bool $preserveHost Preserve the original state of the Host header. + * + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + $clone = clone $this; + $clone->uri = $uri; + + if (!$preserveHost) { + if ($uri->getHost() !== '') { + $clone->headers->set('Host', $uri->getHost()); + } + } else { + if ($uri->getHost() !== '' && (!$this->hasHeader('Host') || $this->getHeaderLine('Host') === '')) { + $clone->headers->set('Host', $uri->getHost()); + } + } + + return $clone; + } + + /** + * Get request content type. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null + */ + public function getContentType() + { + $result = $this->getHeader('Content-Type'); + + return $result ? $result[0] : null; + } + + /** + * Get request media type, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null + */ + public function getMediaType() + { + $contentType = $this->getContentType(); + if ($contentType) { + $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); + + return strtolower($contentTypeParts[0]); + } + + return null; + } + + /** + * Get request media type params, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string[] + */ + public function getMediaTypeParams() + { + $contentType = $this->getContentType(); + $contentTypeParams = []; + if ($contentType) { + $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); + $contentTypePartsLength = count($contentTypeParts); + for ($i = 1; $i < $contentTypePartsLength; $i++) { + $paramParts = explode('=', $contentTypeParts[$i]); + $contentTypeParams[strtolower($paramParts[0])] = $paramParts[1]; + } + } + + return $contentTypeParams; + } + + /** + * Get request content character set, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null + */ + public function getContentCharset() + { + $mediaTypeParams = $this->getMediaTypeParams(); + if (isset($mediaTypeParams['charset'])) { + return $mediaTypeParams['charset']; + } + + return null; + } + + /** + * Get request content length, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return int|null + */ + public function getContentLength() + { + $result = $this->headers->get('Content-Length'); + + return $result ? (int)$result[0] : null; + } + + /** + * Retrieve cookies. + * + * Retrieves cookies sent by the client to the server. + * + * The data MUST be compatible with the structure of the $_COOKIE superglobal. + * + * @return array + */ + public function getCookieParams() + { + return $this->cookies; + } + + /** + * Fetch cookie value from cookies sent by the client to the server. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * + * @return mixed + */ + public function getCookieParam($key, $default = null) + { + $cookies = $this->getCookieParams(); + $result = $default; + if (isset($cookies[$key])) { + $result = $cookies[$key]; + } + + return $result; + } + + /** + * Return an instance with the specified cookies. + * + * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST + * be compatible with the structure of $_COOKIE. Typically, this data will + * be injected at instantiation. + * + * This method MUST NOT update the related Cookie header of the request + * instance, nor related values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated cookie values. + * + * @param array $cookies Array of key/value pairs representing cookies. + * + * @return static + */ + public function withCookieParams(array $cookies) + { + $clone = clone $this; + $clone->cookies = $cookies; + + return $clone; + } + + /** + * Retrieve query string arguments. + * + * Retrieves the deserialized query string arguments, if any. + * + * Note: the query params might not be in sync with the URI or server + * params. If you need to ensure you are only getting the original + * values, you may need to parse the query string from `getUri()->getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams() + { + if (is_array($this->queryParams)) { + return $this->queryParams; + } + + if ($this->uri === null) { + return []; + } + + parse_str($this->uri->getQuery(), $this->queryParams); // <-- URL decodes data + + return $this->queryParams; + } + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * + * @return static + */ + public function withQueryParams(array $query) + { + $clone = clone $this; + $clone->queryParams = $query; + + return $clone; + } + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array + */ + public function getUploadedFiles() + { + return $this->uploadedFiles; + } + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * + * @return static + * + * @throws InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles) + { + $clone = clone $this; + $clone->uploadedFiles = $uploadedFiles; + + return $clone; + } + + /** + * Retrieve server parameters. + * + * Retrieves data related to the incoming request environment, + * typically derived from PHP's $_SERVER superglobal. The data IS NOT + * REQUIRED to originate from $_SERVER. + * + * @return array + */ + public function getServerParams() + { + return $this->serverParams; + } + + /** + * Retrieve a server parameter. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function getServerParam($key, $default = null) + { + $serverParams = $this->getServerParams(); + + return isset($serverParams[$key]) ? $serverParams[$key] : $default; + } + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array + */ + public function getAttributes() + { + return $this->attributes->all(); + } + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * + * @return mixed + */ + public function getAttribute($name, $default = null) + { + return $this->attributes->get($name, $default); + } + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * + * @return static + */ + public function withAttribute($name, $value) + { + $clone = clone $this; + $clone->attributes->set($name, $value); + + return $clone; + } + + /** + * Create a new instance with the specified derived request attributes. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method allows setting all new derived request attributes as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * updated attributes. + * + * @param array $attributes New attributes + * + * @return static + */ + public function withAttributes(array $attributes) + { + $clone = clone $this; + $clone->attributes = new Collection($attributes); + + return $clone; + } + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * + * @param string $name The attribute name. + * + * @return static + */ + public function withoutAttribute($name) + { + $clone = clone $this; + $clone->attributes->remove($name); + + return $clone; + } + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object + * + * @throws RuntimeException if the request body media type parser returns an invalid value + */ + public function getParsedBody() + { + if ($this->bodyParsed !== false) { + return $this->bodyParsed; + } + + if (!$this->body) { + return null; + } + + $mediaType = $this->getMediaType(); + + // Check if this specific media type has a parser registered first + if (!isset($this->bodyParsers[$mediaType])) { + // If not, look for a media type with a structured syntax suffix (RFC 6839) + $parts = explode('+', $mediaType); + if (count($parts) >= 2) { + $mediaType = 'application/' . $parts[count($parts)-1]; + } + } + + if (isset($this->bodyParsers[$mediaType])) { + $body = (string)$this->getBody(); + $parsed = $this->bodyParsers[$mediaType]($body); + + if (!is_null($parsed) && !is_object($parsed) && !is_array($parsed)) { + throw new RuntimeException( + 'Request body media type parser return value must be an array, an object, or null' + ); + } + $this->bodyParsed = $parsed; + return $this->bodyParsed; + } + + return null; + } + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will typically be in an array or object. + * + * @return static + * + * @throws InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data) + { + if (!is_null($data) && !is_object($data) && !is_array($data)) { + throw new InvalidArgumentException('Parsed body value must be an array, an object, or null'); + } + + $clone = clone $this; + $clone->bodyParsed = $data; + + return $clone; + } + + /** + * Force Body to be parsed again. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return $this + */ + public function reparseBody() + { + $this->bodyParsed = false; + + return $this; + } + + /** + * Register media type parser. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $mediaType A HTTP media type (excluding content-type params). + * @param callable $callable A callable that returns parsed contents for media type. + */ + public function registerMediaTypeParser($mediaType, callable $callable) + { + if ($callable instanceof Closure) { + $callable = $callable->bindTo($this); + } + $this->bodyParsers[(string)$mediaType] = $callable; + } + + /** + * Fetch request parameter value from body or query string (in that order). + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key The parameter key. + * @param mixed $default The default value. + * + * @return mixed + */ + public function getParam($key, $default = null) + { + $postParams = $this->getParsedBody(); + $getParams = $this->getQueryParams(); + $result = $default; + if (is_array($postParams) && isset($postParams[$key])) { + $result = $postParams[$key]; + } elseif (is_object($postParams) && property_exists($postParams, $key)) { + $result = $postParams->$key; + } elseif (isset($getParams[$key])) { + $result = $getParams[$key]; + } + + return $result; + } + + /** + * Fetch parameter value from request body. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function getParsedBodyParam($key, $default = null) + { + $postParams = $this->getParsedBody(); + $result = $default; + if (is_array($postParams) && isset($postParams[$key])) { + $result = $postParams[$key]; + } elseif (is_object($postParams) && property_exists($postParams, $key)) { + $result = $postParams->$key; + } + + return $result; + } + + /** + * Fetch parameter value from query string. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function getQueryParam($key, $default = null) + { + $getParams = $this->getQueryParams(); + $result = $default; + if (isset($getParams[$key])) { + $result = $getParams[$key]; + } + + return $result; + } + + /** + * Fetch associative array of body and query string parameters. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param array|null $only list the keys to retrieve. + * + * @return array|null + */ + public function getParams(array $only = null) + { + $params = $this->getQueryParams(); + $postParams = $this->getParsedBody(); + if ($postParams) { + $params = array_replace($params, (array)$postParams); + } + + if ($only) { + $onlyParams = []; + foreach ($only as $key) { + if (array_key_exists($key, $params)) { + $onlyParams[$key] = $params[$key]; + } + } + return $onlyParams; + } + + return $params; + } +} diff --git a/vendor/slim/slim/Slim/Http/RequestBody.php b/vendor/slim/slim/Slim/Http/RequestBody.php new file mode 100644 index 0000000..48e1cc6 --- /dev/null +++ b/vendor/slim/slim/Slim/Http/RequestBody.php @@ -0,0 +1,23 @@ + 'Continue', + StatusCode::HTTP_SWITCHING_PROTOCOLS => 'Switching Protocols', + StatusCode::HTTP_PROCESSING => 'Processing', + //Successful 2xx + StatusCode::HTTP_OK => 'OK', + StatusCode::HTTP_CREATED => 'Created', + StatusCode::HTTP_ACCEPTED => 'Accepted', + StatusCode::HTTP_NONAUTHORITATIVE_INFORMATION => 'Non-Authoritative Information', + StatusCode::HTTP_NO_CONTENT => 'No Content', + StatusCode::HTTP_RESET_CONTENT => 'Reset Content', + StatusCode::HTTP_PARTIAL_CONTENT => 'Partial Content', + StatusCode::HTTP_MULTI_STATUS => 'Multi-Status', + StatusCode::HTTP_ALREADY_REPORTED => 'Already Reported', + StatusCode::HTTP_IM_USED => 'IM Used', + //Redirection 3xx + StatusCode::HTTP_MULTIPLE_CHOICES => 'Multiple Choices', + StatusCode::HTTP_MOVED_PERMANENTLY => 'Moved Permanently', + StatusCode::HTTP_FOUND => 'Found', + StatusCode::HTTP_SEE_OTHER => 'See Other', + StatusCode::HTTP_NOT_MODIFIED => 'Not Modified', + StatusCode::HTTP_USE_PROXY => 'Use Proxy', + StatusCode::HTTP_UNUSED => '(Unused)', + StatusCode::HTTP_TEMPORARY_REDIRECT => 'Temporary Redirect', + StatusCode::HTTP_PERMANENT_REDIRECT => 'Permanent Redirect', + //Client Error 4xx + StatusCode::HTTP_BAD_REQUEST => 'Bad Request', + StatusCode::HTTP_UNAUTHORIZED => 'Unauthorized', + StatusCode::HTTP_PAYMENT_REQUIRED => 'Payment Required', + StatusCode::HTTP_FORBIDDEN => 'Forbidden', + StatusCode::HTTP_NOT_FOUND => 'Not Found', + StatusCode::HTTP_METHOD_NOT_ALLOWED => 'Method Not Allowed', + StatusCode::HTTP_NOT_ACCEPTABLE => 'Not Acceptable', + StatusCode::HTTP_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', + StatusCode::HTTP_REQUEST_TIMEOUT => 'Request Timeout', + StatusCode::HTTP_CONFLICT => 'Conflict', + StatusCode::HTTP_GONE => 'Gone', + StatusCode::HTTP_LENGTH_REQUIRED => 'Length Required', + StatusCode::HTTP_PRECONDITION_FAILED => 'Precondition Failed', + StatusCode::HTTP_REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large', + StatusCode::HTTP_REQUEST_URI_TOO_LONG => 'Request-URI Too Long', + StatusCode::HTTP_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', + StatusCode::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable', + StatusCode::HTTP_EXPECTATION_FAILED => 'Expectation Failed', + StatusCode::HTTP_IM_A_TEAPOT => 'I\'m a teapot', + StatusCode::HTTP_MISDIRECTED_REQUEST => 'Misdirected Request', + StatusCode::HTTP_UNPROCESSABLE_ENTITY => 'Unprocessable Entity', + StatusCode::HTTP_LOCKED => 'Locked', + StatusCode::HTTP_FAILED_DEPENDENCY => 'Failed Dependency', + StatusCode::HTTP_UPGRADE_REQUIRED => 'Upgrade Required', + StatusCode::HTTP_PRECONDITION_REQUIRED => 'Precondition Required', + StatusCode::HTTP_TOO_MANY_REQUESTS => 'Too Many Requests', + StatusCode::HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', + StatusCode::HTTP_CONNECTION_CLOSED_WITHOUT_RESPONSE => 'Connection Closed Without Response', + StatusCode::HTTP_UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons', + StatusCode::HTTP_CLIENT_CLOSED_REQUEST => 'Client Closed Request', + //Server Error 5xx + StatusCode::HTTP_INTERNAL_SERVER_ERROR => 'Internal Server Error', + StatusCode::HTTP_NOT_IMPLEMENTED => 'Not Implemented', + StatusCode::HTTP_BAD_GATEWAY => 'Bad Gateway', + StatusCode::HTTP_SERVICE_UNAVAILABLE => 'Service Unavailable', + StatusCode::HTTP_GATEWAY_TIMEOUT => 'Gateway Timeout', + StatusCode::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported', + StatusCode::HTTP_VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates', + StatusCode::HTTP_INSUFFICIENT_STORAGE => 'Insufficient Storage', + StatusCode::HTTP_LOOP_DETECTED => 'Loop Detected', + StatusCode::HTTP_NOT_EXTENDED => 'Not Extended', + StatusCode::HTTP_NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', + StatusCode::HTTP_NETWORK_CONNECTION_TIMEOUT_ERROR => 'Network Connect Timeout Error', + ]; + + /** + * EOL characters used for HTTP response. + * + * @var string + */ + const EOL = "\r\n"; + + /** + * @param int $status The response status code. + * @param HeadersInterface|null $headers The response headers. + * @param StreamInterface|null $body The response body. + */ + public function __construct( + $status = StatusCode::HTTP_OK, + HeadersInterface $headers = null, + StreamInterface $body = null + ) { + $this->status = $this->filterStatus($status); + $this->headers = $headers ? $headers : new Headers(); + $this->body = $body ? $body : new Body(fopen('php://temp', 'r+')); + } + + /** + * This method is applied to the cloned object + * after PHP performs an initial shallow-copy. This + * method completes a deep-copy by creating new objects + * for the cloned object's internal reference pointers. + */ + public function __clone() + { + $this->headers = clone $this->headers; + } + + /** + * Gets the response status code. + * + * The status code is a 3-digit integer result code of the server's attempt + * to understand and satisfy the request. + * + * @return int + */ + public function getStatusCode() + { + return $this->status; + } + + /** + * Return an instance with the specified status code and, optionally, reason phrase. + * + * If no reason phrase is specified, implementations MAY choose to default + * to the RFC 7231 or IANA recommended reason phrase for the response's + * status code. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * + * @param int $code The 3-digit integer result code to set. + * @param string $reasonPhrase The reason phrase to use with the + * provided status code; if none is provided, implementations MAY + * use the defaults as suggested in the HTTP specification. + * + * @return static + * + * @throws InvalidArgumentException For invalid status code arguments. + */ + public function withStatus($code, $reasonPhrase = '') + { + $code = $this->filterStatus($code); + + if (!is_string($reasonPhrase) && !method_exists($reasonPhrase, '__toString')) { + throw new InvalidArgumentException('ReasonPhrase must be a string'); + } + + $clone = clone $this; + $clone->status = $code; + if ($reasonPhrase === '' && isset(static::$messages[$code])) { + $reasonPhrase = static::$messages[$code]; + } + + if ($reasonPhrase === '') { + throw new InvalidArgumentException('ReasonPhrase must be supplied for this code'); + } + + $clone->reasonPhrase = $reasonPhrase; + + return $clone; + } + + /** + * Filter HTTP status code. + * + * @param int $status HTTP status code. + * + * @return int + * + * @throws InvalidArgumentException If an invalid HTTP status code is provided. + */ + protected function filterStatus($status) + { + if (!is_integer($status) || + $statusStatusCode::HTTP_NETWORK_CONNECTION_TIMEOUT_ERROR + ) { + throw new InvalidArgumentException('Invalid HTTP status code'); + } + + return $status; + } + + /** + * Gets the response reason phrase associated with the status code. + * + * Because a reason phrase is not a required element in a response + * status line, the reason phrase value MAY be null. Implementations MAY + * choose to return the default RFC 7231 recommended reason phrase (or those + * listed in the IANA HTTP Status Code Registry) for the response's + * status code. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * + * @return string Reason phrase; must return an empty string if none present. + */ + public function getReasonPhrase() + { + if ($this->reasonPhrase) { + return $this->reasonPhrase; + } + if (isset(static::$messages[$this->status])) { + return static::$messages[$this->status]; + } + return ''; + } + + /** + * Return an instance with the provided value replacing the specified header. + * + * If a Location header is set and the status code is 200, then set the status + * code to 302 to mimic what PHP does. See https://github.com/slimphp/Slim/issues/1730 + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * + * @return static + * + * @throws InvalidArgumentException For invalid header names or values. + */ + public function withHeader($name, $value) + { + $clone = clone $this; + $clone->headers->set($name, $value); + + if ($this->body instanceof NonBufferedBody) { + header(sprintf('%s: %s', $name, $clone->getHeaderLine($name))); + } + + if ($clone->getStatusCode() === StatusCode::HTTP_OK && strtolower($name) === 'location') { + $clone = $clone->withStatus(StatusCode::HTTP_FOUND); + } + + return $clone; + } + + /** + * Write data to the response body. + * + * Note: This method is not part of the PSR-7 standard. + * + * Proxies to the underlying stream and writes the provided data to it. + * + * @param string $data + * + * @return static + */ + public function write($data) + { + $this->getBody()->write($data); + + return $this; + } + + /** + * Redirect. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method prepares the response object to return an HTTP Redirect + * response to the client. + * + * @param string|UriInterface $url The redirect destination. + * @param int|null $status The redirect HTTP status code. + * + * @return static + */ + public function withRedirect($url, $status = null) + { + $responseWithRedirect = $this->withHeader('Location', (string)$url); + + if (is_null($status) && $this->getStatusCode() === StatusCode::HTTP_OK) { + $status = StatusCode::HTTP_FOUND; + } + + if (!is_null($status)) { + return $responseWithRedirect->withStatus($status); + } + + return $responseWithRedirect; + } + + /** + * Json. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method prepares the response object to return an HTTP Json + * response to the client. + * + * @param mixed $data The data + * @param int $status The HTTP status code. + * @param int $encodingOptions Json encoding options + * + * @return static + * + * @throws RuntimeException + */ + public function withJson($data, $status = null, $encodingOptions = 0) + { + $response = $this->withBody(new Body(fopen('php://temp', 'r+'))); + $response->body->write($json = json_encode($data, $encodingOptions)); + + // Ensure that the json encoding passed successfully + if ($json === false) { + throw new RuntimeException(json_last_error_msg(), json_last_error()); + } + + $responseWithJson = $response->withHeader('Content-Type', 'application/json'); + if (isset($status)) { + return $responseWithJson->withStatus($status); + } + return $responseWithJson; + } + + /** + * Is this response empty? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isEmpty() + { + return in_array( + $this->getStatusCode(), + [StatusCode::HTTP_NO_CONTENT, StatusCode::HTTP_RESET_CONTENT, StatusCode::HTTP_NOT_MODIFIED] + ); + } + + /** + * Is this response informational? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isInformational() + { + return $this->getStatusCode() >= StatusCode::HTTP_CONTINUE && $this->getStatusCode() < StatusCode::HTTP_OK; + } + + /** + * Is this response OK? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isOk() + { + return $this->getStatusCode() === StatusCode::HTTP_OK; + } + + /** + * Is this response successful? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isSuccessful() + { + return $this->getStatusCode() >= StatusCode::HTTP_OK && + $this->getStatusCode() < StatusCode::HTTP_MULTIPLE_CHOICES; + } + + /** + * Is this response a redirect? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isRedirect() + { + return in_array( + $this->getStatusCode(), + [ + StatusCode::HTTP_MOVED_PERMANENTLY, + StatusCode::HTTP_FOUND, + StatusCode::HTTP_SEE_OTHER, + StatusCode::HTTP_TEMPORARY_REDIRECT, + StatusCode::HTTP_PERMANENT_REDIRECT + ] + ); + } + + /** + * Is this response a redirection? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isRedirection() + { + return $this->getStatusCode() >= StatusCode::HTTP_MULTIPLE_CHOICES && + $this->getStatusCode() < StatusCode::HTTP_BAD_REQUEST; + } + + /** + * Is this response forbidden? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isForbidden() + { + return $this->getStatusCode() === StatusCode::HTTP_FORBIDDEN; + } + + /** + * Is this response not Found? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isNotFound() + { + return $this->getStatusCode() === StatusCode::HTTP_NOT_FOUND; + } + + /** + * Is this a bad request? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isBadRequest() + { + return $this->getStatusCode() === StatusCode::HTTP_BAD_REQUEST; + } + + /** + * Is this response a client error? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isClientError() + { + return $this->getStatusCode() >= StatusCode::HTTP_BAD_REQUEST && + $this->getStatusCode() < StatusCode::HTTP_INTERNAL_SERVER_ERROR; + } + + /** + * Is this response a server error? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isServerError() + { + return $this->getStatusCode() >= StatusCode::HTTP_INTERNAL_SERVER_ERROR && $this->getStatusCode() < 600; + } + + /** + * Convert response to string. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string + */ + public function __toString() + { + $output = sprintf( + 'HTTP/%s %s %s', + $this->getProtocolVersion(), + $this->getStatusCode(), + $this->getReasonPhrase() + ); + $output .= Response::EOL; + foreach ($this->getHeaders() as $name => $values) { + $output .= sprintf('%s: %s', $name, $this->getHeaderLine($name)) . Response::EOL; + } + $output .= Response::EOL; + $output .= (string)$this->getBody(); + + return $output; + } +} diff --git a/vendor/slim/slim/Slim/Http/StatusCode.php b/vendor/slim/slim/Slim/Http/StatusCode.php new file mode 100644 index 0000000..8a0899a --- /dev/null +++ b/vendor/slim/slim/Slim/Http/StatusCode.php @@ -0,0 +1,80 @@ + ['r', 'r+', 'w+', 'a+', 'x+', 'c+'], + 'writable' => ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'], + ]; + + /** + * The underlying stream resource + * + * @var resource + */ + protected $stream; + + /** + * Stream metadata + * + * @var array + */ + protected $meta; + + /** + * Is this stream readable? + * + * @var bool + */ + protected $readable; + + /** + * Is this stream writable? + * + * @var bool + */ + protected $writable; + + /** + * Is this stream seekable? + * + * @var bool + */ + protected $seekable; + + /** + * The size of the stream if known + * + * @var null|int + */ + protected $size; + + /** + * Is this stream a pipe? + * + * @var bool + */ + protected $isPipe; + + /** + * @param resource $stream A PHP resource handle. + * + * @throws InvalidArgumentException If argument is not a resource. + */ + public function __construct($stream) + { + $this->attach($stream); + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * The keys returned are identical to the keys returned from PHP's stream_get_meta_data() function. + * + * @link http://php.net/manual/en/function.stream-get-meta-data.php + * + * @param string $key Specific metadata to retrieve. + * + * @return array|mixed|null Returns an associative array if no key is + * provided. Returns a specific key value if a key is provided and the + * value is found, or null if the key is not found. + */ + public function getMetadata($key = null) + { + $this->meta = stream_get_meta_data($this->stream); + if (is_null($key) === true) { + return $this->meta; + } + + return isset($this->meta[$key]) ? $this->meta[$key] : null; + } + + /** + * Is a resource attached to this stream? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + protected function isAttached() + { + return is_resource($this->stream); + } + + /** + * Attach new resource to this object. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param resource $newStream A PHP resource handle. + * + * @throws InvalidArgumentException If argument is not a valid PHP resource. + */ + protected function attach($newStream) + { + if (is_resource($newStream) === false) { + throw new InvalidArgumentException(__METHOD__ . ' argument must be a valid PHP resource'); + } + + if ($this->isAttached() === true) { + $this->detach(); + } + + $this->stream = $newStream; + } + + /** + * Separates any underlying resources from the stream. + * + * After the stream has been detached, the stream is in an unusable state. + * + * @return resource|null Underlying PHP stream, if any + */ + public function detach() + { + $oldResource = $this->stream; + $this->stream = null; + $this->meta = null; + $this->readable = null; + $this->writable = null; + $this->seekable = null; + $this->size = null; + $this->isPipe = null; + + return $oldResource; + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + * + * This method MUST attempt to seek to the beginning of the stream before + * reading data and read the stream until the end is reached. + * + * Warning: This could attempt to load a large amount of data into memory. + * + * This method MUST NOT raise an exception in order to conform with PHP's + * string casting operations. + * + * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring + * + * @return string + */ + public function __toString() + { + if (!$this->isAttached()) { + return ''; + } + + try { + $this->rewind(); + return $this->getContents(); + } catch (RuntimeException $e) { + return ''; + } + } + + /** + * Closes the stream and any underlying resources. + */ + public function close() + { + if ($this->isAttached() === true) { + if ($this->isPipe()) { + pclose($this->stream); + } else { + fclose($this->stream); + } + } + + $this->detach(); + } + + /** + * Get the size of the stream if known. + * + * @return int|null Returns the size in bytes if known, or null if unknown. + */ + public function getSize() + { + if (!$this->size && $this->isAttached() === true) { + $stats = fstat($this->stream); + $this->size = isset($stats['size']) && !$this->isPipe() ? $stats['size'] : null; + } + + return $this->size; + } + + /** + * Returns the current position of the file read/write pointer + * + * @return int Position of the file pointer + * + * @throws RuntimeException on error. + */ + public function tell() + { + if (!$this->isAttached() || ($position = ftell($this->stream)) === false || $this->isPipe()) { + throw new RuntimeException('Could not get the position of the pointer in stream'); + } + + return $position; + } + + /** + * Returns true if the stream is at the end of the stream. + * + * @return bool + */ + public function eof() + { + return $this->isAttached() ? feof($this->stream) : true; + } + + /** + * Returns whether or not the stream is readable. + * + * @return bool + */ + public function isReadable() + { + if ($this->readable === null) { + if ($this->isPipe()) { + $this->readable = true; + } else { + $this->readable = false; + if ($this->isAttached()) { + $meta = $this->getMetadata(); + foreach (self::$modes['readable'] as $mode) { + if (strpos($meta['mode'], $mode) === 0) { + $this->readable = true; + break; + } + } + } + } + } + + return $this->readable; + } + + /** + * Returns whether or not the stream is writable. + * + * @return bool + */ + public function isWritable() + { + if ($this->writable === null) { + $this->writable = false; + if ($this->isAttached()) { + $meta = $this->getMetadata(); + foreach (self::$modes['writable'] as $mode) { + if (strpos($meta['mode'], $mode) === 0) { + $this->writable = true; + break; + } + } + } + } + + return $this->writable; + } + + /** + * Returns whether or not the stream is seekable. + * + * @return bool + */ + public function isSeekable() + { + if ($this->seekable === null) { + $this->seekable = false; + if ($this->isAttached()) { + $meta = $this->getMetadata(); + $this->seekable = !$this->isPipe() && $meta['seekable']; + } + } + + return $this->seekable; + } + + /** + * Seek to a position in the stream. + * + * @link http://www.php.net/manual/en/function.fseek.php + * + * @param int $offset Stream offset + * @param int $whence Specifies how the cursor position will be calculated + * based on the seek offset. Valid values are identical to the built-in + * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to + * offset bytes SEEK_CUR: Set position to current location plus offset + * SEEK_END: Set position to end-of-stream plus offset. + * + * @throws RuntimeException If stream is not seekable + */ + public function seek($offset, $whence = SEEK_SET) + { + // Note that fseek returns 0 on success! + if (!$this->isSeekable() || fseek($this->stream, $offset, $whence) === -1) { + throw new RuntimeException('Could not seek in stream'); + } + } + + /** + * Seek to the beginning of the stream. + * + * If the stream is not seekable, this method will raise an exception; + * otherwise, it will perform a seek(0). + * + * @see seek() + * + * @link http://www.php.net/manual/en/function.fseek.php + * + * @throws RuntimeException on failure. + */ + public function rewind() + { + if (!$this->isSeekable() || rewind($this->stream) === false) { + throw new RuntimeException('Could not rewind stream'); + } + } + + /** + * Read data from the stream. + * + * @param int $length Read up to $length bytes from the object and return + * them. Fewer than $length bytes may be returned if underlying stream + * call returns fewer bytes. + * + * @return string Returns the data read from the stream, or an empty string if no bytes are available. + * + * @throws RuntimeException if an error occurs. + */ + public function read($length) + { + if (!$this->isReadable() || ($data = fread($this->stream, $length)) === false) { + throw new RuntimeException('Could not read from stream'); + } + + return $data; + } + + /** + * Write data to the stream. + * + * @param string $string The string that is to be written. + * + * @return int Returns the number of bytes written to the stream. + * + * @throws RuntimeException If stream is not writable + */ + public function write($string) + { + if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) { + throw new RuntimeException('Could not write to stream'); + } + + // reset size so that it will be recalculated on next call to getSize() + $this->size = null; + + return $written; + } + + /** + * Returns the remaining contents in a string + * + * @return string + * + * @throws RuntimeException If stream is not readable + */ + public function getContents() + { + if (!$this->isReadable() || ($contents = stream_get_contents($this->stream)) === false) { + throw new RuntimeException('Could not get contents of stream'); + } + + return $contents; + } + + /** + * Returns whether or not the stream is a pipe. + * + * @return bool + */ + public function isPipe() + { + if ($this->isPipe === null) { + $this->isPipe = false; + if ($this->isAttached()) { + $mode = fstat($this->stream)['mode']; + $this->isPipe = ($mode & self::FSTAT_MODE_S_IFIFO) !== 0; + } + } + + return $this->isPipe; + } +} diff --git a/vendor/slim/slim/Slim/Http/UploadedFile.php b/vendor/slim/slim/Slim/Http/UploadedFile.php new file mode 100644 index 0000000..401440d --- /dev/null +++ b/vendor/slim/slim/Slim/Http/UploadedFile.php @@ -0,0 +1,330 @@ +has('slim.files')) { + return $env['slim.files']; + } elseif (! empty($_FILES)) { + return static::parseUploadedFiles($_FILES); + } + + return []; + } + + /** + * Parse a non-normalized, i.e. $_FILES superglobal, tree of uploaded file data. + * + * Returns a normalized tree of UploadedFile instances. + * + * @param array $uploadedFiles The non-normalized tree of uploaded file data. + * + * @return array + */ + private static function parseUploadedFiles(array $uploadedFiles) + { + $parsed = []; + foreach ($uploadedFiles as $field => $uploadedFile) { + if (!isset($uploadedFile['error'])) { + if (is_array($uploadedFile)) { + $parsed[$field] = static::parseUploadedFiles($uploadedFile); + } + continue; + } + + $parsed[$field] = []; + if (!is_array($uploadedFile['error'])) { + $parsed[$field] = new static( + $uploadedFile['tmp_name'], + isset($uploadedFile['name']) ? $uploadedFile['name'] : null, + isset($uploadedFile['type']) ? $uploadedFile['type'] : null, + isset($uploadedFile['size']) ? $uploadedFile['size'] : null, + $uploadedFile['error'], + true + ); + } else { + $subArray = []; + foreach ($uploadedFile['error'] as $fileIdx => $error) { + // normalise subarray and re-parse to move the input's keyname up a level + $subArray[$fileIdx]['name'] = $uploadedFile['name'][$fileIdx]; + $subArray[$fileIdx]['type'] = $uploadedFile['type'][$fileIdx]; + $subArray[$fileIdx]['tmp_name'] = $uploadedFile['tmp_name'][$fileIdx]; + $subArray[$fileIdx]['error'] = $uploadedFile['error'][$fileIdx]; + $subArray[$fileIdx]['size'] = $uploadedFile['size'][$fileIdx]; + + $parsed[$field] = static::parseUploadedFiles($subArray); + } + } + } + + return $parsed; + } + + /** + * @param string $file The full path to the uploaded file provided by the client. + * @param string|null $name The file name. + * @param string|null $type The file media type. + * @param int|null $size The file size in bytes. + * @param int $error The UPLOAD_ERR_XXX code representing the status of the upload. + * @param bool $sapi Indicates if the upload is in a SAPI environment. + */ + public function __construct($file, $name = null, $type = null, $size = null, $error = UPLOAD_ERR_OK, $sapi = false) + { + $this->file = $file; + $this->name = $name; + $this->type = $type; + $this->size = $size; + $this->error = $error; + $this->sapi = $sapi; + } + + /** + * Retrieve a stream representing the uploaded file. + * + * This method MUST return a StreamInterface instance, representing the + * uploaded file. The purpose of this method is to allow utilizing native PHP + * stream functionality to manipulate the file upload, such as + * stream_copy_to_stream() (though the result will need to be decorated in a + * native PHP stream wrapper to work with such functions). + * + * If the moveTo() method has been called previously, this method MUST raise + * an exception. + * + * @return StreamInterface + * + * @throws RuntimeException in cases when no stream is available or can be created. + */ + public function getStream() + { + if ($this->moved) { + throw new RuntimeException(sprintf('Uploaded file %s has already been moved', $this->name)); + } + if ($this->stream === null) { + $this->stream = new Stream(fopen($this->file, 'r')); + } + + return $this->stream; + } + + /** + * Move the uploaded file to a new location. + * + * Use this method as an alternative to move_uploaded_file(). This method is + * guaranteed to work in both SAPI and non-SAPI environments. + * Implementations must determine which environment they are in, and use the + * appropriate method (move_uploaded_file(), rename(), or a stream + * operation) to perform the operation. + * + * $targetPath may be an absolute path, or a relative path. If it is a + * relative path, resolution should be the same as used by PHP's rename() + * function. + * + * The original file or stream MUST be removed on completion. + * + * If this method is called more than once, any subsequent calls MUST raise + * an exception. + * + * When used in an SAPI environment where $_FILES is populated, when writing + * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be + * used to ensure permissions and upload status are verified correctly. + * + * If you wish to move to a stream, use getStream(), as SAPI operations + * cannot guarantee writing to stream destinations. + * + * @see http://php.net/is_uploaded_file + * @see http://php.net/move_uploaded_file + * + * @param string $targetPath Path to which to move the uploaded file. + * + * @throws InvalidArgumentException If the $path specified is invalid. + * @throws RuntimeException On any error during the move operation or on the second subsequent call to the method. + */ + public function moveTo($targetPath) + { + if ($this->moved) { + throw new RuntimeException('Uploaded file already moved'); + } + + $targetIsStream = strpos($targetPath, '://') > 0; + if (!$targetIsStream && !is_writable(dirname($targetPath))) { + throw new InvalidArgumentException('Upload target path is not writable'); + } + + if ($targetIsStream) { + if (!copy($this->file, $targetPath)) { + throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); + } + if (!unlink($this->file)) { + throw new RuntimeException(sprintf('Error removing uploaded file %s', $this->name)); + } + } elseif ($this->sapi) { + if (!is_uploaded_file($this->file)) { + throw new RuntimeException(sprintf('%s is not a valid uploaded file', $this->file)); + } + + if (!move_uploaded_file($this->file, $targetPath)) { + throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); + } + } else { + if (!rename($this->file, $targetPath)) { + throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); + } + } + + $this->moved = true; + } + + /** + * Retrieve the error associated with the uploaded file. + * + * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants. + * + * If the file was uploaded successfully, this method MUST return + * UPLOAD_ERR_OK. + * + * Implementations SHOULD return the value stored in the "error" key of + * the file in the $_FILES array. + * + * @see http://php.net/manual/en/features.file-upload.errors.php + * + * @return int + */ + public function getError() + { + return $this->error; + } + + /** + * Retrieve the filename sent by the client. + * + * Do not trust the value returned by this method. A client could send + * a malicious filename with the intention to corrupt or hack your + * application. + * + * Implementations SHOULD return the value stored in the "name" key of + * the file in the $_FILES array. + * + * @return string|null + */ + public function getClientFilename() + { + return $this->name; + } + + /** + * Retrieve the media type sent by the client. + * + * Do not trust the value returned by this method. A client could send + * a malicious media type with the intention to corrupt or hack your + * application. + * + * Implementations SHOULD return the value stored in the "type" key of + * the file in the $_FILES array. + * + * @return string|null + */ + public function getClientMediaType() + { + return $this->type; + } + + /** + * Retrieve the file size. + * + * Implementations SHOULD return the value stored in the "size" key of + * the file in the $_FILES array if available, as PHP calculates this based + * on the actual size transmitted. + * + * @return int|null + */ + public function getSize() + { + return $this->size; + } +} diff --git a/vendor/slim/slim/Slim/Http/Uri.php b/vendor/slim/slim/Slim/Http/Uri.php new file mode 100644 index 0000000..5c67ec8 --- /dev/null +++ b/vendor/slim/slim/Slim/Http/Uri.php @@ -0,0 +1,839 @@ +scheme = $this->filterScheme($scheme); + $this->host = $host; + $this->port = $this->filterPort($port); + $this->path = ($path === null || !strlen($path)) ? '/' : $this->filterPath($path); + $this->query = $this->filterQuery($query); + $this->fragment = $this->filterQuery($fragment); + $this->user = $user; + $this->password = $password; + } + + /** + * Create new Uri from string. + * + * @param string $uri Complete Uri string (i.e., https://user:pass@host:443/path?query). + * + * @return self + */ + public static function createFromString($uri) + { + if (!is_string($uri) && !method_exists($uri, '__toString')) { + throw new InvalidArgumentException('Uri must be a string'); + } + + $parts = parse_url($uri); + $scheme = isset($parts['scheme']) ? $parts['scheme'] : ''; + $user = isset($parts['user']) ? $parts['user'] : ''; + $pass = isset($parts['pass']) ? $parts['pass'] : ''; + $host = isset($parts['host']) ? $parts['host'] : ''; + $port = isset($parts['port']) ? $parts['port'] : null; + $path = isset($parts['path']) ? $parts['path'] : ''; + $query = isset($parts['query']) ? $parts['query'] : ''; + $fragment = isset($parts['fragment']) ? $parts['fragment'] : ''; + + return new static($scheme, $host, $port, $path, $query, $fragment, $user, $pass); + } + + /** + * Create new Uri from environment. + * + * @param Environment $env + * + * @return self + */ + public static function createFromEnvironment(Environment $env) + { + // Scheme + $isSecure = $env->get('HTTPS'); + $scheme = (empty($isSecure) || $isSecure === 'off') ? 'http' : 'https'; + + // Authority: Username and password + $username = $env->get('PHP_AUTH_USER', ''); + $password = $env->get('PHP_AUTH_PW', ''); + + // Authority: Host and Port + if ($env->has('HTTP_HOST')) { + $host = $env->get('HTTP_HOST'); + // set a port default + $port = null; + } else { + $host = $env->get('SERVER_NAME'); + // set a port default + $port = (int)$env->get('SERVER_PORT', 80); + } + + if (preg_match('/^(\[[a-fA-F0-9:.]+\])(:\d+)?\z/', $host, $matches)) { + $host = $matches[1]; + + if (isset($matches[2])) { + $port = (int) substr($matches[2], 1); + } + } else { + $pos = strpos($host, ':'); + if ($pos !== false) { + $port = (int) substr($host, $pos + 1); + $host = strstr($host, ':', true); + } + } + + // Path + $requestScriptName = (string) parse_url($env->get('SCRIPT_NAME'), PHP_URL_PATH); + $requestScriptDir = dirname($requestScriptName); + + // parse_url() requires a full URL. As we don't extract the domain name or scheme, + // we use a stand-in. + $requestUri = (string) parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_PATH); + + $basePath = ''; + $virtualPath = $requestUri; + if (stripos($requestUri, $requestScriptName) === 0) { + $basePath = $requestScriptName; + } elseif ($requestScriptDir !== '/' && stripos($requestUri, $requestScriptDir) === 0) { + $basePath = $requestScriptDir; + } + + if ($basePath) { + $virtualPath = ltrim(substr($requestUri, strlen($basePath)), '/'); + } + + // Query string + $queryString = $env->get('QUERY_STRING', ''); + if ($queryString === '') { + $queryString = parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_QUERY); + } + + // Fragment + $fragment = ''; + + // Build Uri + $uri = new static($scheme, $host, $port, $virtualPath, $queryString, $fragment, $username, $password); + if ($basePath) { + $uri = $uri->withBasePath($basePath); + } + + return $uri; + } + + /** + * Retrieve the scheme component of the URI. + * + * If no scheme is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.1. + * + * The trailing ":" character is not part of the scheme and MUST NOT be + * added. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.1 + * + * @return string The URI scheme. + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * + * @return self A new instance with the specified scheme. + * + * @throws InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme($scheme) + { + $scheme = $this->filterScheme($scheme); + $clone = clone $this; + $clone->scheme = $scheme; + + return $clone; + } + + /** + * Filter Uri scheme. + * + * @param string $scheme Raw Uri scheme. + * @return string + * + * @throws InvalidArgumentException If the Uri scheme is not a string. + * @throws InvalidArgumentException If Uri scheme is not "", "https", or "http". + */ + protected function filterScheme($scheme) + { + static $valid = [ + '' => true, + 'https' => true, + 'http' => true, + ]; + + if (!is_string($scheme) && !method_exists($scheme, '__toString')) { + throw new InvalidArgumentException('Uri scheme must be a string'); + } + + $scheme = str_replace('://', '', strtolower((string)$scheme)); + if (!isset($valid[$scheme])) { + throw new InvalidArgumentException('Uri scheme must be one of: "", "https", "http"'); + } + + return $scheme; + } + + /** + * Retrieve the authority component of the URI. + * + * If no authority information is present, this method MUST return an empty + * string. + * + * The authority syntax of the URI is: + * + *
+     * [user-info@]host[:port]
+     * 
+ * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority() + { + $userInfo = $this->getUserInfo(); + $host = $this->getHost(); + $port = $this->getPort(); + + return ($userInfo !== '' ? $userInfo . '@' : '') . $host . ($port !== null ? ':' . $port : ''); + } + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo() + { + return $this->user . ($this->password !== '' ? ':' . $this->password : ''); + } + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * + * @return self A new instance with the specified user information. + */ + public function withUserInfo($user, $password = null) + { + $clone = clone $this; + $clone->user = $this->filterUserInfo($user); + if ('' !== $clone->user) { + $clone->password = !in_array($password, [null, ''], true) ? $this->filterUserInfo($password) : ''; + } else { + $clone->password = ''; + } + + return $clone; + } + + /** + * Filters the user info string. + * + * @param string $query The raw uri query string. + * + * @return string The percent-encoded query string. + */ + protected function filterUserInfo($query) + { + return preg_replace_callback( + '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=]+|%(?![A-Fa-f0-9]{2}))/u', + function ($match) { + return rawurlencode($match[0]); + }, + $query + ); + } + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * + * @return string The URI host. + */ + public function getHost() + { + return $this->host; + } + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * + * @return self A new instance with the specified host. + */ + public function withHost($host) + { + $clone = clone $this; + $clone->host = $host; + + return $clone; + } + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort() + { + return $this->port && !$this->hasStandardPort() ? $this->port : null; + } + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * + * @return self A new instance with the specified port. + */ + public function withPort($port) + { + $port = $this->filterPort($port); + $clone = clone $this; + $clone->port = $port; + + return $clone; + } + + /** + * Does this Uri use a standard port? + * + * @return bool + */ + protected function hasStandardPort() + { + return ($this->scheme === 'http' && $this->port === 80) || ($this->scheme === 'https' && $this->port === 443); + } + + /** + * Filter Uri port. + * + * @param null|int $port The Uri port number. + * @return null|int + * + * @throws InvalidArgumentException If the port is invalid. + */ + protected function filterPort($port) + { + if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) { + return $port; + } + + throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)'); + } + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * + * @return string The URI path. + */ + public function getPath() + { + return $this->path; + } + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * + * @return static A new instance with the specified path. + * + * @throws InvalidArgumentException For invalid paths. + */ + public function withPath($path) + { + if (!is_string($path)) { + throw new InvalidArgumentException('Uri path must be a string'); + } + + $clone = clone $this; + $clone->path = $this->filterPath($path); + + // if the path is absolute, then clear basePath + if (substr($path, 0, 1) == '/') { + $clone->basePath = ''; + } + + return $clone; + } + + /** + * Retrieve the base path segment of the URI. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method MUST return a string; if no path is present it MUST return + * an empty string. + * + * @return string The base path segment of the URI. + */ + public function getBasePath() + { + return $this->basePath; + } + + /** + * Set base path. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $basePath + * + * @return static + */ + public function withBasePath($basePath) + { + if (!is_string($basePath)) { + throw new InvalidArgumentException('Uri path must be a string'); + } + if (!empty($basePath)) { + $basePath = '/' . trim($basePath, '/'); // <-- Trim on both sides + } + $clone = clone $this; + + if ($basePath !== '/') { + $clone->basePath = $this->filterPath($basePath); + } + + return $clone; + } + + /** + * Filter Uri path. + * + * Returns a RFC 3986 percent-encoded uri path. + * + * This method percent-encodes all reserved + * characters in the provided path string. This method + * will NOT double-encode characters that are already + * percent-encoded. + * + * @param string $path The raw uri path. + * + * @return string + * + * @link http://www.faqs.org/rfcs/rfc3986.html + */ + protected function filterPath($path) + { + return preg_replace_callback( + '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/', + function ($match) { + return rawurlencode($match[0]); + }, + $path + ); + } + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * + * @return string + */ + public function getQuery() + { + return $this->query; + } + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * + * @return self A new instance with the specified query string. + * + * @throws InvalidArgumentException For invalid query strings. + */ + public function withQuery($query) + { + if (!is_string($query) && !method_exists($query, '__toString')) { + throw new InvalidArgumentException('Uri query must be a string'); + } + $query = ltrim((string)$query, '?'); + $clone = clone $this; + $clone->query = $this->filterQuery($query); + + return $clone; + } + + /** + * Filters the query string or fragment of a URI. + * + * @param string $query The raw uri query string. + * + * @return string The percent-encoded query string. + */ + protected function filterQuery($query) + { + return preg_replace_callback( + '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/', + function ($match) { + return rawurlencode($match[0]); + }, + $query + ); + } + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * + * @return string The URI fragment. + */ + public function getFragment() + { + return $this->fragment; + } + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * + * @return static A new instance with the specified fragment. + */ + public function withFragment($fragment) + { + if (!is_string($fragment) && !method_exists($fragment, '__toString')) { + throw new InvalidArgumentException('Uri fragment must be a string'); + } + $fragment = ltrim((string)$fragment, '#'); + $clone = clone $this; + $clone->fragment = $this->filterQuery($fragment); + + return $clone; + } + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * + * @return string + */ + public function __toString() + { + $scheme = $this->getScheme(); + $authority = $this->getAuthority(); + $basePath = $this->getBasePath(); + $path = $this->getPath(); + $query = $this->getQuery(); + $fragment = $this->getFragment(); + + $path = $basePath . '/' . ltrim($path, '/'); + + return ($scheme !== '' ? $scheme . ':' : '') + . ($authority !== '' ? '//' . $authority : '') + . $path + . ($query !== '' ? '?' . $query : '') + . ($fragment !== '' ? '#' . $fragment : ''); + } + + /** + * Return the fully qualified base URL. + * + * Note that this method never includes a trailing / + * + * This method is not part of PSR-7. + * + * @return string + */ + public function getBaseUrl() + { + $scheme = $this->getScheme(); + $authority = $this->getAuthority(); + $basePath = $this->getBasePath(); + + if ($authority !== '' && substr($basePath, 0, 1) !== '/') { + $basePath = $basePath . '/' . $basePath; + } + + return ($scheme !== '' ? $scheme . ':' : '') + . ($authority ? '//' . $authority : '') + . rtrim($basePath, '/'); + } +} diff --git a/vendor/slim/slim/Slim/Interfaces/CallableResolverInterface.php b/vendor/slim/slim/Slim/Interfaces/CallableResolverInterface.php new file mode 100644 index 0000000..b3fd3fa --- /dev/null +++ b/vendor/slim/slim/Slim/Interfaces/CallableResolverInterface.php @@ -0,0 +1,24 @@ +middlewareLock) { + throw new RuntimeException('Middleware can’t be added once the stack is dequeuing'); + } + + if (is_null($this->tip)) { + $this->seedMiddlewareStack(); + } + $next = $this->tip; + $this->tip = function ( + ServerRequestInterface $request, + ResponseInterface $response + ) use ( + $callable, + $next + ) { + $result = call_user_func($callable, $request, $response, $next); + if ($result instanceof ResponseInterface === false) { + throw new UnexpectedValueException( + 'Middleware must return instance of \Psr\Http\Message\ResponseInterface' + ); + } + + return $result; + }; + + return $this; + } + + /** + * Seed middleware stack with first callable + * + * @param callable $kernel The last item to run as middleware + * + * @throws RuntimeException if the stack is seeded more than once + */ + protected function seedMiddlewareStack(callable $kernel = null) + { + if (!is_null($this->tip)) { + throw new RuntimeException('MiddlewareStack can only be seeded once.'); + } + if ($kernel === null) { + $kernel = $this; + } + $this->tip = $kernel; + } + + /** + * Call middleware stack + * + * @param ServerRequestInterface $request A request object + * @param ResponseInterface $response A response object + * + * @return ResponseInterface + */ + public function callMiddlewareStack(ServerRequestInterface $request, ResponseInterface $response) + { + if (is_null($this->tip)) { + $this->seedMiddlewareStack(); + } + /** @var callable $start */ + $start = $this->tip; + $this->middlewareLock = true; + $response = $start($request, $response); + $this->middlewareLock = false; + return $response; + } +} diff --git a/vendor/slim/slim/Slim/Routable.php b/vendor/slim/slim/Slim/Routable.php new file mode 100644 index 0000000..2563e05 --- /dev/null +++ b/vendor/slim/slim/Slim/Routable.php @@ -0,0 +1,109 @@ +pattern = $pattern; + $this->callable = $callable; + } + + /** + * Get the middleware registered for the group + * + * @return callable[] + */ + public function getMiddleware() + { + return $this->middleware; + } + + /** + * Get the route pattern + * + * @return string + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * Set container for use with resolveCallable + * + * @param ContainerInterface $container + * + * @return static + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + return $this; + } + + /** + * Prepend middleware to the middleware collection + * + * @param callable|string $callable The callback routine + * + * @return static + */ + public function add($callable) + { + $this->middleware[] = new DeferredCallable($callable, $this->container); + return $this; + } + + /** + * Set the route pattern + * + * @param string $newPattern + */ + public function setPattern($newPattern) + { + $this->pattern = $newPattern; + } +} diff --git a/vendor/slim/slim/Slim/Route.php b/vendor/slim/slim/Slim/Route.php new file mode 100644 index 0000000..7af218c --- /dev/null +++ b/vendor/slim/slim/Slim/Route.php @@ -0,0 +1,295 @@ +methods = is_string($methods) ? [$methods] : $methods; + $this->groups = $groups; + $this->identifier = 'route' . $identifier; + } + + public function finalize() + { + if ($this->finalized) { + return; + } + + $groupMiddleware = []; + foreach ($this->getGroups() as $group) { + $groupMiddleware = array_merge($group->getMiddleware(), $groupMiddleware); + } + + $this->middleware = array_merge($this->middleware, $groupMiddleware); + + foreach ($this->getMiddleware() as $middleware) { + $this->addMiddleware($middleware); + } + + $this->finalized = true; + } + + /** + * Get route callable + * + * @return callable + */ + public function getCallable() + { + return $this->callable; + } + + /** + * This method enables you to override the Route's callable + * + * @param string|Closure $callable + */ + public function setCallable($callable) + { + $this->callable = $callable; + } + + /** + * Get route methods + * + * @return string[] + */ + public function getMethods() + { + return $this->methods; + } + + /** + * Get parent route groups + * + * @return RouteGroup[] + */ + public function getGroups() + { + return $this->groups; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * Get route identifier + * + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Get output buffering mode + * + * @return boolean|string + */ + public function getOutputBuffering() + { + return $this->outputBuffering; + } + + /** + * {@inheritdoc} + */ + public function setOutputBuffering($mode) + { + if (!in_array($mode, [false, 'prepend', 'append'], true)) { + throw new InvalidArgumentException('Unknown output buffering mode'); + } + $this->outputBuffering = $mode; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setName($name) + { + if (!is_string($name)) { + throw new InvalidArgumentException('Route name must be a string'); + } + $this->name = $name; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setArgument($name, $value, $includeInSavedArguments = true) + { + if ($includeInSavedArguments) { + $this->savedArguments[$name] = $value; + } + $this->arguments[$name] = $value; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments, $includeInSavedArguments = true) + { + if ($includeInSavedArguments) { + $this->savedArguments = $arguments; + } + $this->arguments = $arguments; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * {@inheritdoc} + */ + public function getArgument($name, $default = null) + { + if (array_key_exists($name, $this->arguments)) { + return $this->arguments[$name]; + } + return $default; + } + + /** + * {@inheritdoc} + */ + public function prepare(ServerRequestInterface $request, array $arguments) + { + // Remove temp arguments + $this->setArguments($this->savedArguments); + + // Add the route arguments + foreach ($arguments as $k => $v) { + $this->setArgument($k, $v, false); + } + } + + /** + * {@inheritdoc} + */ + public function run(ServerRequestInterface $request, ResponseInterface $response) + { + // Finalise route now that we are about to run it + $this->finalize(); + + // Traverse middleware stack and fetch updated response + return $this->callMiddlewareStack($request, $response); + } + + /** + * {@inheritdoc} + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response) + { + $this->callable = $this->resolveCallable($this->callable); + + /** @var InvocationStrategyInterface $handler */ + $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse(); + + $newResponse = $handler($this->callable, $request, $response, $this->arguments); + + if ($newResponse instanceof ResponseInterface) { + // if route callback returns a ResponseInterface, then use it + $response = $newResponse; + } elseif (is_string($newResponse)) { + // if route callback returns a string, then append it to the response + if ($response->getBody()->isWritable()) { + $response->getBody()->write($newResponse); + } + } + + return $response; + } +} diff --git a/vendor/slim/slim/Slim/RouteGroup.php b/vendor/slim/slim/Slim/RouteGroup.php new file mode 100644 index 0000000..26fe5c0 --- /dev/null +++ b/vendor/slim/slim/Slim/RouteGroup.php @@ -0,0 +1,27 @@ +resolveCallable($this->callable); + if ($callable instanceof Closure && $app !== null) { + $callable = $callable->bindTo($app); + } + + $callable($app); + } +} diff --git a/vendor/slim/slim/Slim/Router.php b/vendor/slim/slim/Slim/Router.php new file mode 100644 index 0000000..08a9501 --- /dev/null +++ b/vendor/slim/slim/Slim/Router.php @@ -0,0 +1,449 @@ +routeParser = $parser ?: new StdParser; + } + + /** + * Set the base path used in pathFor() + * + * @param string $basePath + * + * @return static + * @throws InvalidArgumentException + */ + public function setBasePath($basePath) + { + if (!is_string($basePath)) { + throw new InvalidArgumentException('Router basePath must be a string'); + } + + $this->basePath = $basePath; + + return $this; + } + + /** + * Get the base path used in pathFor() + * + * @return string + */ + public function getBasePath() + { + return $this->basePath; + } + + /** + * Set path to fast route cache file. If this is false then route caching is disabled. + * + * @param string|false $cacheFile + * + * @return static + * + * @throws InvalidArgumentException If cacheFile is not a string or not false + * @throws RuntimeException If cacheFile directory is not writable + */ + public function setCacheFile($cacheFile) + { + if (!is_string($cacheFile) && $cacheFile !== false) { + throw new InvalidArgumentException('Router cache file must be a string or false'); + } + + if ($cacheFile && file_exists($cacheFile) && !is_readable($cacheFile)) { + throw new RuntimeException( + sprintf('Router cache file `%s` is not readable', $cacheFile) + ); + } + + if ($cacheFile && !file_exists($cacheFile) && !is_writable(dirname($cacheFile))) { + throw new RuntimeException( + sprintf('Router cache file directory `%s` is not writable', dirname($cacheFile)) + ); + } + + $this->cacheFile = $cacheFile; + return $this; + } + + /** + * @param ContainerInterface $container + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function map($methods, $pattern, $handler) + { + if (!is_string($pattern)) { + throw new InvalidArgumentException('Route pattern must be a string'); + } + + // Prepend parent group pattern(s) + if ($this->routeGroups) { + $pattern = $this->processGroups() . $pattern; + } + + // According to RFC methods are defined in uppercase (See RFC 7231) + $methods = array_map("strtoupper", $methods); + + /** @var Route $route */ + $route = $this->createRoute($methods, $pattern, $handler); + + $this->routes[$route->getIdentifier()] = $route; + $this->routeCounter++; + + return $route; + } + + /** + * {@inheritdoc} + */ + public function dispatch(ServerRequestInterface $request) + { + $uri = '/' . ltrim($request->getUri()->getPath(), '/'); + + return $this->createDispatcher()->dispatch( + $request->getMethod(), + $uri + ); + } + + /** + * Create a new Route object + * + * @param string[] $methods Array of HTTP methods + * @param string $pattern The route pattern + * @param callable $callable The route callable + * + * @return RouteInterface + */ + protected function createRoute($methods, $pattern, $callable) + { + $route = new Route($methods, $pattern, $callable, $this->routeGroups, $this->routeCounter); + if (!empty($this->container)) { + $route->setContainer($this->container); + } + + return $route; + } + + /** + * @return Dispatcher + */ + protected function createDispatcher() + { + if ($this->dispatcher) { + return $this->dispatcher; + } + + $routeDefinitionCallback = function (RouteCollector $r) { + foreach ($this->getRoutes() as $route) { + $r->addRoute($route->getMethods(), $route->getPattern(), $route->getIdentifier()); + } + }; + + if ($this->cacheFile) { + $this->dispatcher = \FastRoute\cachedDispatcher($routeDefinitionCallback, [ + 'routeParser' => $this->routeParser, + 'cacheFile' => $this->cacheFile, + ]); + } else { + $this->dispatcher = \FastRoute\simpleDispatcher($routeDefinitionCallback, [ + 'routeParser' => $this->routeParser, + ]); + } + + return $this->dispatcher; + } + + /** + * @param Dispatcher $dispatcher + */ + public function setDispatcher(Dispatcher $dispatcher) + { + $this->dispatcher = $dispatcher; + } + + /** + * Get route objects + * + * @return Route[] + */ + public function getRoutes() + { + return $this->routes; + } + + /** + * {@inheritdoc} + */ + public function getNamedRoute($name) + { + foreach ($this->routes as $route) { + if ($name == $route->getName()) { + return $route; + } + } + throw new RuntimeException('Named route does not exist for name: ' . $name); + } + + /** + * Remove named route + * + * @param string $name Route name + * + * @throws RuntimeException If named route does not exist + */ + public function removeNamedRoute($name) + { + $route = $this->getNamedRoute($name); + + // no exception, route exists, now remove by id + unset($this->routes[$route->getIdentifier()]); + } + + /** + * Process route groups + * + * @return string A group pattern to prefix routes with + */ + protected function processGroups() + { + $pattern = ""; + foreach ($this->routeGroups as $group) { + $pattern .= $group->getPattern(); + } + return $pattern; + } + + /** + * {@inheritdoc} + */ + public function pushGroup($pattern, $callable) + { + $group = new RouteGroup($pattern, $callable); + array_push($this->routeGroups, $group); + return $group; + } + + /** + * {@inheritdoc} + */ + public function popGroup() + { + $group = array_pop($this->routeGroups); + return $group instanceof RouteGroup ? $group : false; + } + + /** + * {@inheritdoc} + */ + public function lookupRoute($identifier) + { + if (!isset($this->routes[$identifier])) { + throw new RuntimeException('Route not found, looks like your route cache is stale.'); + } + return $this->routes[$identifier]; + } + + /** + * {@inheritdoc} + */ + public function relativePathFor($name, array $data = [], array $queryParams = []) + { + $route = $this->getNamedRoute($name); + $pattern = $route->getPattern(); + + $routeDatas = $this->routeParser->parse($pattern); + // $routeDatas is an array of all possible routes that can be made. There is + // one routedata for each optional parameter plus one for no optional parameters. + // + // The most specific is last, so we look for that first. + $routeDatas = array_reverse($routeDatas); + + $segments = []; + $segmentName = ''; + foreach ($routeDatas as $routeData) { + foreach ($routeData as $item) { + if (is_string($item)) { + // this segment is a static string + $segments[] = $item; + continue; + } + + // This segment has a parameter: first element is the name + if (!array_key_exists($item[0], $data)) { + // we don't have a data element for this segment: cancel + // testing this routeData item, so that we can try a less + // specific routeData item. + $segments = []; + $segmentName = $item[0]; + break; + } + $segments[] = $data[$item[0]]; + } + if (!empty($segments)) { + // we found all the parameters for this route data, no need to check + // less specific ones + break; + } + } + + if (empty($segments)) { + throw new InvalidArgumentException('Missing data for URL segment: ' . $segmentName); + } + $url = implode('', $segments); + + $hasQueryParams = array_filter($queryParams, function ($value) { + return $value !== null; + }) !== []; + + if ($hasQueryParams) { + $url .= '?' . http_build_query($queryParams); + } + + return $url; + } + + /** + * {@inheritdoc} + */ + public function pathFor($name, array $data = [], array $queryParams = []) + { + return $this->urlFor($name, $data, $queryParams); + } + + /** + * Build the path for a named route including the base path + * + * @param string $name Route name + * @param array $data Named argument replacement data + * @param array $queryParams Optional query string parameters + * + * @return string + * + * @throws RuntimeException If named route does not exist + * @throws InvalidArgumentException If required data not provided + */ + public function urlFor($name, array $data = [], array $queryParams = []) + { + $url = $this->relativePathFor($name, $data, $queryParams); + + if ($this->basePath) { + $url = $this->basePath . $url; + } + + return $url; + } + + /** + * Get fully qualified URL for named route + * + * @param UriInterface $uri + * @param string $routeName + * @param array $data Named argument replacement data + * @param array $queryParams Optional query string parameters + * + * @return string + * + * @throws RuntimeException If named route does not exist + * @throws InvalidArgumentException If required data not provided + */ + public function fullUrlFor(UriInterface $uri, $routeName, array $data = [], array $queryParams = []) + { + $path = $this->urlFor($routeName, $data, $queryParams); + $scheme = $uri->getScheme(); + $authority = $uri->getAuthority(); + $protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : ''); + + return $protocol . $path; + } +} diff --git a/vendor/slim/slim/composer.json b/vendor/slim/slim/composer.json new file mode 100644 index 0000000..7be1584 --- /dev/null +++ b/vendor/slim/slim/composer.json @@ -0,0 +1,63 @@ +{ + "name": "slim/slim", + "type": "library", + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "keywords": ["framework","micro","api","router"], + "homepage": "https://slimframework.com", + "license": "MIT", + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "require": { + "php": ">=5.5.0", + "ext-json": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "pimple/pimple": "^3.0", + "psr/http-message": "^1.0", + "nikic/fast-route": "^1.0", + "psr/container": "^1.0" + }, + "require-dev": { + "squizlabs/php_codesniffer": "^2.5", + "phpunit/phpunit": "^4.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "autoload-dev": { + "psr-4": { + "Slim\\Tests\\": "tests" + } + }, + "scripts": { + "test": [ + "phpunit", + "phpcs" + ] + } +} diff --git a/vendor/slim/slim/phpstan.neon.dist b/vendor/slim/slim/phpstan.neon.dist new file mode 100644 index 0000000..2fc56e7 --- /dev/null +++ b/vendor/slim/slim/phpstan.neon.dist @@ -0,0 +1,2 @@ +parameters: + level: 1 diff --git a/vendor/symfony/config/Builder/ClassBuilder.php b/vendor/symfony/config/Builder/ClassBuilder.php new file mode 100644 index 0000000..26fcab4 --- /dev/null +++ b/vendor/symfony/config/Builder/ClassBuilder.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +/** + * Build PHP classes to generate config. + * + * @internal + * + * @author Tobias Nyholm + */ +class ClassBuilder +{ + /** @var string */ + private $namespace; + + /** @var string */ + private $name; + + /** @var Property[] */ + private $properties = []; + + /** @var Method[] */ + private $methods = []; + private $require = []; + private $use = []; + private $implements = []; + private $allowExtraKeys = false; + + public function __construct(string $namespace, string $name) + { + $this->namespace = $namespace; + $this->name = ucfirst($this->camelCase($name)).'Config'; + } + + public function getDirectory(): string + { + return str_replace('\\', \DIRECTORY_SEPARATOR, $this->namespace); + } + + public function getFilename(): string + { + return $this->name.'.php'; + } + + public function build(): string + { + $rootPath = explode(\DIRECTORY_SEPARATOR, $this->getDirectory()); + $require = ''; + foreach ($this->require as $class) { + // figure out relative path. + $path = explode(\DIRECTORY_SEPARATOR, $class->getDirectory()); + $path[] = $class->getFilename(); + foreach ($rootPath as $key => $value) { + if ($path[$key] !== $value) { + break; + } + unset($path[$key]); + } + $require .= sprintf('require_once __DIR__.\DIRECTORY_SEPARATOR.\'%s\';', implode('\'.\DIRECTORY_SEPARATOR.\'', $path))."\n"; + } + $use = ''; + foreach (array_keys($this->use) as $statement) { + $use .= sprintf('use %s;', $statement)."\n"; + } + + $implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements); + $body = ''; + foreach ($this->properties as $property) { + $body .= ' '.$property->getContent()."\n"; + } + foreach ($this->methods as $method) { + $lines = explode("\n", $method->getContent()); + foreach ($lines as $line) { + $body .= ' '.$line."\n"; + } + } + + $content = strtr(' $this->namespace, 'REQUIRE' => $require, 'USE' => $use, 'CLASS' => $this->getName(), 'IMPLEMENTS' => $implements, 'BODY' => $body]); + + return $content; + } + + public function addRequire(self $class): void + { + $this->require[] = $class; + } + + public function addUse(string $class): void + { + $this->use[$class] = true; + } + + public function addImplements(string $interface): void + { + $this->implements[] = '\\'.ltrim($interface, '\\'); + } + + public function addMethod(string $name, string $body, array $params = []): void + { + $this->methods[] = new Method(strtr($body, ['NAME' => $this->camelCase($name)] + $params)); + } + + public function addProperty(string $name, string $classType = null): Property + { + $property = new Property($name, '_' !== $name[0] ? $this->camelCase($name) : $name); + if (null !== $classType) { + $property->setType($classType); + } + $this->properties[] = $property; + $property->setContent(sprintf('private $%s;', $property->getName())); + + return $property; + } + + public function getProperties(): array + { + return $this->properties; + } + + private function camelCase(string $input): string + { + $output = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + + return preg_replace('#\W#', '', $output); + } + + public function getName(): string + { + return $this->name; + } + + public function getNamespace(): string + { + return $this->namespace; + } + + public function getFqcn(): string + { + return '\\'.$this->namespace.'\\'.$this->name; + } + + public function setAllowExtraKeys(bool $allowExtraKeys): void + { + $this->allowExtraKeys = $allowExtraKeys; + } + + public function shouldAllowExtraKeys(): bool + { + return $this->allowExtraKeys; + } +} diff --git a/vendor/symfony/config/Builder/ConfigBuilderGenerator.php b/vendor/symfony/config/Builder/ConfigBuilderGenerator.php new file mode 100644 index 0000000..979c955 --- /dev/null +++ b/vendor/symfony/config/Builder/ConfigBuilderGenerator.php @@ -0,0 +1,458 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\BooleanNode; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\EnumNode; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\FloatNode; +use Symfony\Component\Config\Definition\IntegerNode; +use Symfony\Component\Config\Definition\NodeInterface; +use Symfony\Component\Config\Definition\PrototypedArrayNode; +use Symfony\Component\Config\Definition\ScalarNode; +use Symfony\Component\Config\Definition\VariableNode; +use Symfony\Component\Config\Loader\ParamConfigurator; + +/** + * Generate ConfigBuilders to help create valid config. + * + * @author Tobias Nyholm + */ +class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface +{ + private $classes; + private $outputDir; + + public function __construct(string $outputDir) + { + $this->outputDir = $outputDir; + } + + /** + * @return \Closure that will return the root config class + */ + public function build(ConfigurationInterface $configuration): \Closure + { + $this->classes = []; + + $rootNode = $configuration->getConfigTreeBuilder()->buildTree(); + $rootClass = new ClassBuilder('Symfony\\Config', $rootNode->getName()); + + $path = $this->getFullPath($rootClass); + if (!is_file($path)) { + // Generate the class if the file not exists + $this->classes[] = $rootClass; + $this->buildNode($rootNode, $rootClass, $this->getSubNamespace($rootClass)); + $rootClass->addImplements(ConfigBuilderInterface::class); + $rootClass->addMethod('getExtensionAlias', ' +public function NAME(): string +{ + return \'ALIAS\'; +}', ['ALIAS' => $rootNode->getPath()]); + + $this->writeClasses(); + } + + $loader = \Closure::fromCallable(function () use ($path, $rootClass) { + require_once $path; + $className = $rootClass->getFqcn(); + + return new $className(); + }); + + return $loader; + } + + private function getFullPath(ClassBuilder $class): string + { + $directory = $this->outputDir.\DIRECTORY_SEPARATOR.$class->getDirectory(); + if (!is_dir($directory)) { + @mkdir($directory, 0777, true); + } + + return $directory.\DIRECTORY_SEPARATOR.$class->getFilename(); + } + + private function writeClasses(): void + { + foreach ($this->classes as $class) { + $this->buildConstructor($class); + $this->buildToArray($class); + $this->buildSetExtraKey($class); + + file_put_contents($this->getFullPath($class), $class->build()); + } + + $this->classes = []; + } + + private function buildNode(NodeInterface $node, ClassBuilder $class, string $namespace): void + { + if (!$node instanceof ArrayNode) { + throw new \LogicException('The node was expected to be an ArrayNode. This Configuration includes an edge case not supported yet.'); + } + + foreach ($node->getChildren() as $child) { + switch (true) { + case $child instanceof ScalarNode: + $this->handleScalarNode($child, $class); + break; + case $child instanceof PrototypedArrayNode: + $this->handlePrototypedArrayNode($child, $class, $namespace); + break; + case $child instanceof VariableNode: + $this->handleVariableNode($child, $class); + break; + case $child instanceof ArrayNode: + $this->handleArrayNode($child, $class, $namespace); + break; + default: + throw new \RuntimeException(sprintf('Unknown node "%s".', \get_class($child))); + } + } + } + + private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $namespace): void + { + $childClass = new ClassBuilder($namespace, $node->getName()); + $childClass->setAllowExtraKeys($node->shouldIgnoreExtraKeys()); + $class->addRequire($childClass); + $this->classes[] = $childClass; + + $property = $class->addProperty($node->getName(), $childClass->getFqcn()); + $body = ' +public function NAME(array $value = []): CLASS +{ + if (null === $this->PROPERTY) { + $this->PROPERTY = new CLASS($value); + } elseif ([] !== $value) { + throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\'); + } + + return $this->PROPERTY; +}'; + $class->addUse(InvalidConfigurationException::class); + $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn()]); + + $this->buildNode($node, $childClass, $this->getSubNamespace($childClass)); + } + + private function handleVariableNode(VariableNode $node, ClassBuilder $class): void + { + $comment = $this->getComment($node); + $property = $class->addProperty($node->getName()); + $class->addUse(ParamConfigurator::class); + + $body = ' +/** +COMMENT * @return $this + */ +public function NAME($valueDEFAULT): self +{ + $this->PROPERTY = $value; + + return $this; +}'; + $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment, 'DEFAULT' => $node->hasDefaultValue() ? ' = '.var_export($node->getDefaultValue(), true) : '']); + } + + private function handlePrototypedArrayNode(PrototypedArrayNode $node, ClassBuilder $class, string $namespace): void + { + $name = $this->getSingularName($node); + $prototype = $node->getPrototype(); + $methodName = $name; + + $parameterType = $this->getParameterType($prototype); + if (null !== $parameterType || $prototype instanceof ScalarNode) { + $class->addUse(ParamConfigurator::class); + $property = $class->addProperty($node->getName()); + if (null === $key = $node->getKeyAttribute()) { + // This is an array of values; don't use singular name + $body = ' +/** + * @param ParamConfigurator|list $value + * @return $this + */ +public function NAME($value): self +{ + $this->PROPERTY = $value; + + return $this; +}'; + + $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'TYPE' => '' === $parameterType ? 'mixed' : $parameterType]); + } else { + $body = ' +/** + * @param ParamConfigurator|TYPE $value + * @return $this + */ +public function NAME(string $VAR, $VALUE): self +{ + $this->PROPERTY[$VAR] = $VALUE; + + return $this; +}'; + + $class->addMethod($methodName, $body, ['PROPERTY' => $property->getName(), 'TYPE' => '' === $parameterType ? 'mixed' : $parameterType, 'VAR' => '' === $key ? 'key' : $key, 'VALUE' => 'value' === $key ? 'data' : 'value']); + } + + return; + } + + $childClass = new ClassBuilder($namespace, $name); + if ($prototype instanceof ArrayNode) { + $childClass->setAllowExtraKeys($prototype->shouldIgnoreExtraKeys()); + } + $class->addRequire($childClass); + $this->classes[] = $childClass; + $property = $class->addProperty($node->getName(), $childClass->getFqcn().'[]'); + + if (null === $key = $node->getKeyAttribute()) { + $body = ' +public function NAME(array $value = []): CLASS +{ + return $this->PROPERTY[] = new CLASS($value); +}'; + $class->addMethod($methodName, $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn()]); + } else { + $body = ' +public function NAME(string $VAR, array $VALUE = []): CLASS +{ + if (!isset($this->PROPERTY[$VAR])) { + return $this->PROPERTY[$VAR] = new CLASS($value); + } + if ([] === $value) { + return $this->PROPERTY[$VAR]; + } + + throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\'); +}'; + $class->addUse(InvalidConfigurationException::class); + $class->addMethod($methodName, $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn(), 'VAR' => '' === $key ? 'key' : $key, 'VALUE' => 'value' === $key ? 'data' : 'value']); + } + + $this->buildNode($prototype, $childClass, $namespace.'\\'.$childClass->getName()); + } + + private function handleScalarNode(ScalarNode $node, ClassBuilder $class): void + { + $comment = $this->getComment($node); + $property = $class->addProperty($node->getName()); + $class->addUse(ParamConfigurator::class); + + $body = ' +/** +COMMENT * @return $this + */ +public function NAME($value): self +{ + $this->PROPERTY = $value; + + return $this; +}'; + + $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment]); + } + + private function getParameterType(NodeInterface $node): ?string + { + if ($node instanceof BooleanNode) { + return 'bool'; + } + + if ($node instanceof IntegerNode) { + return 'int'; + } + + if ($node instanceof FloatNode) { + return 'float'; + } + + if ($node instanceof EnumNode) { + return ''; + } + + if ($node instanceof PrototypedArrayNode && $node->getPrototype() instanceof ScalarNode) { + // This is just an array of variables + return 'array'; + } + + if ($node instanceof VariableNode) { + // mixed + return ''; + } + + return null; + } + + private function getComment(VariableNode $node): string + { + $comment = ''; + if ('' !== $info = (string) $node->getInfo()) { + $comment .= ' * '.$info."\n"; + } + + foreach ((array) ($node->getExample() ?? []) as $example) { + $comment .= ' * @example '.$example."\n"; + } + + if ('' !== $default = $node->getDefaultValue()) { + $comment .= ' * @default '.(null === $default ? 'null' : var_export($default, true))."\n"; + } + + if ($node instanceof EnumNode) { + $comment .= sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_map(function ($a) { + return var_export($a, true); + }, $node->getValues())))."\n"; + } else { + $parameterType = $this->getParameterType($node); + if (null === $parameterType || '' === $parameterType) { + $parameterType = 'mixed'; + } + $comment .= ' * @param ParamConfigurator|'.$parameterType.' $value'."\n"; + } + + if ($node->isDeprecated()) { + $comment .= ' * @deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n"; + } + + return $comment; + } + + /** + * Pick a good singular name. + */ + private function getSingularName(PrototypedArrayNode $node): string + { + $name = $node->getName(); + if ('s' !== substr($name, -1)) { + return $name; + } + + $parent = $node->getParent(); + $mappings = $parent instanceof ArrayNode ? $parent->getXmlRemappings() : []; + foreach ($mappings as $map) { + if ($map[1] === $name) { + $name = $map[0]; + break; + } + } + + return $name; + } + + private function buildToArray(ClassBuilder $class): void + { + $body = '$output = [];'; + foreach ($class->getProperties() as $p) { + $code = '$this->PROPERTY'; + if (null !== $p->getType()) { + if ($p->isArray()) { + $code = 'array_map(function ($v) { return $v->toArray(); }, $this->PROPERTY)'; + } else { + $code = '$this->PROPERTY->toArray()'; + } + } + + $body .= strtr(' + if (null !== $this->PROPERTY) { + $output[\'ORG_NAME\'] = '.$code.'; + }', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName()]); + } + + $extraKeys = $class->shouldAllowExtraKeys() ? ' + $this->_extraKeys' : ''; + + $class->addMethod('toArray', ' +public function NAME(): array +{ + '.$body.' + + return $output'.$extraKeys.'; +}'); + } + + private function buildConstructor(ClassBuilder $class): void + { + $body = ''; + foreach ($class->getProperties() as $p) { + $code = '$value[\'ORG_NAME\']'; + if (null !== $p->getType()) { + if ($p->isArray()) { + $code = 'array_map(function ($v) { return new '.$p->getType().'($v); }, $value[\'ORG_NAME\'])'; + } else { + $code = 'new '.$p->getType().'($value[\'ORG_NAME\'])'; + } + } + + $body .= strtr(' + if (isset($value[\'ORG_NAME\'])) { + $this->PROPERTY = '.$code.'; + unset($value[\'ORG_NAME\']); + } +', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName()]); + } + + if ($class->shouldAllowExtraKeys()) { + $body .= ' + $this->_extraKeys = $value; +'; + } else { + $body .= ' + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__).implode(\', \', array_keys($value))); + }'; + + $class->addUse(InvalidConfigurationException::class); + } + + $class->addMethod('__construct', ' +public function __construct(array $value = []) +{ +'.$body.' +}'); + } + + private function buildSetExtraKey(ClassBuilder $class): void + { + if (!$class->shouldAllowExtraKeys()) { + return; + } + + $class->addUse(ParamConfigurator::class); + + $class->addProperty('_extraKeys'); + + $class->addMethod('set', ' +/** + * @param ParamConfigurator|mixed $value + * @return $this + */ +public function NAME(string $key, $value): self +{ + if (null === $value) { + unset($this->_extraKeys[$key]); + } else { + $this->_extraKeys[$key] = $value; + } + + return $this; +}'); + } + + private function getSubNamespace(ClassBuilder $rootClass): string + { + return sprintf('%s\\%s', $rootClass->getNamespace(), substr($rootClass->getName(), 0, -6)); + } +} diff --git a/vendor/symfony/config/Builder/ConfigBuilderGeneratorInterface.php b/vendor/symfony/config/Builder/ConfigBuilderGeneratorInterface.php new file mode 100644 index 0000000..c52c9e5 --- /dev/null +++ b/vendor/symfony/config/Builder/ConfigBuilderGeneratorInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * Generates ConfigBuilders to help create valid config. + * + * @author Tobias Nyholm + */ +interface ConfigBuilderGeneratorInterface +{ + /** + * @return \Closure that will return the root config class + */ + public function build(ConfigurationInterface $configuration): \Closure; +} diff --git a/vendor/symfony/config/Builder/ConfigBuilderInterface.php b/vendor/symfony/config/Builder/ConfigBuilderInterface.php new file mode 100644 index 0000000..fd3129c --- /dev/null +++ b/vendor/symfony/config/Builder/ConfigBuilderInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +/** + * A ConfigBuilder provides helper methods to build a large complex array. + * + * @author Tobias Nyholm + */ +interface ConfigBuilderInterface +{ + /** + * Gets all configuration represented as an array. + */ + public function toArray(): array; + + /** + * Gets the alias for the extension which config we are building. + */ + public function getExtensionAlias(): string; +} diff --git a/vendor/symfony/config/Builder/Method.php b/vendor/symfony/config/Builder/Method.php new file mode 100644 index 0000000..3577e3d --- /dev/null +++ b/vendor/symfony/config/Builder/Method.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +/** + * Represents a method when building classes. + * + * @internal + * + * @author Tobias Nyholm + */ +class Method +{ + private $content; + + public function __construct(string $content) + { + $this->content = $content; + } + + public function getContent(): string + { + return $this->content; + } +} diff --git a/vendor/symfony/config/Builder/Property.php b/vendor/symfony/config/Builder/Property.php new file mode 100644 index 0000000..1b24c47 --- /dev/null +++ b/vendor/symfony/config/Builder/Property.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +/** + * Represents a property when building classes. + * + * @internal + * + * @author Tobias Nyholm + */ +class Property +{ + private $name; + private $originalName; + private $array = false; + private $type = null; + private $content; + + public function __construct(string $originalName, string $name) + { + $this->name = $name; + $this->originalName = $originalName; + } + + public function getName(): string + { + return $this->name; + } + + public function getOriginalName(): string + { + return $this->originalName; + } + + public function setType(string $type): void + { + $this->array = false; + $this->type = $type; + + if ('[]' === substr($type, -2)) { + $this->array = true; + $this->type = substr($type, 0, -2); + } + } + + public function getType(): ?string + { + return $this->type; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(string $content): void + { + $this->content = $content; + } + + public function isArray(): bool + { + return $this->array; + } +} diff --git a/vendor/symfony/config/CHANGELOG.md b/vendor/symfony/config/CHANGELOG.md new file mode 100644 index 0000000..75ef8bc --- /dev/null +++ b/vendor/symfony/config/CHANGELOG.md @@ -0,0 +1,129 @@ +CHANGELOG +========= + +5.3.0 +----- + + * Add support for generating `ConfigBuilder` for extensions + +5.1.0 +----- + + * updated the signature of method `NodeDefinition::setDeprecated()` to `NodeDefinition::setDeprecation(string $package, string $version, string $message)` + * updated the signature of method `BaseNode::setDeprecated()` to `BaseNode::setDeprecation(string $package, string $version, string $message)` + * deprecated passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node + * deprecated `BaseNode::getDeprecationMessage()`, use `BaseNode::getDeprecation()` instead + +5.0.0 +----- + + * Dropped support for constructing a `TreeBuilder` without passing root node information. + * Removed the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead + * Added method `getChildNodeDefinitions()` to ParentNodeDefinitionInterface + * Removed `FileLoaderLoadException`, use `LoaderLoadException` instead + +4.4.0 +----- + + * added a way to exclude patterns of resources from being imported by the `import()` method + +4.3.0 +----- + + * deprecated using environment variables with `cannotBeEmpty()` if the value is validated with `validate()` + * made `Resource\*` classes final and not implement `Serializable` anymore + * deprecated the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead + +4.2.0 +----- + + * deprecated constructing a `TreeBuilder` without passing root node information + * renamed `FileLoaderLoadException` to `LoaderLoadException` + +4.1.0 +----- + + * added `setPathSeparator` method to `NodeBuilder` class + * added third `$pathSeparator` constructor argument to `BaseNode` + * the `Processor` class has been made final + +4.0.0 +----- + + * removed `ConfigCachePass` + +3.4.0 +----- + + * added `setDeprecated()` method to indicate a deprecated node + * added `XmlUtils::parse()` method to parse an XML string + * deprecated `ConfigCachePass` + +3.3.0 +----- + + * added `ReflectionClassResource` class + * added second `$exists` constructor argument to `ClassExistenceResource` + * made `ClassExistenceResource` work with interfaces and traits + * added `ConfigCachePass` (originally in FrameworkBundle) + * added `castToArray()` helper to turn any config value into an array + +3.0.0 +----- + + * removed `ReferenceDumper` class + * removed the `ResourceInterface::isFresh()` method + * removed `BCResourceInterfaceChecker` class + * removed `ResourceInterface::getResource()` method + +2.8.0 +----- + +The edge case of defining just one value for nodes of type Enum is now allowed: + +```php +$rootNode + ->children() + ->enumNode('variable') + ->values(['value']) + ->end() + ->end() +; +``` + +Before: `InvalidArgumentException` (variable must contain at least two +distinct elements). +After: the code will work as expected and it will restrict the values of the +`variable` option to just `value`. + + * deprecated the `ResourceInterface::isFresh()` method. If you implement custom resource types and they + can be validated that way, make them implement the new `SelfCheckingResourceInterface`. + * deprecated the getResource() method in ResourceInterface. You can still call this method + on concrete classes implementing the interface, but it does not make sense at the interface + level as you need to know about the particular type of resource at hand to understand the + semantics of the returned value. + +2.7.0 +----- + + * added `ConfigCacheInterface`, `ConfigCacheFactoryInterface` and a basic `ConfigCacheFactory` + implementation to delegate creation of ConfigCache instances + +2.2.0 +----- + + * added `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()` + to ease configuration when some sections are respectively disabled / enabled + by default. + * added a `normalizeKeys()` method for array nodes (to avoid key normalization) + * added numerical type handling for config definitions + * added convenience methods for optional configuration sections to `ArrayNodeDefinition` + * added a utils class for XML manipulations + +2.1.0 +----- + + * added a way to add documentation on configuration + * implemented `Serializable` on resources + * `LoaderResolverInterface` is now used instead of `LoaderResolver` for type + hinting diff --git a/vendor/symfony/config/ConfigCache.php b/vendor/symfony/config/ConfigCache.php new file mode 100644 index 0000000..3b09052 --- /dev/null +++ b/vendor/symfony/config/ConfigCache.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +use Symfony\Component\Config\Resource\SelfCheckingResourceChecker; + +/** + * ConfigCache caches arbitrary content in files on disk. + * + * When in debug mode, those metadata resources that implement + * \Symfony\Component\Config\Resource\SelfCheckingResourceInterface will + * be used to check cache freshness. + * + * @author Fabien Potencier + * @author Matthias Pigulla + */ +class ConfigCache extends ResourceCheckerConfigCache +{ + private $debug; + + /** + * @param string $file The absolute cache path + * @param bool $debug Whether debugging is enabled or not + */ + public function __construct(string $file, bool $debug) + { + $this->debug = $debug; + + $checkers = []; + if (true === $this->debug) { + $checkers = [new SelfCheckingResourceChecker()]; + } + + parent::__construct($file, $checkers); + } + + /** + * Checks if the cache is still fresh. + * + * This implementation always returns true when debug is off and the + * cache file exists. + * + * @return bool + */ + public function isFresh() + { + if (!$this->debug && is_file($this->getPath())) { + return true; + } + + return parent::isFresh(); + } +} diff --git a/vendor/symfony/config/ConfigCacheFactory.php b/vendor/symfony/config/ConfigCacheFactory.php new file mode 100644 index 0000000..11fd3cb --- /dev/null +++ b/vendor/symfony/config/ConfigCacheFactory.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +/** + * Basic implementation of ConfigCacheFactoryInterface that + * creates an instance of the default ConfigCache. + * + * This factory and/or cache do not support cache validation + * by means of ResourceChecker instances (that is, service-based). + * + * @author Matthias Pigulla + */ +class ConfigCacheFactory implements ConfigCacheFactoryInterface +{ + private $debug; + + /** + * @param bool $debug The debug flag to pass to ConfigCache + */ + public function __construct(bool $debug) + { + $this->debug = $debug; + } + + /** + * {@inheritdoc} + */ + public function cache(string $file, callable $callback) + { + $cache = new ConfigCache($file, $this->debug); + if (!$cache->isFresh()) { + $callback($cache); + } + + return $cache; + } +} diff --git a/vendor/symfony/config/ConfigCacheFactoryInterface.php b/vendor/symfony/config/ConfigCacheFactoryInterface.php new file mode 100644 index 0000000..146ee9b --- /dev/null +++ b/vendor/symfony/config/ConfigCacheFactoryInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +/** + * Interface for a ConfigCache factory. This factory creates + * an instance of ConfigCacheInterface and initializes the + * cache if necessary. + * + * @author Matthias Pigulla + */ +interface ConfigCacheFactoryInterface +{ + /** + * Creates a cache instance and (re-)initializes it if necessary. + * + * @param string $file The absolute cache file path + * @param callable $callable The callable to be executed when the cache needs to be filled (i. e. is not fresh). The cache will be passed as the only parameter to this callback + * + * @return ConfigCacheInterface + */ + public function cache(string $file, callable $callable); +} diff --git a/vendor/symfony/config/ConfigCacheInterface.php b/vendor/symfony/config/ConfigCacheInterface.php new file mode 100644 index 0000000..3cd7a5c --- /dev/null +++ b/vendor/symfony/config/ConfigCacheInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +use Symfony\Component\Config\Resource\ResourceInterface; + +/** + * Interface for ConfigCache. + * + * @author Matthias Pigulla + */ +interface ConfigCacheInterface +{ + /** + * Gets the cache file path. + * + * @return string + */ + public function getPath(); + + /** + * Checks if the cache is still fresh. + * + * This check should take the metadata passed to the write() method into consideration. + * + * @return bool + */ + public function isFresh(); + + /** + * Writes the given content into the cache file. Metadata will be stored + * independently and can be used to check cache freshness at a later time. + * + * @param string $content The content to write into the cache + * @param ResourceInterface[]|null $metadata An array of ResourceInterface instances + * + * @throws \RuntimeException When the cache file cannot be written + */ + public function write(string $content, array $metadata = null); +} diff --git a/vendor/symfony/config/Definition/ArrayNode.php b/vendor/symfony/config/Definition/ArrayNode.php new file mode 100644 index 0000000..bd0eae9 --- /dev/null +++ b/vendor/symfony/config/Definition/ArrayNode.php @@ -0,0 +1,404 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; +use Symfony\Component\Config\Definition\Exception\UnsetKeyException; + +/** + * Represents an Array node in the config tree. + * + * @author Johannes M. Schmitt + */ +class ArrayNode extends BaseNode implements PrototypeNodeInterface +{ + protected $xmlRemappings = []; + protected $children = []; + protected $allowFalse = false; + protected $allowNewKeys = true; + protected $addIfNotSet = false; + protected $performDeepMerging = true; + protected $ignoreExtraKeys = false; + protected $removeExtraKeys = true; + protected $normalizeKeys = true; + + public function setNormalizeKeys(bool $normalizeKeys) + { + $this->normalizeKeys = $normalizeKeys; + } + + /** + * {@inheritdoc} + * + * Namely, you mostly have foo_bar in YAML while you have foo-bar in XML. + * After running this method, all keys are normalized to foo_bar. + * + * If you have a mixed key like foo-bar_moo, it will not be altered. + * The key will also not be altered if the target key already exists. + */ + protected function preNormalize($value) + { + if (!$this->normalizeKeys || !\is_array($value)) { + return $value; + } + + $normalized = []; + + foreach ($value as $k => $v) { + if (str_contains($k, '-') && !str_contains($k, '_') && !\array_key_exists($normalizedKey = str_replace('-', '_', $k), $value)) { + $normalized[$normalizedKey] = $v; + } else { + $normalized[$k] = $v; + } + } + + return $normalized; + } + + /** + * Retrieves the children of this node. + * + * @return array + */ + public function getChildren() + { + return $this->children; + } + + /** + * Sets the xml remappings that should be performed. + * + * @param array $remappings An array of the form [[string, string]] + */ + public function setXmlRemappings(array $remappings) + { + $this->xmlRemappings = $remappings; + } + + /** + * Gets the xml remappings that should be performed. + * + * @return array an array of the form [[string, string]] + */ + public function getXmlRemappings() + { + return $this->xmlRemappings; + } + + /** + * Sets whether to add default values for this array if it has not been + * defined in any of the configuration files. + */ + public function setAddIfNotSet(bool $boolean) + { + $this->addIfNotSet = $boolean; + } + + /** + * Sets whether false is allowed as value indicating that the array should be unset. + */ + public function setAllowFalse(bool $allow) + { + $this->allowFalse = $allow; + } + + /** + * Sets whether new keys can be defined in subsequent configurations. + */ + public function setAllowNewKeys(bool $allow) + { + $this->allowNewKeys = $allow; + } + + /** + * Sets if deep merging should occur. + */ + public function setPerformDeepMerging(bool $boolean) + { + $this->performDeepMerging = $boolean; + } + + /** + * Whether extra keys should just be ignored without an exception. + * + * @param bool $boolean To allow extra keys + * @param bool $remove To remove extra keys + */ + public function setIgnoreExtraKeys(bool $boolean, bool $remove = true) + { + $this->ignoreExtraKeys = $boolean; + $this->removeExtraKeys = $this->ignoreExtraKeys && $remove; + } + + /** + * Returns true when extra keys should be ignored without an exception. + */ + public function shouldIgnoreExtraKeys(): bool + { + return $this->ignoreExtraKeys; + } + + /** + * {@inheritdoc} + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function hasDefaultValue() + { + return $this->addIfNotSet; + } + + /** + * {@inheritdoc} + */ + public function getDefaultValue() + { + if (!$this->hasDefaultValue()) { + throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath())); + } + + $defaults = []; + foreach ($this->children as $name => $child) { + if ($child->hasDefaultValue()) { + $defaults[$name] = $child->getDefaultValue(); + } + } + + return $defaults; + } + + /** + * Adds a child node. + * + * @throws \InvalidArgumentException when the child node has no name + * @throws \InvalidArgumentException when the child node's name is not unique + */ + public function addChild(NodeInterface $node) + { + $name = $node->getName(); + if ('' === $name) { + throw new \InvalidArgumentException('Child nodes must be named.'); + } + if (isset($this->children[$name])) { + throw new \InvalidArgumentException(sprintf('A child node named "%s" already exists.', $name)); + } + + $this->children[$name] = $node; + } + + /** + * {@inheritdoc} + * + * @throws UnsetKeyException + * @throws InvalidConfigurationException if the node doesn't have enough children + */ + protected function finalizeValue($value) + { + if (false === $value) { + throw new UnsetKeyException(sprintf('Unsetting key for path "%s", value: %s.', $this->getPath(), json_encode($value))); + } + + foreach ($this->children as $name => $child) { + if (!\array_key_exists($name, $value)) { + if ($child->isRequired()) { + $message = sprintf('The child config "%s" under "%s" must be configured', $name, $this->getPath()); + if ($child->getInfo()) { + $message .= sprintf(': %s', $child->getInfo()); + } else { + $message .= '.'; + } + $ex = new InvalidConfigurationException($message); + $ex->setPath($this->getPath()); + + throw $ex; + } + + if ($child->hasDefaultValue()) { + $value[$name] = $child->getDefaultValue(); + } + + continue; + } + + if ($child->isDeprecated()) { + $deprecation = $child->getDeprecation($name, $this->getPath()); + trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']); + } + + try { + $value[$name] = $child->finalize($value[$name]); + } catch (UnsetKeyException $e) { + unset($value[$name]); + } + } + + return $value; + } + + /** + * {@inheritdoc} + */ + protected function validateType($value) + { + if (!\is_array($value) && (!$this->allowFalse || false !== $value)) { + $ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected "array", but got "%s"', $this->getPath(), get_debug_type($value))); + if ($hint = $this->getInfo()) { + $ex->addHint($hint); + } + $ex->setPath($this->getPath()); + + throw $ex; + } + } + + /** + * {@inheritdoc} + * + * @throws InvalidConfigurationException + */ + protected function normalizeValue($value) + { + if (false === $value) { + return $value; + } + + $value = $this->remapXml($value); + + $normalized = []; + foreach ($value as $name => $val) { + if (isset($this->children[$name])) { + try { + $normalized[$name] = $this->children[$name]->normalize($val); + } catch (UnsetKeyException $e) { + } + unset($value[$name]); + } elseif (!$this->removeExtraKeys) { + $normalized[$name] = $val; + } + } + + // if extra fields are present, throw exception + if (\count($value) && !$this->ignoreExtraKeys) { + $proposals = array_keys($this->children); + sort($proposals); + $guesses = []; + + foreach (array_keys($value) as $subject) { + $minScore = \INF; + foreach ($proposals as $proposal) { + $distance = levenshtein($subject, $proposal); + if ($distance <= $minScore && $distance < 3) { + $guesses[$proposal] = $distance; + $minScore = $distance; + } + } + } + + $msg = sprintf('Unrecognized option%s "%s" under "%s"', 1 === \count($value) ? '' : 's', implode(', ', array_keys($value)), $this->getPath()); + + if (\count($guesses)) { + asort($guesses); + $msg .= sprintf('. Did you mean "%s"?', implode('", "', array_keys($guesses))); + } else { + $msg .= sprintf('. Available option%s %s "%s".', 1 === \count($proposals) ? '' : 's', 1 === \count($proposals) ? 'is' : 'are', implode('", "', $proposals)); + } + + $ex = new InvalidConfigurationException($msg); + $ex->setPath($this->getPath()); + + throw $ex; + } + + return $normalized; + } + + /** + * Remaps multiple singular values to a single plural value. + * + * @return array + */ + protected function remapXml(array $value) + { + foreach ($this->xmlRemappings as [$singular, $plural]) { + if (!isset($value[$singular])) { + continue; + } + + $value[$plural] = Processor::normalizeConfig($value, $singular, $plural); + unset($value[$singular]); + } + + return $value; + } + + /** + * {@inheritdoc} + * + * @throws InvalidConfigurationException + * @throws \RuntimeException + */ + protected function mergeValues($leftSide, $rightSide) + { + if (false === $rightSide) { + // if this is still false after the last config has been merged the + // finalization pass will take care of removing this key entirely + return false; + } + + if (false === $leftSide || !$this->performDeepMerging) { + return $rightSide; + } + + foreach ($rightSide as $k => $v) { + // no conflict + if (!\array_key_exists($k, $leftSide)) { + if (!$this->allowNewKeys) { + $ex = new InvalidConfigurationException(sprintf('You are not allowed to define new elements for path "%s". Please define all elements for this path in one config file. If you are trying to overwrite an element, make sure you redefine it with the same name.', $this->getPath())); + $ex->setPath($this->getPath()); + + throw $ex; + } + + $leftSide[$k] = $v; + continue; + } + + if (!isset($this->children[$k])) { + if (!$this->ignoreExtraKeys || $this->removeExtraKeys) { + throw new \RuntimeException('merge() expects a normalized config array.'); + } + + $leftSide[$k] = $v; + continue; + } + + $leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v); + } + + return $leftSide; + } + + /** + * {@inheritdoc} + */ + protected function allowPlaceholders(): bool + { + return false; + } +} diff --git a/vendor/symfony/config/Definition/BaseNode.php b/vendor/symfony/config/Definition/BaseNode.php new file mode 100644 index 0000000..5ca1239 --- /dev/null +++ b/vendor/symfony/config/Definition/BaseNode.php @@ -0,0 +1,589 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\Exception; +use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; +use Symfony\Component\Config\Definition\Exception\UnsetKeyException; + +/** + * The base node class. + * + * @author Johannes M. Schmitt + */ +abstract class BaseNode implements NodeInterface +{ + public const DEFAULT_PATH_SEPARATOR = '.'; + + private static $placeholderUniquePrefixes = []; + private static $placeholders = []; + + protected $name; + protected $parent; + protected $normalizationClosures = []; + protected $finalValidationClosures = []; + protected $allowOverwrite = true; + protected $required = false; + protected $deprecation = []; + protected $equivalentValues = []; + protected $attributes = []; + protected $pathSeparator; + + private $handlingPlaceholder; + + /** + * @throws \InvalidArgumentException if the name contains a period + */ + public function __construct(?string $name, NodeInterface $parent = null, string $pathSeparator = self::DEFAULT_PATH_SEPARATOR) + { + if (str_contains($name = (string) $name, $pathSeparator)) { + throw new \InvalidArgumentException('The name must not contain ".'.$pathSeparator.'".'); + } + + $this->name = $name; + $this->parent = $parent; + $this->pathSeparator = $pathSeparator; + } + + /** + * Register possible (dummy) values for a dynamic placeholder value. + * + * Matching configuration values will be processed with a provided value, one by one. After a provided value is + * successfully processed the configuration value is returned as is, thus preserving the placeholder. + * + * @internal + */ + public static function setPlaceholder(string $placeholder, array $values): void + { + if (!$values) { + throw new \InvalidArgumentException('At least one value must be provided.'); + } + + self::$placeholders[$placeholder] = $values; + } + + /** + * Adds a common prefix for dynamic placeholder values. + * + * Matching configuration values will be skipped from being processed and are returned as is, thus preserving the + * placeholder. An exact match provided by {@see setPlaceholder()} might take precedence. + * + * @internal + */ + public static function setPlaceholderUniquePrefix(string $prefix): void + { + self::$placeholderUniquePrefixes[] = $prefix; + } + + /** + * Resets all current placeholders available. + * + * @internal + */ + public static function resetPlaceholders(): void + { + self::$placeholderUniquePrefixes = []; + self::$placeholders = []; + } + + public function setAttribute(string $key, $value) + { + $this->attributes[$key] = $value; + } + + /** + * @return mixed + */ + public function getAttribute(string $key, $default = null) + { + return $this->attributes[$key] ?? $default; + } + + /** + * @return bool + */ + public function hasAttribute(string $key) + { + return isset($this->attributes[$key]); + } + + /** + * @return array + */ + public function getAttributes() + { + return $this->attributes; + } + + public function setAttributes(array $attributes) + { + $this->attributes = $attributes; + } + + public function removeAttribute(string $key) + { + unset($this->attributes[$key]); + } + + /** + * Sets an info message. + */ + public function setInfo(string $info) + { + $this->setAttribute('info', $info); + } + + /** + * Returns info message. + * + * @return string|null + */ + public function getInfo() + { + return $this->getAttribute('info'); + } + + /** + * Sets the example configuration for this node. + * + * @param string|array $example + */ + public function setExample($example) + { + $this->setAttribute('example', $example); + } + + /** + * Retrieves the example configuration for this node. + * + * @return string|array|null + */ + public function getExample() + { + return $this->getAttribute('example'); + } + + /** + * Adds an equivalent value. + * + * @param mixed $originalValue + * @param mixed $equivalentValue + */ + public function addEquivalentValue($originalValue, $equivalentValue) + { + $this->equivalentValues[] = [$originalValue, $equivalentValue]; + } + + /** + * Set this node as required. + */ + public function setRequired(bool $boolean) + { + $this->required = $boolean; + } + + /** + * Sets this node as deprecated. + * + * @param string $package The name of the composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message the deprecation message to use + * + * You can use %node% and %path% placeholders in your message to display, + * respectively, the node name and its complete path + */ + public function setDeprecated(?string $package/*, string $version, string $message = 'The child node "%node%" at path "%path%" is deprecated.' */) + { + $args = \func_get_args(); + + if (\func_num_args() < 2) { + trigger_deprecation('symfony/config', '5.1', 'The signature of method "%s()" requires 3 arguments: "string $package, string $version, string $message", not defining them is deprecated.', __METHOD__); + + if (!isset($args[0])) { + trigger_deprecation('symfony/config', '5.1', 'Passing a null message to un-deprecate a node is deprecated.'); + + $this->deprecation = []; + + return; + } + + $message = (string) $args[0]; + $package = $version = ''; + } else { + $package = (string) $args[0]; + $version = (string) $args[1]; + $message = (string) ($args[2] ?? 'The child node "%node%" at path "%path%" is deprecated.'); + } + + $this->deprecation = [ + 'package' => $package, + 'version' => $version, + 'message' => $message, + ]; + } + + /** + * Sets if this node can be overridden. + */ + public function setAllowOverwrite(bool $allow) + { + $this->allowOverwrite = $allow; + } + + /** + * Sets the closures used for normalization. + * + * @param \Closure[] $closures An array of Closures used for normalization + */ + public function setNormalizationClosures(array $closures) + { + $this->normalizationClosures = $closures; + } + + /** + * Sets the closures used for final validation. + * + * @param \Closure[] $closures An array of Closures used for final validation + */ + public function setFinalValidationClosures(array $closures) + { + $this->finalValidationClosures = $closures; + } + + /** + * {@inheritdoc} + */ + public function isRequired() + { + return $this->required; + } + + /** + * Checks if this node is deprecated. + * + * @return bool + */ + public function isDeprecated() + { + return (bool) $this->deprecation; + } + + /** + * Returns the deprecated message. + * + * @param string $node the configuration node name + * @param string $path the path of the node + * + * @return string + * + * @deprecated since Symfony 5.1, use "getDeprecation()" instead. + */ + public function getDeprecationMessage(string $node, string $path) + { + trigger_deprecation('symfony/config', '5.1', 'The "%s()" method is deprecated, use "getDeprecation()" instead.', __METHOD__); + + return $this->getDeprecation($node, $path)['message']; + } + + /** + * @param string $node The configuration node name + * @param string $path The path of the node + */ + public function getDeprecation(string $node, string $path): array + { + return [ + 'package' => $this->deprecation['package'] ?? '', + 'version' => $this->deprecation['version'] ?? '', + 'message' => strtr($this->deprecation['message'] ?? '', ['%node%' => $node, '%path%' => $path]), + ]; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getPath() + { + if (null !== $this->parent) { + return $this->parent->getPath().$this->pathSeparator.$this->name; + } + + return $this->name; + } + + /** + * {@inheritdoc} + */ + final public function merge($leftSide, $rightSide) + { + if (!$this->allowOverwrite) { + throw new ForbiddenOverwriteException(sprintf('Configuration path "%s" cannot be overwritten. You have to define all options for this path, and any of its sub-paths in one configuration section.', $this->getPath())); + } + + if ($leftSide !== $leftPlaceholders = self::resolvePlaceholderValue($leftSide)) { + foreach ($leftPlaceholders as $leftPlaceholder) { + $this->handlingPlaceholder = $leftSide; + try { + $this->merge($leftPlaceholder, $rightSide); + } finally { + $this->handlingPlaceholder = null; + } + } + + return $rightSide; + } + + if ($rightSide !== $rightPlaceholders = self::resolvePlaceholderValue($rightSide)) { + foreach ($rightPlaceholders as $rightPlaceholder) { + $this->handlingPlaceholder = $rightSide; + try { + $this->merge($leftSide, $rightPlaceholder); + } finally { + $this->handlingPlaceholder = null; + } + } + + return $rightSide; + } + + $this->doValidateType($leftSide); + $this->doValidateType($rightSide); + + return $this->mergeValues($leftSide, $rightSide); + } + + /** + * {@inheritdoc} + */ + final public function normalize($value) + { + $value = $this->preNormalize($value); + + // run custom normalization closures + foreach ($this->normalizationClosures as $closure) { + $value = $closure($value); + } + + // resolve placeholder value + if ($value !== $placeholders = self::resolvePlaceholderValue($value)) { + foreach ($placeholders as $placeholder) { + $this->handlingPlaceholder = $value; + try { + $this->normalize($placeholder); + } finally { + $this->handlingPlaceholder = null; + } + } + + return $value; + } + + // replace value with their equivalent + foreach ($this->equivalentValues as $data) { + if ($data[0] === $value) { + $value = $data[1]; + } + } + + // validate type + $this->doValidateType($value); + + // normalize value + return $this->normalizeValue($value); + } + + /** + * Normalizes the value before any other normalization is applied. + * + * @param mixed $value + * + * @return mixed + */ + protected function preNormalize($value) + { + return $value; + } + + /** + * Returns parent node for this node. + * + * @return NodeInterface|null + */ + public function getParent() + { + return $this->parent; + } + + /** + * {@inheritdoc} + */ + final public function finalize($value) + { + if ($value !== $placeholders = self::resolvePlaceholderValue($value)) { + foreach ($placeholders as $placeholder) { + $this->handlingPlaceholder = $value; + try { + $this->finalize($placeholder); + } finally { + $this->handlingPlaceholder = null; + } + } + + return $value; + } + + $this->doValidateType($value); + + $value = $this->finalizeValue($value); + + // Perform validation on the final value if a closure has been set. + // The closure is also allowed to return another value. + foreach ($this->finalValidationClosures as $closure) { + try { + $value = $closure($value); + } catch (Exception $e) { + if ($e instanceof UnsetKeyException && null !== $this->handlingPlaceholder) { + continue; + } + + throw $e; + } catch (\Exception $e) { + throw new InvalidConfigurationException(sprintf('Invalid configuration for path "%s": ', $this->getPath()).$e->getMessage(), $e->getCode(), $e); + } + } + + return $value; + } + + /** + * Validates the type of a Node. + * + * @param mixed $value The value to validate + * + * @throws InvalidTypeException when the value is invalid + */ + abstract protected function validateType($value); + + /** + * Normalizes the value. + * + * @param mixed $value The value to normalize + * + * @return mixed + */ + abstract protected function normalizeValue($value); + + /** + * Merges two values together. + * + * @param mixed $leftSide + * @param mixed $rightSide + * + * @return mixed + */ + abstract protected function mergeValues($leftSide, $rightSide); + + /** + * Finalizes a value. + * + * @param mixed $value The value to finalize + * + * @return mixed + */ + abstract protected function finalizeValue($value); + + /** + * Tests if placeholder values are allowed for this node. + */ + protected function allowPlaceholders(): bool + { + return true; + } + + /** + * Tests if a placeholder is being handled currently. + */ + protected function isHandlingPlaceholder(): bool + { + return null !== $this->handlingPlaceholder; + } + + /** + * Gets allowed dynamic types for this node. + */ + protected function getValidPlaceholderTypes(): array + { + return []; + } + + private static function resolvePlaceholderValue($value) + { + if (\is_string($value)) { + if (isset(self::$placeholders[$value])) { + return self::$placeholders[$value]; + } + + foreach (self::$placeholderUniquePrefixes as $placeholderUniquePrefix) { + if (str_starts_with($value, $placeholderUniquePrefix)) { + return []; + } + } + } + + return $value; + } + + private function doValidateType($value): void + { + if (null !== $this->handlingPlaceholder && !$this->allowPlaceholders()) { + $e = new InvalidTypeException(sprintf('A dynamic value is not compatible with a "%s" node type at path "%s".', static::class, $this->getPath())); + $e->setPath($this->getPath()); + + throw $e; + } + + if (null === $this->handlingPlaceholder || null === $value) { + $this->validateType($value); + + return; + } + + $knownTypes = array_keys(self::$placeholders[$this->handlingPlaceholder]); + $validTypes = $this->getValidPlaceholderTypes(); + + if ($validTypes && array_diff($knownTypes, $validTypes)) { + $e = new InvalidTypeException(sprintf( + 'Invalid type for path "%s". Expected %s, but got %s.', + $this->getPath(), + 1 === \count($validTypes) ? '"'.reset($validTypes).'"' : 'one of "'.implode('", "', $validTypes).'"', + 1 === \count($knownTypes) ? '"'.reset($knownTypes).'"' : 'one of "'.implode('", "', $knownTypes).'"' + )); + if ($hint = $this->getInfo()) { + $e->addHint($hint); + } + $e->setPath($this->getPath()); + + throw $e; + } + + $this->validateType($value); + } +} diff --git a/vendor/symfony/config/Definition/BooleanNode.php b/vendor/symfony/config/Definition/BooleanNode.php new file mode 100644 index 0000000..c64ecb8 --- /dev/null +++ b/vendor/symfony/config/Definition/BooleanNode.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; + +/** + * This node represents a Boolean value in the config tree. + * + * @author Johannes M. Schmitt + */ +class BooleanNode extends ScalarNode +{ + /** + * {@inheritdoc} + */ + protected function validateType($value) + { + if (!\is_bool($value)) { + $ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected "bool", but got "%s".', $this->getPath(), get_debug_type($value))); + if ($hint = $this->getInfo()) { + $ex->addHint($hint); + } + $ex->setPath($this->getPath()); + + throw $ex; + } + } + + /** + * {@inheritdoc} + */ + protected function isValueEmpty($value) + { + // a boolean value cannot be empty + return false; + } + + /** + * {@inheritdoc} + */ + protected function getValidPlaceholderTypes(): array + { + return ['bool']; + } +} diff --git a/vendor/symfony/config/Definition/Builder/ArrayNodeDefinition.php b/vendor/symfony/config/Definition/Builder/ArrayNodeDefinition.php new file mode 100644 index 0000000..eb5b040 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/ArrayNodeDefinition.php @@ -0,0 +1,549 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; +use Symfony\Component\Config\Definition\PrototypedArrayNode; + +/** + * This class provides a fluent interface for defining an array node. + * + * @author Johannes M. Schmitt + */ +class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinitionInterface +{ + protected $performDeepMerging = true; + protected $ignoreExtraKeys = false; + protected $removeExtraKeys = true; + protected $children = []; + protected $prototype; + protected $atLeastOne = false; + protected $allowNewKeys = true; + protected $key; + protected $removeKeyItem; + protected $addDefaults = false; + protected $addDefaultChildren = false; + protected $nodeBuilder; + protected $normalizeKeys = true; + + /** + * {@inheritdoc} + */ + public function __construct(?string $name, NodeParentInterface $parent = null) + { + parent::__construct($name, $parent); + + $this->nullEquivalent = []; + $this->trueEquivalent = []; + } + + /** + * {@inheritdoc} + */ + public function setBuilder(NodeBuilder $builder) + { + $this->nodeBuilder = $builder; + } + + /** + * {@inheritdoc} + */ + public function children() + { + return $this->getNodeBuilder(); + } + + /** + * Sets a prototype for child nodes. + * + * @return NodeDefinition + */ + public function prototype(string $type) + { + return $this->prototype = $this->getNodeBuilder()->node(null, $type)->setParent($this); + } + + /** + * @return VariableNodeDefinition + */ + public function variablePrototype() + { + return $this->prototype('variable'); + } + + /** + * @return ScalarNodeDefinition + */ + public function scalarPrototype() + { + return $this->prototype('scalar'); + } + + /** + * @return BooleanNodeDefinition + */ + public function booleanPrototype() + { + return $this->prototype('boolean'); + } + + /** + * @return IntegerNodeDefinition + */ + public function integerPrototype() + { + return $this->prototype('integer'); + } + + /** + * @return FloatNodeDefinition + */ + public function floatPrototype() + { + return $this->prototype('float'); + } + + /** + * @return ArrayNodeDefinition + */ + public function arrayPrototype() + { + return $this->prototype('array'); + } + + /** + * @return EnumNodeDefinition + */ + public function enumPrototype() + { + return $this->prototype('enum'); + } + + /** + * Adds the default value if the node is not set in the configuration. + * + * This method is applicable to concrete nodes only (not to prototype nodes). + * If this function has been called and the node is not set during the finalization + * phase, it's default value will be derived from its children default values. + * + * @return $this + */ + public function addDefaultsIfNotSet() + { + $this->addDefaults = true; + + return $this; + } + + /** + * Adds children with a default value when none are defined. + * + * This method is applicable to prototype nodes only. + * + * @param int|string|array|null $children The number of children|The child name|The children names to be added + * + * @return $this + */ + public function addDefaultChildrenIfNoneSet($children = null) + { + $this->addDefaultChildren = $children; + + return $this; + } + + /** + * Requires the node to have at least one element. + * + * This method is applicable to prototype nodes only. + * + * @return $this + */ + public function requiresAtLeastOneElement() + { + $this->atLeastOne = true; + + return $this; + } + + /** + * Disallows adding news keys in a subsequent configuration. + * + * If used all keys have to be defined in the same configuration file. + * + * @return $this + */ + public function disallowNewKeysInSubsequentConfigs() + { + $this->allowNewKeys = false; + + return $this; + } + + /** + * Sets a normalization rule for XML configurations. + * + * @param string $singular The key to remap + * @param string|null $plural The plural of the key for irregular plurals + * + * @return $this + */ + public function fixXmlConfig(string $singular, string $plural = null) + { + $this->normalization()->remap($singular, $plural); + + return $this; + } + + /** + * Sets the attribute which value is to be used as key. + * + * This is useful when you have an indexed array that should be an + * associative array. You can select an item from within the array + * to be the key of the particular item. For example, if "id" is the + * "key", then: + * + * [ + * ['id' => 'my_name', 'foo' => 'bar'], + * ]; + * + * becomes + * + * [ + * 'my_name' => ['foo' => 'bar'], + * ]; + * + * If you'd like "'id' => 'my_name'" to still be present in the resulting + * array, then you can set the second argument of this method to false. + * + * This method is applicable to prototype nodes only. + * + * @param string $name The name of the key + * @param bool $removeKeyItem Whether or not the key item should be removed + * + * @return $this + */ + public function useAttributeAsKey(string $name, bool $removeKeyItem = true) + { + $this->key = $name; + $this->removeKeyItem = $removeKeyItem; + + return $this; + } + + /** + * Sets whether the node can be unset. + * + * @return $this + */ + public function canBeUnset(bool $allow = true) + { + $this->merge()->allowUnset($allow); + + return $this; + } + + /** + * Adds an "enabled" boolean to enable the current section. + * + * By default, the section is disabled. If any configuration is specified then + * the node will be automatically enabled: + * + * enableableArrayNode: {enabled: true, ...} # The config is enabled & default values get overridden + * enableableArrayNode: ~ # The config is enabled & use the default values + * enableableArrayNode: true # The config is enabled & use the default values + * enableableArrayNode: {other: value, ...} # The config is enabled & default values get overridden + * enableableArrayNode: {enabled: false, ...} # The config is disabled + * enableableArrayNode: false # The config is disabled + * + * @return $this + */ + public function canBeEnabled() + { + $this + ->addDefaultsIfNotSet() + ->treatFalseLike(['enabled' => false]) + ->treatTrueLike(['enabled' => true]) + ->treatNullLike(['enabled' => true]) + ->beforeNormalization() + ->ifArray() + ->then(function (array $v) { + $v['enabled'] = $v['enabled'] ?? true; + + return $v; + }) + ->end() + ->children() + ->booleanNode('enabled') + ->defaultFalse() + ; + + return $this; + } + + /** + * Adds an "enabled" boolean to enable the current section. + * + * By default, the section is enabled. + * + * @return $this + */ + public function canBeDisabled() + { + $this + ->addDefaultsIfNotSet() + ->treatFalseLike(['enabled' => false]) + ->treatTrueLike(['enabled' => true]) + ->treatNullLike(['enabled' => true]) + ->children() + ->booleanNode('enabled') + ->defaultTrue() + ; + + return $this; + } + + /** + * Disables the deep merging of the node. + * + * @return $this + */ + public function performNoDeepMerging() + { + $this->performDeepMerging = false; + + return $this; + } + + /** + * Allows extra config keys to be specified under an array without + * throwing an exception. + * + * Those config values are ignored and removed from the resulting + * array. This should be used only in special cases where you want + * to send an entire configuration array through a special tree that + * processes only part of the array. + * + * @param bool $remove Whether to remove the extra keys + * + * @return $this + */ + public function ignoreExtraKeys(bool $remove = true) + { + $this->ignoreExtraKeys = true; + $this->removeExtraKeys = $remove; + + return $this; + } + + /** + * Sets whether to enable key normalization. + * + * @return $this + */ + public function normalizeKeys(bool $bool) + { + $this->normalizeKeys = $bool; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function append(NodeDefinition $node) + { + $this->children[$node->name] = $node->setParent($this); + + return $this; + } + + /** + * Returns a node builder to be used to add children and prototype. + * + * @return NodeBuilder + */ + protected function getNodeBuilder() + { + if (null === $this->nodeBuilder) { + $this->nodeBuilder = new NodeBuilder(); + } + + return $this->nodeBuilder->setParent($this); + } + + /** + * {@inheritdoc} + */ + protected function createNode() + { + if (null === $this->prototype) { + $node = new ArrayNode($this->name, $this->parent, $this->pathSeparator); + + $this->validateConcreteNode($node); + + $node->setAddIfNotSet($this->addDefaults); + + foreach ($this->children as $child) { + $child->parent = $node; + $node->addChild($child->getNode()); + } + } else { + $node = new PrototypedArrayNode($this->name, $this->parent, $this->pathSeparator); + + $this->validatePrototypeNode($node); + + if (null !== $this->key) { + $node->setKeyAttribute($this->key, $this->removeKeyItem); + } + + if (true === $this->atLeastOne || false === $this->allowEmptyValue) { + $node->setMinNumberOfElements(1); + } + + if ($this->default) { + if (!\is_array($this->defaultValue)) { + throw new \InvalidArgumentException(sprintf('%s: the default value of an array node has to be an array.', $node->getPath())); + } + + $node->setDefaultValue($this->defaultValue); + } + + if (false !== $this->addDefaultChildren) { + $node->setAddChildrenIfNoneSet($this->addDefaultChildren); + if ($this->prototype instanceof static && null === $this->prototype->prototype) { + $this->prototype->addDefaultsIfNotSet(); + } + } + + $this->prototype->parent = $node; + $node->setPrototype($this->prototype->getNode()); + } + + $node->setAllowNewKeys($this->allowNewKeys); + $node->addEquivalentValue(null, $this->nullEquivalent); + $node->addEquivalentValue(true, $this->trueEquivalent); + $node->addEquivalentValue(false, $this->falseEquivalent); + $node->setPerformDeepMerging($this->performDeepMerging); + $node->setRequired($this->required); + $node->setIgnoreExtraKeys($this->ignoreExtraKeys, $this->removeExtraKeys); + $node->setNormalizeKeys($this->normalizeKeys); + + if ($this->deprecation) { + $node->setDeprecated($this->deprecation['package'], $this->deprecation['version'], $this->deprecation['message']); + } + + if (null !== $this->normalization) { + $node->setNormalizationClosures($this->normalization->before); + $node->setXmlRemappings($this->normalization->remappings); + } + + if (null !== $this->merge) { + $node->setAllowOverwrite($this->merge->allowOverwrite); + $node->setAllowFalse($this->merge->allowFalse); + } + + if (null !== $this->validation) { + $node->setFinalValidationClosures($this->validation->rules); + } + + return $node; + } + + /** + * Validate the configuration of a concrete node. + * + * @throws InvalidDefinitionException + */ + protected function validateConcreteNode(ArrayNode $node) + { + $path = $node->getPath(); + + if (null !== $this->key) { + throw new InvalidDefinitionException(sprintf('->useAttributeAsKey() is not applicable to concrete nodes at path "%s".', $path)); + } + + if (false === $this->allowEmptyValue) { + throw new InvalidDefinitionException(sprintf('->cannotBeEmpty() is not applicable to concrete nodes at path "%s".', $path)); + } + + if (true === $this->atLeastOne) { + throw new InvalidDefinitionException(sprintf('->requiresAtLeastOneElement() is not applicable to concrete nodes at path "%s".', $path)); + } + + if ($this->default) { + throw new InvalidDefinitionException(sprintf('->defaultValue() is not applicable to concrete nodes at path "%s".', $path)); + } + + if (false !== $this->addDefaultChildren) { + throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() is not applicable to concrete nodes at path "%s".', $path)); + } + } + + /** + * Validate the configuration of a prototype node. + * + * @throws InvalidDefinitionException + */ + protected function validatePrototypeNode(PrototypedArrayNode $node) + { + $path = $node->getPath(); + + if ($this->addDefaults) { + throw new InvalidDefinitionException(sprintf('->addDefaultsIfNotSet() is not applicable to prototype nodes at path "%s".', $path)); + } + + if (false !== $this->addDefaultChildren) { + if ($this->default) { + throw new InvalidDefinitionException(sprintf('A default value and default children might not be used together at path "%s".', $path)); + } + + if (null !== $this->key && (null === $this->addDefaultChildren || \is_int($this->addDefaultChildren) && $this->addDefaultChildren > 0)) { + throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() should set default children names as ->useAttributeAsKey() is used at path "%s".', $path)); + } + + if (null === $this->key && (\is_string($this->addDefaultChildren) || \is_array($this->addDefaultChildren))) { + throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() might not set default children names as ->useAttributeAsKey() is not used at path "%s".', $path)); + } + } + } + + /** + * @return NodeDefinition[] + */ + public function getChildNodeDefinitions() + { + return $this->children; + } + + /** + * Finds a node defined by the given $nodePath. + * + * @param string $nodePath The path of the node to find. e.g "doctrine.orm.mappings" + */ + public function find(string $nodePath): NodeDefinition + { + $firstPathSegment = (false === $pathSeparatorPos = strpos($nodePath, $this->pathSeparator)) + ? $nodePath + : substr($nodePath, 0, $pathSeparatorPos); + + if (null === $node = ($this->children[$firstPathSegment] ?? null)) { + throw new \RuntimeException(sprintf('Node with name "%s" does not exist in the current node "%s".', $firstPathSegment, $this->name)); + } + + if (false === $pathSeparatorPos) { + return $node; + } + + return $node->find(substr($nodePath, $pathSeparatorPos + \strlen($this->pathSeparator))); + } +} diff --git a/vendor/symfony/config/Definition/Builder/BooleanNodeDefinition.php b/vendor/symfony/config/Definition/Builder/BooleanNodeDefinition.php new file mode 100644 index 0000000..ace0b34 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/BooleanNodeDefinition.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\BooleanNode; +use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; + +/** + * This class provides a fluent interface for defining a node. + * + * @author Johannes M. Schmitt + */ +class BooleanNodeDefinition extends ScalarNodeDefinition +{ + /** + * {@inheritdoc} + */ + public function __construct(?string $name, NodeParentInterface $parent = null) + { + parent::__construct($name, $parent); + + $this->nullEquivalent = true; + } + + /** + * Instantiate a Node. + * + * @return BooleanNode + */ + protected function instantiateNode() + { + return new BooleanNode($this->name, $this->parent, $this->pathSeparator); + } + + /** + * {@inheritdoc} + * + * @throws InvalidDefinitionException + */ + public function cannotBeEmpty() + { + throw new InvalidDefinitionException('->cannotBeEmpty() is not applicable to BooleanNodeDefinition.'); + } +} diff --git a/vendor/symfony/config/Definition/Builder/BuilderAwareInterface.php b/vendor/symfony/config/Definition/Builder/BuilderAwareInterface.php new file mode 100644 index 0000000..f30b873 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/BuilderAwareInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +/** + * An interface that can be implemented by nodes which build other nodes. + * + * @author Roland Franssen + */ +interface BuilderAwareInterface +{ + /** + * Sets a custom children builder. + */ + public function setBuilder(NodeBuilder $builder); +} diff --git a/vendor/symfony/config/Definition/Builder/EnumNodeDefinition.php b/vendor/symfony/config/Definition/Builder/EnumNodeDefinition.php new file mode 100644 index 0000000..52e2fd1 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/EnumNodeDefinition.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\EnumNode; + +/** + * Enum Node Definition. + * + * @author Johannes M. Schmitt + */ +class EnumNodeDefinition extends ScalarNodeDefinition +{ + private $values; + + /** + * @return $this + */ + public function values(array $values) + { + $values = array_unique($values); + + if (empty($values)) { + throw new \InvalidArgumentException('->values() must be called with at least one value.'); + } + + $this->values = $values; + + return $this; + } + + /** + * Instantiate a Node. + * + * @return EnumNode + * + * @throws \RuntimeException + */ + protected function instantiateNode() + { + if (null === $this->values) { + throw new \RuntimeException('You must call ->values() on enum nodes.'); + } + + return new EnumNode($this->name, $this->parent, $this->values, $this->pathSeparator); + } +} diff --git a/vendor/symfony/config/Definition/Builder/ExprBuilder.php b/vendor/symfony/config/Definition/Builder/ExprBuilder.php new file mode 100644 index 0000000..f0b7a59 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/ExprBuilder.php @@ -0,0 +1,246 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\Exception\UnsetKeyException; + +/** + * This class builds an if expression. + * + * @author Johannes M. Schmitt + * @author Christophe Coevoet + */ +class ExprBuilder +{ + protected $node; + public $ifPart; + public $thenPart; + + public function __construct(NodeDefinition $node) + { + $this->node = $node; + } + + /** + * Marks the expression as being always used. + * + * @return $this + */ + public function always(\Closure $then = null) + { + $this->ifPart = function () { return true; }; + + if (null !== $then) { + $this->thenPart = $then; + } + + return $this; + } + + /** + * Sets a closure to use as tests. + * + * The default one tests if the value is true. + * + * @return $this + */ + public function ifTrue(\Closure $closure = null) + { + if (null === $closure) { + $closure = function ($v) { return true === $v; }; + } + + $this->ifPart = $closure; + + return $this; + } + + /** + * Tests if the value is a string. + * + * @return $this + */ + public function ifString() + { + $this->ifPart = function ($v) { return \is_string($v); }; + + return $this; + } + + /** + * Tests if the value is null. + * + * @return $this + */ + public function ifNull() + { + $this->ifPart = function ($v) { return null === $v; }; + + return $this; + } + + /** + * Tests if the value is empty. + * + * @return ExprBuilder + */ + public function ifEmpty() + { + $this->ifPart = function ($v) { return empty($v); }; + + return $this; + } + + /** + * Tests if the value is an array. + * + * @return $this + */ + public function ifArray() + { + $this->ifPart = function ($v) { return \is_array($v); }; + + return $this; + } + + /** + * Tests if the value is in an array. + * + * @return $this + */ + public function ifInArray(array $array) + { + $this->ifPart = function ($v) use ($array) { return \in_array($v, $array, true); }; + + return $this; + } + + /** + * Tests if the value is not in an array. + * + * @return $this + */ + public function ifNotInArray(array $array) + { + $this->ifPart = function ($v) use ($array) { return !\in_array($v, $array, true); }; + + return $this; + } + + /** + * Transforms variables of any type into an array. + * + * @return $this + */ + public function castToArray() + { + $this->ifPart = function ($v) { return !\is_array($v); }; + $this->thenPart = function ($v) { return [$v]; }; + + return $this; + } + + /** + * Sets the closure to run if the test pass. + * + * @return $this + */ + public function then(\Closure $closure) + { + $this->thenPart = $closure; + + return $this; + } + + /** + * Sets a closure returning an empty array. + * + * @return $this + */ + public function thenEmptyArray() + { + $this->thenPart = function () { return []; }; + + return $this; + } + + /** + * Sets a closure marking the value as invalid at processing time. + * + * if you want to add the value of the node in your message just use a %s placeholder. + * + * @return $this + * + * @throws \InvalidArgumentException + */ + public function thenInvalid(string $message) + { + $this->thenPart = function ($v) use ($message) { throw new \InvalidArgumentException(sprintf($message, json_encode($v))); }; + + return $this; + } + + /** + * Sets a closure unsetting this key of the array at processing time. + * + * @return $this + * + * @throws UnsetKeyException + */ + public function thenUnset() + { + $this->thenPart = function () { throw new UnsetKeyException('Unsetting key.'); }; + + return $this; + } + + /** + * Returns the related node. + * + * @return NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition + * + * @throws \RuntimeException + */ + public function end() + { + if (null === $this->ifPart) { + throw new \RuntimeException('You must specify an if part.'); + } + if (null === $this->thenPart) { + throw new \RuntimeException('You must specify a then part.'); + } + + return $this->node; + } + + /** + * Builds the expressions. + * + * @param ExprBuilder[] $expressions An array of ExprBuilder instances to build + * + * @return array + */ + public static function buildExpressions(array $expressions) + { + foreach ($expressions as $k => $expr) { + if ($expr instanceof self) { + $if = $expr->ifPart; + $then = $expr->thenPart; + $expressions[$k] = function ($v) use ($if, $then) { + return $if($v) ? $then($v) : $v; + }; + } + } + + return $expressions; + } +} diff --git a/vendor/symfony/config/Definition/Builder/FloatNodeDefinition.php b/vendor/symfony/config/Definition/Builder/FloatNodeDefinition.php new file mode 100644 index 0000000..f50f190 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/FloatNodeDefinition.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\FloatNode; + +/** + * This class provides a fluent interface for defining a float node. + * + * @author Jeanmonod David + */ +class FloatNodeDefinition extends NumericNodeDefinition +{ + /** + * Instantiates a Node. + * + * @return FloatNode + */ + protected function instantiateNode() + { + return new FloatNode($this->name, $this->parent, $this->min, $this->max, $this->pathSeparator); + } +} diff --git a/vendor/symfony/config/Definition/Builder/IntegerNodeDefinition.php b/vendor/symfony/config/Definition/Builder/IntegerNodeDefinition.php new file mode 100644 index 0000000..d28e5ae --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/IntegerNodeDefinition.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\IntegerNode; + +/** + * This class provides a fluent interface for defining an integer node. + * + * @author Jeanmonod David + */ +class IntegerNodeDefinition extends NumericNodeDefinition +{ + /** + * Instantiates a Node. + * + * @return IntegerNode + */ + protected function instantiateNode() + { + return new IntegerNode($this->name, $this->parent, $this->min, $this->max, $this->pathSeparator); + } +} diff --git a/vendor/symfony/config/Definition/Builder/MergeBuilder.php b/vendor/symfony/config/Definition/Builder/MergeBuilder.php new file mode 100644 index 0000000..a88d49b --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/MergeBuilder.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +/** + * This class builds merge conditions. + * + * @author Johannes M. Schmitt + */ +class MergeBuilder +{ + protected $node; + public $allowFalse = false; + public $allowOverwrite = true; + + public function __construct(NodeDefinition $node) + { + $this->node = $node; + } + + /** + * Sets whether the node can be unset. + * + * @return $this + */ + public function allowUnset(bool $allow = true) + { + $this->allowFalse = $allow; + + return $this; + } + + /** + * Sets whether the node can be overwritten. + * + * @return $this + */ + public function denyOverwrite(bool $deny = true) + { + $this->allowOverwrite = !$deny; + + return $this; + } + + /** + * Returns the related node. + * + * @return NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition + */ + public function end() + { + return $this->node; + } +} diff --git a/vendor/symfony/config/Definition/Builder/NodeBuilder.php b/vendor/symfony/config/Definition/Builder/NodeBuilder.php new file mode 100644 index 0000000..245e972 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/NodeBuilder.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +/** + * This class provides a fluent interface for building a node. + * + * @author Johannes M. Schmitt + */ +class NodeBuilder implements NodeParentInterface +{ + protected $parent; + protected $nodeMapping; + + public function __construct() + { + $this->nodeMapping = [ + 'variable' => VariableNodeDefinition::class, + 'scalar' => ScalarNodeDefinition::class, + 'boolean' => BooleanNodeDefinition::class, + 'integer' => IntegerNodeDefinition::class, + 'float' => FloatNodeDefinition::class, + 'array' => ArrayNodeDefinition::class, + 'enum' => EnumNodeDefinition::class, + ]; + } + + /** + * Set the parent node. + * + * @return $this + */ + public function setParent(ParentNodeDefinitionInterface $parent = null) + { + $this->parent = $parent; + + return $this; + } + + /** + * Creates a child array node. + * + * @return ArrayNodeDefinition + */ + public function arrayNode(string $name) + { + return $this->node($name, 'array'); + } + + /** + * Creates a child scalar node. + * + * @return ScalarNodeDefinition + */ + public function scalarNode(string $name) + { + return $this->node($name, 'scalar'); + } + + /** + * Creates a child Boolean node. + * + * @return BooleanNodeDefinition + */ + public function booleanNode(string $name) + { + return $this->node($name, 'boolean'); + } + + /** + * Creates a child integer node. + * + * @return IntegerNodeDefinition + */ + public function integerNode(string $name) + { + return $this->node($name, 'integer'); + } + + /** + * Creates a child float node. + * + * @return FloatNodeDefinition + */ + public function floatNode(string $name) + { + return $this->node($name, 'float'); + } + + /** + * Creates a child EnumNode. + * + * @return EnumNodeDefinition + */ + public function enumNode(string $name) + { + return $this->node($name, 'enum'); + } + + /** + * Creates a child variable node. + * + * @return VariableNodeDefinition + */ + public function variableNode(string $name) + { + return $this->node($name, 'variable'); + } + + /** + * Returns the parent node. + * + * @return NodeDefinition&ParentNodeDefinitionInterface + */ + public function end() + { + return $this->parent; + } + + /** + * Creates a child node. + * + * @return NodeDefinition + * + * @throws \RuntimeException When the node type is not registered + * @throws \RuntimeException When the node class is not found + */ + public function node(?string $name, string $type) + { + $class = $this->getNodeClass($type); + + $node = new $class($name); + + $this->append($node); + + return $node; + } + + /** + * Appends a node definition. + * + * Usage: + * + * $node = new ArrayNodeDefinition('name') + * ->children() + * ->scalarNode('foo')->end() + * ->scalarNode('baz')->end() + * ->append($this->getBarNodeDefinition()) + * ->end() + * ; + * + * @return $this + */ + public function append(NodeDefinition $node) + { + if ($node instanceof BuilderAwareInterface) { + $builder = clone $this; + $builder->setParent(null); + $node->setBuilder($builder); + } + + if (null !== $this->parent) { + $this->parent->append($node); + // Make this builder the node parent to allow for a fluid interface + $node->setParent($this); + } + + return $this; + } + + /** + * Adds or overrides a node Type. + * + * @param string $type The name of the type + * @param string $class The fully qualified name the node definition class + * + * @return $this + */ + public function setNodeClass(string $type, string $class) + { + $this->nodeMapping[strtolower($type)] = $class; + + return $this; + } + + /** + * Returns the class name of the node definition. + * + * @return string + * + * @throws \RuntimeException When the node type is not registered + * @throws \RuntimeException When the node class is not found + */ + protected function getNodeClass(string $type) + { + $type = strtolower($type); + + if (!isset($this->nodeMapping[$type])) { + throw new \RuntimeException(sprintf('The node type "%s" is not registered.', $type)); + } + + $class = $this->nodeMapping[$type]; + + if (!class_exists($class)) { + throw new \RuntimeException(sprintf('The node class "%s" does not exist.', $class)); + } + + return $class; + } +} diff --git a/vendor/symfony/config/Definition/Builder/NodeDefinition.php b/vendor/symfony/config/Definition/Builder/NodeDefinition.php new file mode 100644 index 0000000..cf153f0 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/NodeDefinition.php @@ -0,0 +1,383 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\BaseNode; +use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; +use Symfony\Component\Config\Definition\NodeInterface; + +/** + * This class provides a fluent interface for defining a node. + * + * @author Johannes M. Schmitt + */ +abstract class NodeDefinition implements NodeParentInterface +{ + protected $name; + protected $normalization; + protected $validation; + protected $defaultValue; + protected $default = false; + protected $required = false; + protected $deprecation = []; + protected $merge; + protected $allowEmptyValue = true; + protected $nullEquivalent; + protected $trueEquivalent = true; + protected $falseEquivalent = false; + protected $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR; + protected $parent; + protected $attributes = []; + + public function __construct(?string $name, NodeParentInterface $parent = null) + { + $this->parent = $parent; + $this->name = $name; + } + + /** + * Sets the parent node. + * + * @return $this + */ + public function setParent(NodeParentInterface $parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * Sets info message. + * + * @return $this + */ + public function info(string $info) + { + return $this->attribute('info', $info); + } + + /** + * Sets example configuration. + * + * @param string|array $example + * + * @return $this + */ + public function example($example) + { + return $this->attribute('example', $example); + } + + /** + * Sets an attribute on the node. + * + * @param mixed $value + * + * @return $this + */ + public function attribute(string $key, $value) + { + $this->attributes[$key] = $value; + + return $this; + } + + /** + * Returns the parent node. + * + * @return NodeParentInterface|NodeBuilder|NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition|null + */ + public function end() + { + return $this->parent; + } + + /** + * Creates the node. + * + * @return NodeInterface + */ + public function getNode(bool $forceRootNode = false) + { + if ($forceRootNode) { + $this->parent = null; + } + + if (null !== $this->normalization) { + $this->normalization->before = ExprBuilder::buildExpressions($this->normalization->before); + } + + if (null !== $this->validation) { + $this->validation->rules = ExprBuilder::buildExpressions($this->validation->rules); + } + + $node = $this->createNode(); + if ($node instanceof BaseNode) { + $node->setAttributes($this->attributes); + } + + return $node; + } + + /** + * Sets the default value. + * + * @param mixed $value The default value + * + * @return $this + */ + public function defaultValue($value) + { + $this->default = true; + $this->defaultValue = $value; + + return $this; + } + + /** + * Sets the node as required. + * + * @return $this + */ + public function isRequired() + { + $this->required = true; + + return $this; + } + + /** + * Sets the node as deprecated. + * + * @param string $package The name of the composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message the deprecation message to use + * + * You can use %node% and %path% placeholders in your message to display, + * respectively, the node name and its complete path + * + * @return $this + */ + public function setDeprecated(/* string $package, string $version, string $message = 'The child node "%node%" at path "%path%" is deprecated.' */) + { + $args = \func_get_args(); + + if (\func_num_args() < 2) { + trigger_deprecation('symfony/config', '5.1', 'The signature of method "%s()" requires 3 arguments: "string $package, string $version, string $message", not defining them is deprecated.', __METHOD__); + + $message = $args[0] ?? 'The child node "%node%" at path "%path%" is deprecated.'; + $package = $version = ''; + } else { + $package = (string) $args[0]; + $version = (string) $args[1]; + $message = (string) ($args[2] ?? 'The child node "%node%" at path "%path%" is deprecated.'); + } + + $this->deprecation = [ + 'package' => $package, + 'version' => $version, + 'message' => $message, + ]; + + return $this; + } + + /** + * Sets the equivalent value used when the node contains null. + * + * @param mixed $value + * + * @return $this + */ + public function treatNullLike($value) + { + $this->nullEquivalent = $value; + + return $this; + } + + /** + * Sets the equivalent value used when the node contains true. + * + * @param mixed $value + * + * @return $this + */ + public function treatTrueLike($value) + { + $this->trueEquivalent = $value; + + return $this; + } + + /** + * Sets the equivalent value used when the node contains false. + * + * @param mixed $value + * + * @return $this + */ + public function treatFalseLike($value) + { + $this->falseEquivalent = $value; + + return $this; + } + + /** + * Sets null as the default value. + * + * @return $this + */ + public function defaultNull() + { + return $this->defaultValue(null); + } + + /** + * Sets true as the default value. + * + * @return $this + */ + public function defaultTrue() + { + return $this->defaultValue(true); + } + + /** + * Sets false as the default value. + * + * @return $this + */ + public function defaultFalse() + { + return $this->defaultValue(false); + } + + /** + * Sets an expression to run before the normalization. + * + * @return ExprBuilder + */ + public function beforeNormalization() + { + return $this->normalization()->before(); + } + + /** + * Denies the node value being empty. + * + * @return $this + */ + public function cannotBeEmpty() + { + $this->allowEmptyValue = false; + + return $this; + } + + /** + * Sets an expression to run for the validation. + * + * The expression receives the value of the node and must return it. It can + * modify it. + * An exception should be thrown when the node is not valid. + * + * @return ExprBuilder + */ + public function validate() + { + return $this->validation()->rule(); + } + + /** + * Sets whether the node can be overwritten. + * + * @return $this + */ + public function cannotBeOverwritten(bool $deny = true) + { + $this->merge()->denyOverwrite($deny); + + return $this; + } + + /** + * Gets the builder for validation rules. + * + * @return ValidationBuilder + */ + protected function validation() + { + if (null === $this->validation) { + $this->validation = new ValidationBuilder($this); + } + + return $this->validation; + } + + /** + * Gets the builder for merging rules. + * + * @return MergeBuilder + */ + protected function merge() + { + if (null === $this->merge) { + $this->merge = new MergeBuilder($this); + } + + return $this->merge; + } + + /** + * Gets the builder for normalization rules. + * + * @return NormalizationBuilder + */ + protected function normalization() + { + if (null === $this->normalization) { + $this->normalization = new NormalizationBuilder($this); + } + + return $this->normalization; + } + + /** + * Instantiate and configure the node according to this definition. + * + * @return NodeInterface + * + * @throws InvalidDefinitionException When the definition is invalid + */ + abstract protected function createNode(); + + /** + * Set PathSeparator to use. + * + * @return $this + */ + public function setPathSeparator(string $separator) + { + if ($this instanceof ParentNodeDefinitionInterface) { + foreach ($this->getChildNodeDefinitions() as $child) { + $child->setPathSeparator($separator); + } + } + + $this->pathSeparator = $separator; + + return $this; + } +} diff --git a/vendor/symfony/config/Definition/Builder/NodeParentInterface.php b/vendor/symfony/config/Definition/Builder/NodeParentInterface.php new file mode 100644 index 0000000..305e993 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/NodeParentInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +/** + * An interface that must be implemented by all node parents. + * + * @author Victor Berchet + */ +interface NodeParentInterface +{ +} diff --git a/vendor/symfony/config/Definition/Builder/NormalizationBuilder.php b/vendor/symfony/config/Definition/Builder/NormalizationBuilder.php new file mode 100644 index 0000000..06cbbd4 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/NormalizationBuilder.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +/** + * This class builds normalization conditions. + * + * @author Johannes M. Schmitt + */ +class NormalizationBuilder +{ + protected $node; + public $before = []; + public $remappings = []; + + public function __construct(NodeDefinition $node) + { + $this->node = $node; + } + + /** + * Registers a key to remap to its plural form. + * + * @param string $key The key to remap + * @param string|null $plural The plural of the key in case of irregular plural + * + * @return $this + */ + public function remap(string $key, string $plural = null) + { + $this->remappings[] = [$key, null === $plural ? $key.'s' : $plural]; + + return $this; + } + + /** + * Registers a closure to run before the normalization or an expression builder to build it if null is provided. + * + * @return ExprBuilder|$this + */ + public function before(\Closure $closure = null) + { + if (null !== $closure) { + $this->before[] = $closure; + + return $this; + } + + return $this->before[] = new ExprBuilder($this->node); + } +} diff --git a/vendor/symfony/config/Definition/Builder/NumericNodeDefinition.php b/vendor/symfony/config/Definition/Builder/NumericNodeDefinition.php new file mode 100644 index 0000000..c4bff17 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/NumericNodeDefinition.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; + +/** + * Abstract class that contains common code of integer and float node definitions. + * + * @author David Jeanmonod + */ +abstract class NumericNodeDefinition extends ScalarNodeDefinition +{ + protected $min; + protected $max; + + /** + * Ensures that the value is smaller than the given reference. + * + * @param int|float $max + * + * @return $this + * + * @throws \InvalidArgumentException when the constraint is inconsistent + */ + public function max($max) + { + if (isset($this->min) && $this->min > $max) { + throw new \InvalidArgumentException(sprintf('You cannot define a max(%s) as you already have a min(%s).', $max, $this->min)); + } + $this->max = $max; + + return $this; + } + + /** + * Ensures that the value is bigger than the given reference. + * + * @param int|float $min + * + * @return $this + * + * @throws \InvalidArgumentException when the constraint is inconsistent + */ + public function min($min) + { + if (isset($this->max) && $this->max < $min) { + throw new \InvalidArgumentException(sprintf('You cannot define a min(%s) as you already have a max(%s).', $min, $this->max)); + } + $this->min = $min; + + return $this; + } + + /** + * {@inheritdoc} + * + * @throws InvalidDefinitionException + */ + public function cannotBeEmpty() + { + throw new InvalidDefinitionException('->cannotBeEmpty() is not applicable to NumericNodeDefinition.'); + } +} diff --git a/vendor/symfony/config/Definition/Builder/ParentNodeDefinitionInterface.php b/vendor/symfony/config/Definition/Builder/ParentNodeDefinitionInterface.php new file mode 100644 index 0000000..449b91a --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/ParentNodeDefinitionInterface.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +/** + * An interface that must be implemented by nodes which can have children. + * + * @author Victor Berchet + */ +interface ParentNodeDefinitionInterface extends BuilderAwareInterface +{ + /** + * Returns a builder to add children nodes. + * + * @return NodeBuilder + */ + public function children(); + + /** + * Appends a node definition. + * + * Usage: + * + * $node = $parentNode + * ->children() + * ->scalarNode('foo')->end() + * ->scalarNode('baz')->end() + * ->append($this->getBarNodeDefinition()) + * ->end() + * ; + * + * @return $this + */ + public function append(NodeDefinition $node); + + /** + * Gets the child node definitions. + * + * @return NodeDefinition[] + */ + public function getChildNodeDefinitions(); +} diff --git a/vendor/symfony/config/Definition/Builder/ScalarNodeDefinition.php b/vendor/symfony/config/Definition/Builder/ScalarNodeDefinition.php new file mode 100644 index 0000000..076f74b --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/ScalarNodeDefinition.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\ScalarNode; + +/** + * This class provides a fluent interface for defining a node. + * + * @author Johannes M. Schmitt + */ +class ScalarNodeDefinition extends VariableNodeDefinition +{ + /** + * Instantiate a Node. + * + * @return ScalarNode + */ + protected function instantiateNode() + { + return new ScalarNode($this->name, $this->parent, $this->pathSeparator); + } +} diff --git a/vendor/symfony/config/Definition/Builder/TreeBuilder.php b/vendor/symfony/config/Definition/Builder/TreeBuilder.php new file mode 100644 index 0000000..f3c3c21 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/TreeBuilder.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\NodeInterface; + +/** + * This is the entry class for building a config tree. + * + * @author Johannes M. Schmitt + */ +class TreeBuilder implements NodeParentInterface +{ + protected $tree; + protected $root; + + public function __construct(string $name, string $type = 'array', NodeBuilder $builder = null) + { + $builder = $builder ?? new NodeBuilder(); + $this->root = $builder->node($name, $type)->setParent($this); + } + + /** + * @return NodeDefinition|ArrayNodeDefinition The root node (as an ArrayNodeDefinition when the type is 'array') + */ + public function getRootNode(): NodeDefinition + { + return $this->root; + } + + /** + * Builds the tree. + * + * @return NodeInterface + * + * @throws \RuntimeException + */ + public function buildTree() + { + if (null !== $this->tree) { + return $this->tree; + } + + return $this->tree = $this->root->getNode(true); + } + + public function setPathSeparator(string $separator) + { + // unset last built as changing path separator changes all nodes + $this->tree = null; + + $this->root->setPathSeparator($separator); + } +} diff --git a/vendor/symfony/config/Definition/Builder/ValidationBuilder.php b/vendor/symfony/config/Definition/Builder/ValidationBuilder.php new file mode 100644 index 0000000..4efc726 --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/ValidationBuilder.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +/** + * This class builds validation conditions. + * + * @author Christophe Coevoet + */ +class ValidationBuilder +{ + protected $node; + public $rules = []; + + public function __construct(NodeDefinition $node) + { + $this->node = $node; + } + + /** + * Registers a closure to run as normalization or an expression builder to build it if null is provided. + * + * @return ExprBuilder|$this + */ + public function rule(\Closure $closure = null) + { + if (null !== $closure) { + $this->rules[] = $closure; + + return $this; + } + + return $this->rules[] = new ExprBuilder($this->node); + } +} diff --git a/vendor/symfony/config/Definition/Builder/VariableNodeDefinition.php b/vendor/symfony/config/Definition/Builder/VariableNodeDefinition.php new file mode 100644 index 0000000..eea16cc --- /dev/null +++ b/vendor/symfony/config/Definition/Builder/VariableNodeDefinition.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\VariableNode; + +/** + * This class provides a fluent interface for defining a node. + * + * @author Johannes M. Schmitt + */ +class VariableNodeDefinition extends NodeDefinition +{ + /** + * Instantiate a Node. + * + * @return VariableNode + */ + protected function instantiateNode() + { + return new VariableNode($this->name, $this->parent, $this->pathSeparator); + } + + /** + * {@inheritdoc} + */ + protected function createNode() + { + $node = $this->instantiateNode(); + + if (null !== $this->normalization) { + $node->setNormalizationClosures($this->normalization->before); + } + + if (null !== $this->merge) { + $node->setAllowOverwrite($this->merge->allowOverwrite); + } + + if (true === $this->default) { + $node->setDefaultValue($this->defaultValue); + } + + $node->setAllowEmptyValue($this->allowEmptyValue); + $node->addEquivalentValue(null, $this->nullEquivalent); + $node->addEquivalentValue(true, $this->trueEquivalent); + $node->addEquivalentValue(false, $this->falseEquivalent); + $node->setRequired($this->required); + + if ($this->deprecation) { + $node->setDeprecated($this->deprecation['package'], $this->deprecation['version'], $this->deprecation['message']); + } + + if (null !== $this->validation) { + $node->setFinalValidationClosures($this->validation->rules); + } + + return $node; + } +} diff --git a/vendor/symfony/config/Definition/ConfigurationInterface.php b/vendor/symfony/config/Definition/ConfigurationInterface.php new file mode 100644 index 0000000..7b5d443 --- /dev/null +++ b/vendor/symfony/config/Definition/ConfigurationInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; + +/** + * Configuration interface. + * + * @author Victor Berchet + */ +interface ConfigurationInterface +{ + /** + * Generates the configuration tree builder. + * + * @return TreeBuilder + */ + public function getConfigTreeBuilder(); +} diff --git a/vendor/symfony/config/Definition/Dumper/XmlReferenceDumper.php b/vendor/symfony/config/Definition/Dumper/XmlReferenceDumper.php new file mode 100644 index 0000000..a8b18a0 --- /dev/null +++ b/vendor/symfony/config/Definition/Dumper/XmlReferenceDumper.php @@ -0,0 +1,306 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Dumper; + +use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\BaseNode; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\EnumNode; +use Symfony\Component\Config\Definition\NodeInterface; +use Symfony\Component\Config\Definition\PrototypedArrayNode; + +/** + * Dumps an XML reference configuration for the given configuration/node instance. + * + * @author Wouter J + */ +class XmlReferenceDumper +{ + private $reference; + + public function dump(ConfigurationInterface $configuration, string $namespace = null) + { + return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree(), $namespace); + } + + public function dumpNode(NodeInterface $node, string $namespace = null) + { + $this->reference = ''; + $this->writeNode($node, 0, true, $namespace); + $ref = $this->reference; + $this->reference = null; + + return $ref; + } + + private function writeNode(NodeInterface $node, int $depth = 0, bool $root = false, string $namespace = null) + { + $rootName = ($root ? 'config' : $node->getName()); + $rootNamespace = ($namespace ?: ($root ? 'http://example.org/schema/dic/'.$node->getName() : null)); + + // xml remapping + if ($node->getParent()) { + $remapping = array_filter($node->getParent()->getXmlRemappings(), function (array $mapping) use ($rootName) { + return $rootName === $mapping[1]; + }); + + if (\count($remapping)) { + [$singular] = current($remapping); + $rootName = $singular; + } + } + $rootName = str_replace('_', '-', $rootName); + + $rootAttributes = []; + $rootAttributeComments = []; + $rootChildren = []; + $rootComments = []; + + if ($node instanceof ArrayNode) { + $children = $node->getChildren(); + + // comments about the root node + if ($rootInfo = $node->getInfo()) { + $rootComments[] = $rootInfo; + } + + if ($rootNamespace) { + $rootComments[] = 'Namespace: '.$rootNamespace; + } + + // render prototyped nodes + if ($node instanceof PrototypedArrayNode) { + $prototype = $node->getPrototype(); + + $info = 'prototype'; + if (null !== $prototype->getInfo()) { + $info .= ': '.$prototype->getInfo(); + } + array_unshift($rootComments, $info); + + if ($key = $node->getKeyAttribute()) { + $rootAttributes[$key] = str_replace('-', ' ', $rootName).' '.$key; + } + + if ($prototype instanceof PrototypedArrayNode) { + $prototype->setName($key ?? ''); + $children = [$key => $prototype]; + } elseif ($prototype instanceof ArrayNode) { + $children = $prototype->getChildren(); + } else { + if ($prototype->hasDefaultValue()) { + $prototypeValue = $prototype->getDefaultValue(); + } else { + switch (\get_class($prototype)) { + case 'Symfony\Component\Config\Definition\ScalarNode': + $prototypeValue = 'scalar value'; + break; + + case 'Symfony\Component\Config\Definition\FloatNode': + case 'Symfony\Component\Config\Definition\IntegerNode': + $prototypeValue = 'numeric value'; + break; + + case 'Symfony\Component\Config\Definition\BooleanNode': + $prototypeValue = 'true|false'; + break; + + case 'Symfony\Component\Config\Definition\EnumNode': + $prototypeValue = implode('|', array_map('json_encode', $prototype->getValues())); + break; + + default: + $prototypeValue = 'value'; + } + } + } + } + + // get attributes and elements + foreach ($children as $child) { + if ($child instanceof ArrayNode) { + // get elements + $rootChildren[] = $child; + + continue; + } + + // get attributes + + // metadata + $name = str_replace('_', '-', $child->getName()); + $value = '%%%%not_defined%%%%'; // use a string which isn't used in the normal world + + // comments + $comments = []; + if ($child instanceof BaseNode && $info = $child->getInfo()) { + $comments[] = $info; + } + + if ($child instanceof BaseNode && $example = $child->getExample()) { + $comments[] = 'Example: '.$example; + } + + if ($child->isRequired()) { + $comments[] = 'Required'; + } + + if ($child instanceof BaseNode && $child->isDeprecated()) { + $deprecation = $child->getDeprecation($child->getName(), $node->getPath()); + $comments[] = sprintf('Deprecated (%s)', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']); + } + + if ($child instanceof EnumNode) { + $comments[] = 'One of '.implode('; ', array_map('json_encode', $child->getValues())); + } + + if (\count($comments)) { + $rootAttributeComments[$name] = implode(";\n", $comments); + } + + // default values + if ($child->hasDefaultValue()) { + $value = $child->getDefaultValue(); + } + + // append attribute + $rootAttributes[$name] = $value; + } + } + + // render comments + + // root node comment + if (\count($rootComments)) { + foreach ($rootComments as $comment) { + $this->writeLine('', $depth); + } + } + + // attribute comments + if (\count($rootAttributeComments)) { + foreach ($rootAttributeComments as $attrName => $comment) { + $commentDepth = $depth + 4 + \strlen($attrName) + 2; + $commentLines = explode("\n", $comment); + $multiline = (\count($commentLines) > 1); + $comment = implode(\PHP_EOL.str_repeat(' ', $commentDepth), $commentLines); + + if ($multiline) { + $this->writeLine('', $depth); + } else { + $this->writeLine('', $depth); + } + } + } + + // render start tag + attributes + $rootIsVariablePrototype = isset($prototypeValue); + $rootIsEmptyTag = (0 === \count($rootChildren) && !$rootIsVariablePrototype); + $rootOpenTag = '<'.$rootName; + if (1 >= ($attributesCount = \count($rootAttributes))) { + if (1 === $attributesCount) { + $rootOpenTag .= sprintf(' %s="%s"', current(array_keys($rootAttributes)), $this->writeValue(current($rootAttributes))); + } + + $rootOpenTag .= $rootIsEmptyTag ? ' />' : '>'; + + if ($rootIsVariablePrototype) { + $rootOpenTag .= $prototypeValue.''; + } + + $this->writeLine($rootOpenTag, $depth); + } else { + $this->writeLine($rootOpenTag, $depth); + + $i = 1; + + foreach ($rootAttributes as $attrName => $attrValue) { + $attr = sprintf('%s="%s"', $attrName, $this->writeValue($attrValue)); + + $this->writeLine($attr, $depth + 4); + + if ($attributesCount === $i++) { + $this->writeLine($rootIsEmptyTag ? '/>' : '>', $depth); + + if ($rootIsVariablePrototype) { + $rootOpenTag .= $prototypeValue.''; + } + } + } + } + + // render children tags + foreach ($rootChildren as $child) { + $this->writeLine(''); + $this->writeNode($child, $depth + 4); + } + + // render end tag + if (!$rootIsEmptyTag && !$rootIsVariablePrototype) { + $this->writeLine(''); + + $rootEndTag = ''; + $this->writeLine($rootEndTag, $depth); + } + } + + /** + * Outputs a single config reference line. + */ + private function writeLine(string $text, int $indent = 0) + { + $indent = \strlen($text) + $indent; + $format = '%'.$indent.'s'; + + $this->reference .= sprintf($format, $text).\PHP_EOL; + } + + /** + * Renders the string conversion of the value. + * + * @param mixed $value + */ + private function writeValue($value): string + { + if ('%%%%not_defined%%%%' === $value) { + return ''; + } + + if (\is_string($value) || is_numeric($value)) { + return $value; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + if (null === $value) { + return 'null'; + } + + if (empty($value)) { + return ''; + } + + if (\is_array($value)) { + return implode(',', $value); + } + + return ''; + } +} diff --git a/vendor/symfony/config/Definition/Dumper/YamlReferenceDumper.php b/vendor/symfony/config/Definition/Dumper/YamlReferenceDumper.php new file mode 100644 index 0000000..6fcfb71 --- /dev/null +++ b/vendor/symfony/config/Definition/Dumper/YamlReferenceDumper.php @@ -0,0 +1,251 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Dumper; + +use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\BaseNode; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\EnumNode; +use Symfony\Component\Config\Definition\NodeInterface; +use Symfony\Component\Config\Definition\PrototypedArrayNode; +use Symfony\Component\Config\Definition\ScalarNode; +use Symfony\Component\Config\Definition\VariableNode; +use Symfony\Component\Yaml\Inline; + +/** + * Dumps a Yaml reference configuration for the given configuration/node instance. + * + * @author Kevin Bond + */ +class YamlReferenceDumper +{ + private $reference; + + public function dump(ConfigurationInterface $configuration) + { + return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree()); + } + + public function dumpAtPath(ConfigurationInterface $configuration, string $path) + { + $rootNode = $node = $configuration->getConfigTreeBuilder()->buildTree(); + + foreach (explode('.', $path) as $step) { + if (!$node instanceof ArrayNode) { + throw new \UnexpectedValueException(sprintf('Unable to find node at path "%s.%s".', $rootNode->getName(), $path)); + } + + /** @var NodeInterface[] $children */ + $children = $node instanceof PrototypedArrayNode ? $this->getPrototypeChildren($node) : $node->getChildren(); + + foreach ($children as $child) { + if ($child->getName() === $step) { + $node = $child; + + continue 2; + } + } + + throw new \UnexpectedValueException(sprintf('Unable to find node at path "%s.%s".', $rootNode->getName(), $path)); + } + + return $this->dumpNode($node); + } + + public function dumpNode(NodeInterface $node) + { + $this->reference = ''; + $this->writeNode($node); + $ref = $this->reference; + $this->reference = null; + + return $ref; + } + + private function writeNode(NodeInterface $node, NodeInterface $parentNode = null, int $depth = 0, bool $prototypedArray = false) + { + $comments = []; + $default = ''; + $defaultArray = null; + $children = null; + $example = null; + if ($node instanceof BaseNode) { + $example = $node->getExample(); + } + + // defaults + if ($node instanceof ArrayNode) { + $children = $node->getChildren(); + + if ($node instanceof PrototypedArrayNode) { + $children = $this->getPrototypeChildren($node); + } + + if (!$children) { + if ($node->hasDefaultValue() && \count($defaultArray = $node->getDefaultValue())) { + $default = ''; + } elseif (!\is_array($example)) { + $default = '[]'; + } + } + } elseif ($node instanceof EnumNode) { + $comments[] = 'One of '.implode('; ', array_map('json_encode', $node->getValues())); + $default = $node->hasDefaultValue() ? Inline::dump($node->getDefaultValue()) : '~'; + } elseif (VariableNode::class === \get_class($node) && \is_array($example)) { + // If there is an array example, we are sure we dont need to print a default value + $default = ''; + } else { + $default = '~'; + + if ($node->hasDefaultValue()) { + $default = $node->getDefaultValue(); + + if (\is_array($default)) { + if (\count($defaultArray = $node->getDefaultValue())) { + $default = ''; + } elseif (!\is_array($example)) { + $default = '[]'; + } + } else { + $default = Inline::dump($default); + } + } + } + + // required? + if ($node->isRequired()) { + $comments[] = 'Required'; + } + + // deprecated? + if ($node instanceof BaseNode && $node->isDeprecated()) { + $deprecation = $node->getDeprecation($node->getName(), $parentNode ? $parentNode->getPath() : $node->getPath()); + $comments[] = sprintf('Deprecated (%s)', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']); + } + + // example + if ($example && !\is_array($example)) { + $comments[] = 'Example: '.Inline::dump($example); + } + + $default = '' != (string) $default ? ' '.$default : ''; + $comments = \count($comments) ? '# '.implode(', ', $comments) : ''; + + $key = $prototypedArray ? '-' : $node->getName().':'; + $text = rtrim(sprintf('%-21s%s %s', $key, $default, $comments), ' '); + + if ($node instanceof BaseNode && $info = $node->getInfo()) { + $this->writeLine(''); + // indenting multi-line info + $info = str_replace("\n", sprintf("\n%".($depth * 4).'s# ', ' '), $info); + $this->writeLine('# '.$info, $depth * 4); + } + + $this->writeLine($text, $depth * 4); + + // output defaults + if ($defaultArray) { + $this->writeLine(''); + + $message = \count($defaultArray) > 1 ? 'Defaults' : 'Default'; + + $this->writeLine('# '.$message.':', $depth * 4 + 4); + + $this->writeArray($defaultArray, $depth + 1); + } + + if (\is_array($example)) { + $this->writeLine(''); + + $message = \count($example) > 1 ? 'Examples' : 'Example'; + + $this->writeLine('# '.$message.':', $depth * 4 + 4); + + $this->writeArray(array_map([Inline::class, 'dump'], $example), $depth + 1); + } + + if ($children) { + foreach ($children as $childNode) { + $this->writeNode($childNode, $node, $depth + 1, $node instanceof PrototypedArrayNode && !$node->getKeyAttribute()); + } + } + } + + /** + * Outputs a single config reference line. + */ + private function writeLine(string $text, int $indent = 0) + { + $indent = \strlen($text) + $indent; + $format = '%'.$indent.'s'; + + $this->reference .= sprintf($format, $text)."\n"; + } + + private function writeArray(array $array, int $depth) + { + $isIndexed = array_values($array) === $array; + + foreach ($array as $key => $value) { + if (\is_array($value)) { + $val = ''; + } else { + $val = $value; + } + + if ($isIndexed) { + $this->writeLine('- '.$val, $depth * 4); + } else { + $this->writeLine(sprintf('%-20s %s', $key.':', $val), $depth * 4); + } + + if (\is_array($value)) { + $this->writeArray($value, $depth + 1); + } + } + } + + private function getPrototypeChildren(PrototypedArrayNode $node): array + { + $prototype = $node->getPrototype(); + $key = $node->getKeyAttribute(); + + // Do not expand prototype if it isn't an array node nor uses attribute as key + if (!$key && !$prototype instanceof ArrayNode) { + return $node->getChildren(); + } + + if ($prototype instanceof ArrayNode) { + $keyNode = new ArrayNode($key, $node); + $children = $prototype->getChildren(); + + if ($prototype instanceof PrototypedArrayNode && $prototype->getKeyAttribute()) { + $children = $this->getPrototypeChildren($prototype); + } + + // add children + foreach ($children as $childNode) { + $keyNode->addChild($childNode); + } + } else { + $keyNode = new ScalarNode($key, $node); + } + + $info = 'Prototype'; + if (null !== $prototype->getInfo()) { + $info .= ': '.$prototype->getInfo(); + } + $keyNode->setInfo($info); + + return [$key => $keyNode]; + } +} diff --git a/vendor/symfony/config/Definition/EnumNode.php b/vendor/symfony/config/Definition/EnumNode.php new file mode 100644 index 0000000..822e6b5 --- /dev/null +++ b/vendor/symfony/config/Definition/EnumNode.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; + +/** + * Node which only allows a finite set of values. + * + * @author Johannes M. Schmitt + */ +class EnumNode extends ScalarNode +{ + private $values; + + public function __construct(?string $name, NodeInterface $parent = null, array $values = [], string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR) + { + $values = array_unique($values); + if (empty($values)) { + throw new \InvalidArgumentException('$values must contain at least one element.'); + } + + parent::__construct($name, $parent, $pathSeparator); + $this->values = $values; + } + + public function getValues() + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + protected function finalizeValue($value) + { + $value = parent::finalizeValue($value); + + if (!\in_array($value, $this->values, true)) { + $ex = new InvalidConfigurationException(sprintf('The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), implode(', ', array_map('json_encode', $this->values)))); + $ex->setPath($this->getPath()); + + throw $ex; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + protected function allowPlaceholders(): bool + { + return false; + } +} diff --git a/vendor/symfony/config/Definition/Exception/DuplicateKeyException.php b/vendor/symfony/config/Definition/Exception/DuplicateKeyException.php new file mode 100644 index 0000000..48dd932 --- /dev/null +++ b/vendor/symfony/config/Definition/Exception/DuplicateKeyException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Exception; + +/** + * This exception is thrown whenever the key of an array is not unique. This can + * only be the case if the configuration is coming from an XML file. + * + * @author Johannes M. Schmitt + */ +class DuplicateKeyException extends InvalidConfigurationException +{ +} diff --git a/vendor/symfony/config/Definition/Exception/Exception.php b/vendor/symfony/config/Definition/Exception/Exception.php new file mode 100644 index 0000000..8933a49 --- /dev/null +++ b/vendor/symfony/config/Definition/Exception/Exception.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Exception; + +/** + * Base exception for all configuration exceptions. + * + * @author Johannes M. Schmitt + */ +class Exception extends \RuntimeException +{ +} diff --git a/vendor/symfony/config/Definition/Exception/ForbiddenOverwriteException.php b/vendor/symfony/config/Definition/Exception/ForbiddenOverwriteException.php new file mode 100644 index 0000000..726c07f --- /dev/null +++ b/vendor/symfony/config/Definition/Exception/ForbiddenOverwriteException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Exception; + +/** + * This exception is thrown when a configuration path is overwritten from a + * subsequent configuration file, but the entry node specifically forbids this. + * + * @author Johannes M. Schmitt + */ +class ForbiddenOverwriteException extends InvalidConfigurationException +{ +} diff --git a/vendor/symfony/config/Definition/Exception/InvalidConfigurationException.php b/vendor/symfony/config/Definition/Exception/InvalidConfigurationException.php new file mode 100644 index 0000000..ceb5e23 --- /dev/null +++ b/vendor/symfony/config/Definition/Exception/InvalidConfigurationException.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Exception; + +/** + * A very general exception which can be thrown whenever non of the more specific + * exceptions is suitable. + * + * @author Johannes M. Schmitt + */ +class InvalidConfigurationException extends Exception +{ + private $path; + private $containsHints = false; + + public function setPath(string $path) + { + $this->path = $path; + } + + public function getPath() + { + return $this->path; + } + + /** + * Adds extra information that is suffixed to the original exception message. + */ + public function addHint(string $hint) + { + if (!$this->containsHints) { + $this->message .= "\nHint: ".$hint; + $this->containsHints = true; + } else { + $this->message .= ', '.$hint; + } + } +} diff --git a/vendor/symfony/config/Definition/Exception/InvalidDefinitionException.php b/vendor/symfony/config/Definition/Exception/InvalidDefinitionException.php new file mode 100644 index 0000000..98310da --- /dev/null +++ b/vendor/symfony/config/Definition/Exception/InvalidDefinitionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Exception; + +/** + * Thrown when an error is detected in a node Definition. + * + * @author Victor Berchet + */ +class InvalidDefinitionException extends Exception +{ +} diff --git a/vendor/symfony/config/Definition/Exception/InvalidTypeException.php b/vendor/symfony/config/Definition/Exception/InvalidTypeException.php new file mode 100644 index 0000000..d7ca8c9 --- /dev/null +++ b/vendor/symfony/config/Definition/Exception/InvalidTypeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Exception; + +/** + * This exception is thrown if an invalid type is encountered. + * + * @author Johannes M. Schmitt + */ +class InvalidTypeException extends InvalidConfigurationException +{ +} diff --git a/vendor/symfony/config/Definition/Exception/UnsetKeyException.php b/vendor/symfony/config/Definition/Exception/UnsetKeyException.php new file mode 100644 index 0000000..863181a --- /dev/null +++ b/vendor/symfony/config/Definition/Exception/UnsetKeyException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Exception; + +/** + * This exception is usually not encountered by the end-user, but only used + * internally to signal the parent scope to unset a key. + * + * @author Johannes M. Schmitt + */ +class UnsetKeyException extends Exception +{ +} diff --git a/vendor/symfony/config/Definition/FloatNode.php b/vendor/symfony/config/Definition/FloatNode.php new file mode 100644 index 0000000..527f996 --- /dev/null +++ b/vendor/symfony/config/Definition/FloatNode.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; + +/** + * This node represents a float value in the config tree. + * + * @author Jeanmonod David + */ +class FloatNode extends NumericNode +{ + /** + * {@inheritdoc} + */ + protected function validateType($value) + { + // Integers are also accepted, we just cast them + if (\is_int($value)) { + $value = (float) $value; + } + + if (!\is_float($value)) { + $ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected "float", but got "%s".', $this->getPath(), get_debug_type($value))); + if ($hint = $this->getInfo()) { + $ex->addHint($hint); + } + $ex->setPath($this->getPath()); + + throw $ex; + } + } + + /** + * {@inheritdoc} + */ + protected function getValidPlaceholderTypes(): array + { + return ['float']; + } +} diff --git a/vendor/symfony/config/Definition/IntegerNode.php b/vendor/symfony/config/Definition/IntegerNode.php new file mode 100644 index 0000000..dfb4cc6 --- /dev/null +++ b/vendor/symfony/config/Definition/IntegerNode.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; + +/** + * This node represents an integer value in the config tree. + * + * @author Jeanmonod David + */ +class IntegerNode extends NumericNode +{ + /** + * {@inheritdoc} + */ + protected function validateType($value) + { + if (!\is_int($value)) { + $ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected "int", but got "%s".', $this->getPath(), get_debug_type($value))); + if ($hint = $this->getInfo()) { + $ex->addHint($hint); + } + $ex->setPath($this->getPath()); + + throw $ex; + } + } + + /** + * {@inheritdoc} + */ + protected function getValidPlaceholderTypes(): array + { + return ['int']; + } +} diff --git a/vendor/symfony/config/Definition/NodeInterface.php b/vendor/symfony/config/Definition/NodeInterface.php new file mode 100644 index 0000000..9c279ae --- /dev/null +++ b/vendor/symfony/config/Definition/NodeInterface.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; + +/** + * Common Interface among all nodes. + * + * In most cases, it is better to inherit from BaseNode instead of implementing + * this interface yourself. + * + * @author Johannes M. Schmitt + */ +interface NodeInterface +{ + /** + * Returns the name of the node. + * + * @return string + */ + public function getName(); + + /** + * Returns the path of the node. + * + * @return string + */ + public function getPath(); + + /** + * Returns true when the node is required. + * + * @return bool + */ + public function isRequired(); + + /** + * Returns true when the node has a default value. + * + * @return bool + */ + public function hasDefaultValue(); + + /** + * Returns the default value of the node. + * + * @return mixed + * + * @throws \RuntimeException if the node has no default value + */ + public function getDefaultValue(); + + /** + * Normalizes a value. + * + * @param mixed $value The value to normalize + * + * @return mixed + * + * @throws InvalidTypeException if the value type is invalid + */ + public function normalize($value); + + /** + * Merges two values together. + * + * @param mixed $leftSide + * @param mixed $rightSide + * + * @return mixed + * + * @throws ForbiddenOverwriteException if the configuration path cannot be overwritten + * @throws InvalidTypeException if the value type is invalid + */ + public function merge($leftSide, $rightSide); + + /** + * Finalizes a value. + * + * @param mixed $value The value to finalize + * + * @return mixed + * + * @throws InvalidTypeException if the value type is invalid + * @throws InvalidConfigurationException if the value is invalid configuration + */ + public function finalize($value); +} diff --git a/vendor/symfony/config/Definition/NumericNode.php b/vendor/symfony/config/Definition/NumericNode.php new file mode 100644 index 0000000..50d137c --- /dev/null +++ b/vendor/symfony/config/Definition/NumericNode.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; + +/** + * This node represents a numeric value in the config tree. + * + * @author David Jeanmonod + */ +class NumericNode extends ScalarNode +{ + protected $min; + protected $max; + + /** + * @param int|float|null $min + * @param int|float|null $max + */ + public function __construct(?string $name, NodeInterface $parent = null, $min = null, $max = null, string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR) + { + parent::__construct($name, $parent, $pathSeparator); + $this->min = $min; + $this->max = $max; + } + + /** + * {@inheritdoc} + */ + protected function finalizeValue($value) + { + $value = parent::finalizeValue($value); + + $errorMsg = null; + if (isset($this->min) && $value < $this->min) { + $errorMsg = sprintf('The value %s is too small for path "%s". Should be greater than or equal to %s', $value, $this->getPath(), $this->min); + } + if (isset($this->max) && $value > $this->max) { + $errorMsg = sprintf('The value %s is too big for path "%s". Should be less than or equal to %s', $value, $this->getPath(), $this->max); + } + if (isset($errorMsg)) { + $ex = new InvalidConfigurationException($errorMsg); + $ex->setPath($this->getPath()); + throw $ex; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + protected function isValueEmpty($value) + { + // a numeric value cannot be empty + return false; + } +} diff --git a/vendor/symfony/config/Definition/Processor.php b/vendor/symfony/config/Definition/Processor.php new file mode 100644 index 0000000..c431408 --- /dev/null +++ b/vendor/symfony/config/Definition/Processor.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +/** + * This class is the entry point for config normalization/merging/finalization. + * + * @author Johannes M. Schmitt + * + * @final + */ +class Processor +{ + /** + * Processes an array of configurations. + * + * @param array $configs An array of configuration items to process + */ + public function process(NodeInterface $configTree, array $configs): array + { + $currentConfig = []; + foreach ($configs as $config) { + $config = $configTree->normalize($config); + $currentConfig = $configTree->merge($currentConfig, $config); + } + + return $configTree->finalize($currentConfig); + } + + /** + * Processes an array of configurations. + * + * @param array $configs An array of configuration items to process + */ + public function processConfiguration(ConfigurationInterface $configuration, array $configs): array + { + return $this->process($configuration->getConfigTreeBuilder()->buildTree(), $configs); + } + + /** + * Normalizes a configuration entry. + * + * This method returns a normalize configuration array for a given key + * to remove the differences due to the original format (YAML and XML mainly). + * + * Here is an example. + * + * The configuration in XML: + * + * twig.extension.foo + * twig.extension.bar + * + * And the same configuration in YAML: + * + * extensions: ['twig.extension.foo', 'twig.extension.bar'] + * + * @param array $config A config array + * @param string $key The key to normalize + * @param string $plural The plural form of the key if it is irregular + */ + public static function normalizeConfig(array $config, string $key, string $plural = null): array + { + if (null === $plural) { + $plural = $key.'s'; + } + + if (isset($config[$plural])) { + return $config[$plural]; + } + + if (isset($config[$key])) { + if (\is_string($config[$key]) || !\is_int(key($config[$key]))) { + // only one + return [$config[$key]]; + } + + return $config[$key]; + } + + return []; + } +} diff --git a/vendor/symfony/config/Definition/PrototypeNodeInterface.php b/vendor/symfony/config/Definition/PrototypeNodeInterface.php new file mode 100644 index 0000000..b160aa9 --- /dev/null +++ b/vendor/symfony/config/Definition/PrototypeNodeInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +/** + * This interface must be implemented by nodes which can be used as prototypes. + * + * @author Johannes M. Schmitt + */ +interface PrototypeNodeInterface extends NodeInterface +{ + /** + * Sets the name of the node. + */ + public function setName(string $name); +} diff --git a/vendor/symfony/config/Definition/PrototypedArrayNode.php b/vendor/symfony/config/Definition/PrototypedArrayNode.php new file mode 100644 index 0000000..e78159c --- /dev/null +++ b/vendor/symfony/config/Definition/PrototypedArrayNode.php @@ -0,0 +1,350 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\DuplicateKeyException; +use Symfony\Component\Config\Definition\Exception\Exception; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\Exception\UnsetKeyException; + +/** + * Represents a prototyped Array node in the config tree. + * + * @author Johannes M. Schmitt + */ +class PrototypedArrayNode extends ArrayNode +{ + protected $prototype; + protected $keyAttribute; + protected $removeKeyAttribute = false; + protected $minNumberOfElements = 0; + protected $defaultValue = []; + protected $defaultChildren; + /** + * @var NodeInterface[] An array of the prototypes of the simplified value children + */ + private $valuePrototypes = []; + + /** + * Sets the minimum number of elements that a prototype based node must + * contain. By default this is zero, meaning no elements. + */ + public function setMinNumberOfElements(int $number) + { + $this->minNumberOfElements = $number; + } + + /** + * Sets the attribute which value is to be used as key. + * + * This is useful when you have an indexed array that should be an + * associative array. You can select an item from within the array + * to be the key of the particular item. For example, if "id" is the + * "key", then: + * + * [ + * ['id' => 'my_name', 'foo' => 'bar'], + * ]; + * + * becomes + * + * [ + * 'my_name' => ['foo' => 'bar'], + * ]; + * + * If you'd like "'id' => 'my_name'" to still be present in the resulting + * array, then you can set the second argument of this method to false. + * + * @param string $attribute The name of the attribute which value is to be used as a key + * @param bool $remove Whether or not to remove the key + */ + public function setKeyAttribute(string $attribute, bool $remove = true) + { + $this->keyAttribute = $attribute; + $this->removeKeyAttribute = $remove; + } + + /** + * Retrieves the name of the attribute which value should be used as key. + * + * @return string|null + */ + public function getKeyAttribute() + { + return $this->keyAttribute; + } + + /** + * Sets the default value of this node. + */ + public function setDefaultValue(array $value) + { + $this->defaultValue = $value; + } + + /** + * {@inheritdoc} + */ + public function hasDefaultValue() + { + return true; + } + + /** + * Adds default children when none are set. + * + * @param int|string|array|null $children The number of children|The child name|The children names to be added + */ + public function setAddChildrenIfNoneSet($children = ['defaults']) + { + if (null === $children) { + $this->defaultChildren = ['defaults']; + } else { + $this->defaultChildren = \is_int($children) && $children > 0 ? range(1, $children) : (array) $children; + } + } + + /** + * {@inheritdoc} + * + * The default value could be either explicited or derived from the prototype + * default value. + */ + public function getDefaultValue() + { + if (null !== $this->defaultChildren) { + $default = $this->prototype->hasDefaultValue() ? $this->prototype->getDefaultValue() : []; + $defaults = []; + foreach (array_values($this->defaultChildren) as $i => $name) { + $defaults[null === $this->keyAttribute ? $i : $name] = $default; + } + + return $defaults; + } + + return $this->defaultValue; + } + + /** + * Sets the node prototype. + */ + public function setPrototype(PrototypeNodeInterface $node) + { + $this->prototype = $node; + } + + /** + * Retrieves the prototype. + * + * @return PrototypeNodeInterface + */ + public function getPrototype() + { + return $this->prototype; + } + + /** + * Disable adding concrete children for prototyped nodes. + * + * @throws Exception + */ + public function addChild(NodeInterface $node) + { + throw new Exception('A prototyped array node cannot have concrete children.'); + } + + /** + * {@inheritdoc} + */ + protected function finalizeValue($value) + { + if (false === $value) { + throw new UnsetKeyException(sprintf('Unsetting key for path "%s", value: %s.', $this->getPath(), json_encode($value))); + } + + foreach ($value as $k => $v) { + $prototype = $this->getPrototypeForChild($k); + try { + $value[$k] = $prototype->finalize($v); + } catch (UnsetKeyException $e) { + unset($value[$k]); + } + } + + if (\count($value) < $this->minNumberOfElements) { + $ex = new InvalidConfigurationException(sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements)); + $ex->setPath($this->getPath()); + + throw $ex; + } + + return $value; + } + + /** + * {@inheritdoc} + * + * @throws DuplicateKeyException + */ + protected function normalizeValue($value) + { + if (false === $value) { + return $value; + } + + $value = $this->remapXml($value); + + $isList = array_is_list($value); + $normalized = []; + foreach ($value as $k => $v) { + if (null !== $this->keyAttribute && \is_array($v)) { + if (!isset($v[$this->keyAttribute]) && \is_int($k) && $isList) { + $ex = new InvalidConfigurationException(sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath())); + $ex->setPath($this->getPath()); + + throw $ex; + } elseif (isset($v[$this->keyAttribute])) { + $k = $v[$this->keyAttribute]; + + if (\is_float($k)) { + $k = var_export($k, true); + } + + // remove the key attribute when required + if ($this->removeKeyAttribute) { + unset($v[$this->keyAttribute]); + } + + // if only "value" is left + if (array_keys($v) === ['value']) { + $v = $v['value']; + if ($this->prototype instanceof ArrayNode && ($children = $this->prototype->getChildren()) && \array_key_exists('value', $children)) { + $valuePrototype = current($this->valuePrototypes) ?: clone $children['value']; + $valuePrototype->parent = $this; + $originalClosures = $this->prototype->normalizationClosures; + if (\is_array($originalClosures)) { + $valuePrototypeClosures = $valuePrototype->normalizationClosures; + $valuePrototype->normalizationClosures = \is_array($valuePrototypeClosures) ? array_merge($originalClosures, $valuePrototypeClosures) : $originalClosures; + } + $this->valuePrototypes[$k] = $valuePrototype; + } + } + } + + if (\array_key_exists($k, $normalized)) { + $ex = new DuplicateKeyException(sprintf('Duplicate key "%s" for path "%s".', $k, $this->getPath())); + $ex->setPath($this->getPath()); + + throw $ex; + } + } + + $prototype = $this->getPrototypeForChild($k); + if (null !== $this->keyAttribute || !$isList) { + $normalized[$k] = $prototype->normalize($v); + } else { + $normalized[] = $prototype->normalize($v); + } + } + + return $normalized; + } + + /** + * {@inheritdoc} + */ + protected function mergeValues($leftSide, $rightSide) + { + if (false === $rightSide) { + // if this is still false after the last config has been merged the + // finalization pass will take care of removing this key entirely + return false; + } + + if (false === $leftSide || !$this->performDeepMerging) { + return $rightSide; + } + + $isList = array_is_list($rightSide); + foreach ($rightSide as $k => $v) { + // prototype, and key is irrelevant there are no named keys, append the element + if (null === $this->keyAttribute && $isList) { + $leftSide[] = $v; + continue; + } + + // no conflict + if (!\array_key_exists($k, $leftSide)) { + if (!$this->allowNewKeys) { + $ex = new InvalidConfigurationException(sprintf('You are not allowed to define new elements for path "%s". Please define all elements for this path in one config file.', $this->getPath())); + $ex->setPath($this->getPath()); + + throw $ex; + } + + $leftSide[$k] = $v; + continue; + } + + $prototype = $this->getPrototypeForChild($k); + $leftSide[$k] = $prototype->merge($leftSide[$k], $v); + } + + return $leftSide; + } + + /** + * Returns a prototype for the child node that is associated to $key in the value array. + * For general child nodes, this will be $this->prototype. + * But if $this->removeKeyAttribute is true and there are only two keys in the child node: + * one is same as this->keyAttribute and the other is 'value', then the prototype will be different. + * + * For example, assume $this->keyAttribute is 'name' and the value array is as follows: + * + * [ + * [ + * 'name' => 'name001', + * 'value' => 'value001' + * ] + * ] + * + * Now, the key is 0 and the child node is: + * + * [ + * 'name' => 'name001', + * 'value' => 'value001' + * ] + * + * When normalizing the value array, the 'name' element will removed from the child node + * and its value becomes the new key of the child node: + * + * [ + * 'name001' => ['value' => 'value001'] + * ] + * + * Now only 'value' element is left in the child node which can be further simplified into a string: + * + * ['name001' => 'value001'] + * + * Now, the key becomes 'name001' and the child node becomes 'value001' and + * the prototype of child node 'name001' should be a ScalarNode instead of an ArrayNode instance. + * + * @return mixed + */ + private function getPrototypeForChild(string $key) + { + $prototype = $this->valuePrototypes[$key] ?? $this->prototype; + $prototype->setName($key); + + return $prototype; + } +} diff --git a/vendor/symfony/config/Definition/ScalarNode.php b/vendor/symfony/config/Definition/ScalarNode.php new file mode 100644 index 0000000..5296c27 --- /dev/null +++ b/vendor/symfony/config/Definition/ScalarNode.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; + +/** + * This node represents a scalar value in the config tree. + * + * The following values are considered scalars: + * * booleans + * * strings + * * null + * * integers + * * floats + * + * @author Johannes M. Schmitt + */ +class ScalarNode extends VariableNode +{ + /** + * {@inheritdoc} + */ + protected function validateType($value) + { + if (!is_scalar($value) && null !== $value) { + $ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected "scalar", but got "%s".', $this->getPath(), get_debug_type($value))); + if ($hint = $this->getInfo()) { + $ex->addHint($hint); + } + $ex->setPath($this->getPath()); + + throw $ex; + } + } + + /** + * {@inheritdoc} + */ + protected function isValueEmpty($value) + { + // assume environment variables are never empty (which in practice is likely to be true during runtime) + // not doing so breaks many configs that are valid today + if ($this->isHandlingPlaceholder()) { + return false; + } + + return null === $value || '' === $value; + } + + /** + * {@inheritdoc} + */ + protected function getValidPlaceholderTypes(): array + { + return ['bool', 'int', 'float', 'string']; + } +} diff --git a/vendor/symfony/config/Definition/VariableNode.php b/vendor/symfony/config/Definition/VariableNode.php new file mode 100644 index 0000000..e868ece --- /dev/null +++ b/vendor/symfony/config/Definition/VariableNode.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; + +/** + * This node represents a value of variable type in the config tree. + * + * This node is intended for values of arbitrary type. + * Any PHP type is accepted as a value. + * + * @author Jeremy Mikola + */ +class VariableNode extends BaseNode implements PrototypeNodeInterface +{ + protected $defaultValueSet = false; + protected $defaultValue; + protected $allowEmptyValue = true; + + public function setDefaultValue($value) + { + $this->defaultValueSet = true; + $this->defaultValue = $value; + } + + /** + * {@inheritdoc} + */ + public function hasDefaultValue() + { + return $this->defaultValueSet; + } + + /** + * {@inheritdoc} + */ + public function getDefaultValue() + { + $v = $this->defaultValue; + + return $v instanceof \Closure ? $v() : $v; + } + + /** + * Sets if this node is allowed to have an empty value. + * + * @param bool $boolean True if this entity will accept empty values + */ + public function setAllowEmptyValue(bool $boolean) + { + $this->allowEmptyValue = $boolean; + } + + /** + * {@inheritdoc} + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + protected function validateType($value) + { + } + + /** + * {@inheritdoc} + */ + protected function finalizeValue($value) + { + // deny environment variables only when using custom validators + // this avoids ever passing an empty value to final validation closures + if (!$this->allowEmptyValue && $this->isHandlingPlaceholder() && $this->finalValidationClosures) { + $e = new InvalidConfigurationException(sprintf('The path "%s" cannot contain an environment variable when empty values are not allowed by definition and are validated.', $this->getPath())); + if ($hint = $this->getInfo()) { + $e->addHint($hint); + } + $e->setPath($this->getPath()); + + throw $e; + } + + if (!$this->allowEmptyValue && $this->isValueEmpty($value)) { + $ex = new InvalidConfigurationException(sprintf('The path "%s" cannot contain an empty value, but got %s.', $this->getPath(), json_encode($value))); + if ($hint = $this->getInfo()) { + $ex->addHint($hint); + } + $ex->setPath($this->getPath()); + + throw $ex; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + protected function normalizeValue($value) + { + return $value; + } + + /** + * {@inheritdoc} + */ + protected function mergeValues($leftSide, $rightSide) + { + return $rightSide; + } + + /** + * Evaluates if the given value is to be treated as empty. + * + * By default, PHP's empty() function is used to test for emptiness. This + * method may be overridden by subtypes to better match their understanding + * of empty data. + * + * @param mixed $value + * + * @return bool + * + * @see finalizeValue() + */ + protected function isValueEmpty($value) + { + return empty($value); + } +} diff --git a/vendor/symfony/config/Exception/FileLoaderImportCircularReferenceException.php b/vendor/symfony/config/Exception/FileLoaderImportCircularReferenceException.php new file mode 100644 index 0000000..e235ea0 --- /dev/null +++ b/vendor/symfony/config/Exception/FileLoaderImportCircularReferenceException.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Exception; + +/** + * Exception class for when a circular reference is detected when importing resources. + * + * @author Fabien Potencier + */ +class FileLoaderImportCircularReferenceException extends LoaderLoadException +{ + public function __construct(array $resources, ?int $code = 0, \Throwable $previous = null) + { + if (null === $code) { + trigger_deprecation('symfony/config', '5.3', 'Passing null as $code to "%s()" is deprecated, pass 0 instead.', __METHOD__); + + $code = 0; + } + + $message = sprintf('Circular reference detected in "%s" ("%s" > "%s").', $this->varToString($resources[0]), implode('" > "', $resources), $resources[0]); + + \Exception::__construct($message, $code, $previous); + } +} diff --git a/vendor/symfony/config/Exception/FileLocatorFileNotFoundException.php b/vendor/symfony/config/Exception/FileLocatorFileNotFoundException.php new file mode 100644 index 0000000..3ee4b93 --- /dev/null +++ b/vendor/symfony/config/Exception/FileLocatorFileNotFoundException.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Exception; + +/** + * File locator exception if a file does not exist. + * + * @author Leo Feyer + */ +class FileLocatorFileNotFoundException extends \InvalidArgumentException +{ + private $paths; + + public function __construct(string $message = '', int $code = 0, \Throwable $previous = null, array $paths = []) + { + parent::__construct($message, $code, $previous); + + $this->paths = $paths; + } + + public function getPaths() + { + return $this->paths; + } +} diff --git a/vendor/symfony/config/Exception/LoaderLoadException.php b/vendor/symfony/config/Exception/LoaderLoadException.php new file mode 100644 index 0000000..b20e74d --- /dev/null +++ b/vendor/symfony/config/Exception/LoaderLoadException.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Exception; + +/** + * Exception class for when a resource cannot be loaded or imported. + * + * @author Ryan Weaver + */ +class LoaderLoadException extends \Exception +{ + /** + * @param string $resource The resource that could not be imported + * @param string|null $sourceResource The original resource importing the new resource + * @param int|null $code The error code + * @param \Throwable|null $previous A previous exception + * @param string|null $type The type of resource + */ + public function __construct(string $resource, string $sourceResource = null, ?int $code = 0, \Throwable $previous = null, string $type = null) + { + if (null === $code) { + trigger_deprecation('symfony/config', '5.3', 'Passing null as $code to "%s()" is deprecated, pass 0 instead.', __METHOD__); + + $code = 0; + } + + $message = ''; + if ($previous) { + // Include the previous exception, to help the user see what might be the underlying cause + + // Trim the trailing period of the previous message. We only want 1 period remove so no rtrim... + if ('.' === substr($previous->getMessage(), -1)) { + $trimmedMessage = substr($previous->getMessage(), 0, -1); + $message .= sprintf('%s', $trimmedMessage).' in '; + } else { + $message .= sprintf('%s', $previous->getMessage()).' in '; + } + $message .= $resource.' '; + + // show tweaked trace to complete the human readable sentence + if (null === $sourceResource) { + $message .= sprintf('(which is loaded in resource "%s")', $resource); + } else { + $message .= sprintf('(which is being imported from "%s")', $sourceResource); + } + $message .= '.'; + + // if there's no previous message, present it the default way + } elseif (null === $sourceResource) { + $message .= sprintf('Cannot load resource "%s".', $resource); + } else { + $message .= sprintf('Cannot import resource "%s" from "%s".', $resource, $sourceResource); + } + + // Is the resource located inside a bundle? + if ('@' === $resource[0]) { + $parts = explode(\DIRECTORY_SEPARATOR, $resource); + $bundle = substr($parts[0], 1); + $message .= sprintf(' Make sure the "%s" bundle is correctly registered and loaded in the application kernel class.', $bundle); + $message .= sprintf(' If the bundle is registered, make sure the bundle path "%s" is not empty.', $resource); + } elseif (null !== $type) { + // maybe there is no loader for this specific type + if ('annotation' === $type) { + $message .= ' Make sure to use PHP 8+ or that annotations are installed and enabled.'; + } else { + $message .= sprintf(' Make sure there is a loader supporting the "%s" type.', $type); + } + } + + parent::__construct($message, $code, $previous); + } + + protected function varToString($var) + { + if (\is_object($var)) { + return sprintf('Object(%s)', \get_class($var)); + } + + if (\is_array($var)) { + $a = []; + foreach ($var as $k => $v) { + $a[] = sprintf('%s => %s', $k, $this->varToString($v)); + } + + return sprintf('Array(%s)', implode(', ', $a)); + } + + if (\is_resource($var)) { + return sprintf('Resource(%s)', get_resource_type($var)); + } + + if (null === $var) { + return 'null'; + } + + if (false === $var) { + return 'false'; + } + + if (true === $var) { + return 'true'; + } + + return (string) $var; + } +} diff --git a/vendor/symfony/config/FileLocator.php b/vendor/symfony/config/FileLocator.php new file mode 100644 index 0000000..da35090 --- /dev/null +++ b/vendor/symfony/config/FileLocator.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; + +/** + * FileLocator uses an array of pre-defined paths to find files. + * + * @author Fabien Potencier + */ +class FileLocator implements FileLocatorInterface +{ + protected $paths; + + /** + * @param string|string[] $paths A path or an array of paths where to look for resources + */ + public function __construct($paths = []) + { + $this->paths = (array) $paths; + } + + /** + * {@inheritdoc} + */ + public function locate(string $name, string $currentPath = null, bool $first = true) + { + if ('' === $name) { + throw new \InvalidArgumentException('An empty file name is not valid to be located.'); + } + + if ($this->isAbsolutePath($name)) { + if (!file_exists($name)) { + throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist.', $name), 0, null, [$name]); + } + + return $name; + } + + $paths = $this->paths; + + if (null !== $currentPath) { + array_unshift($paths, $currentPath); + } + + $paths = array_unique($paths); + $filepaths = $notfound = []; + + foreach ($paths as $path) { + if (@file_exists($file = $path.\DIRECTORY_SEPARATOR.$name)) { + if (true === $first) { + return $file; + } + $filepaths[] = $file; + } else { + $notfound[] = $file; + } + } + + if (!$filepaths) { + throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist (in: "%s").', $name, implode('", "', $paths)), 0, null, $notfound); + } + + return $filepaths; + } + + /** + * Returns whether the file path is an absolute path. + */ + private function isAbsolutePath(string $file): bool + { + if ('/' === $file[0] || '\\' === $file[0] + || (\strlen($file) > 3 && ctype_alpha($file[0]) + && ':' === $file[1] + && ('\\' === $file[2] || '/' === $file[2]) + ) + || null !== parse_url($file, \PHP_URL_SCHEME) + ) { + return true; + } + + return false; + } +} diff --git a/vendor/symfony/config/FileLocatorInterface.php b/vendor/symfony/config/FileLocatorInterface.php new file mode 100644 index 0000000..e3ca1d4 --- /dev/null +++ b/vendor/symfony/config/FileLocatorInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; + +/** + * @author Fabien Potencier + */ +interface FileLocatorInterface +{ + /** + * Returns a full path for a given file name. + * + * @param string $name The file name to locate + * @param string|null $currentPath The current path + * @param bool $first Whether to return the first occurrence or an array of filenames + * + * @return string|array The full path to the file or an array of file paths + * + * @throws \InvalidArgumentException If $name is empty + * @throws FileLocatorFileNotFoundException If a file is not found + */ + public function locate(string $name, string $currentPath = null, bool $first = true); +} diff --git a/vendor/symfony/config/LICENSE b/vendor/symfony/config/LICENSE new file mode 100644 index 0000000..9ff2d0d --- /dev/null +++ b/vendor/symfony/config/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/config/Loader/DelegatingLoader.php b/vendor/symfony/config/Loader/DelegatingLoader.php new file mode 100644 index 0000000..e5a74ee --- /dev/null +++ b/vendor/symfony/config/Loader/DelegatingLoader.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +use Symfony\Component\Config\Exception\LoaderLoadException; + +/** + * DelegatingLoader delegates loading to other loaders using a loader resolver. + * + * This loader acts as an array of LoaderInterface objects - each having + * a chance to load a given resource (handled by the resolver) + * + * @author Fabien Potencier + */ +class DelegatingLoader extends Loader +{ + public function __construct(LoaderResolverInterface $resolver) + { + $this->resolver = $resolver; + } + + /** + * {@inheritdoc} + */ + public function load($resource, string $type = null) + { + if (false === $loader = $this->resolver->resolve($resource, $type)) { + throw new LoaderLoadException($resource, null, 0, null, $type); + } + + return $loader->load($resource, $type); + } + + /** + * {@inheritdoc} + */ + public function supports($resource, string $type = null) + { + return false !== $this->resolver->resolve($resource, $type); + } +} diff --git a/vendor/symfony/config/Loader/FileLoader.php b/vendor/symfony/config/Loader/FileLoader.php new file mode 100644 index 0000000..4e1b46c --- /dev/null +++ b/vendor/symfony/config/Loader/FileLoader.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +use Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException; +use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; +use Symfony\Component\Config\Exception\LoaderLoadException; +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Resource\FileExistenceResource; +use Symfony\Component\Config\Resource\GlobResource; + +/** + * FileLoader is the abstract class used by all built-in loaders that are file based. + * + * @author Fabien Potencier + */ +abstract class FileLoader extends Loader +{ + protected static $loading = []; + + protected $locator; + + private $currentDir; + + public function __construct(FileLocatorInterface $locator, string $env = null) + { + $this->locator = $locator; + parent::__construct($env); + } + + /** + * Sets the current directory. + */ + public function setCurrentDir(string $dir) + { + $this->currentDir = $dir; + } + + /** + * Returns the file locator used by this loader. + * + * @return FileLocatorInterface + */ + public function getLocator() + { + return $this->locator; + } + + /** + * Imports a resource. + * + * @param mixed $resource A Resource + * @param string|null $type The resource type or null if unknown + * @param bool $ignoreErrors Whether to ignore import errors or not + * @param string|null $sourceResource The original resource importing the new resource + * @param string|string[]|null $exclude Glob patterns to exclude from the import + * + * @return mixed + * + * @throws LoaderLoadException + * @throws FileLoaderImportCircularReferenceException + * @throws FileLocatorFileNotFoundException + */ + public function import($resource, string $type = null, bool $ignoreErrors = false, string $sourceResource = null, $exclude = null) + { + if (\is_string($resource) && \strlen($resource) !== ($i = strcspn($resource, '*?{[')) && !str_contains($resource, "\n")) { + $excluded = []; + foreach ((array) $exclude as $pattern) { + foreach ($this->glob($pattern, true, $_, false, true) as $path => $info) { + // normalize Windows slashes and remove trailing slashes + $excluded[rtrim(str_replace('\\', '/', $path), '/')] = true; + } + } + + $ret = []; + $isSubpath = 0 !== $i && str_contains(substr($resource, 0, $i), '/'); + foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath, false, $excluded) as $path => $info) { + if (null !== $res = $this->doImport($path, 'glob' === $type ? null : $type, $ignoreErrors, $sourceResource)) { + $ret[] = $res; + } + $isSubpath = true; + } + + if ($isSubpath) { + return isset($ret[1]) ? $ret : ($ret[0] ?? null); + } + } + + return $this->doImport($resource, $type, $ignoreErrors, $sourceResource); + } + + /** + * @internal + */ + protected function glob(string $pattern, bool $recursive, &$resource = null, bool $ignoreErrors = false, bool $forExclusion = false, array $excluded = []) + { + if (\strlen($pattern) === $i = strcspn($pattern, '*?{[')) { + $prefix = $pattern; + $pattern = ''; + } elseif (0 === $i || !str_contains(substr($pattern, 0, $i), '/')) { + $prefix = '.'; + $pattern = '/'.$pattern; + } else { + $prefix = \dirname(substr($pattern, 0, 1 + $i)); + $pattern = substr($pattern, \strlen($prefix)); + } + + try { + $prefix = $this->locator->locate($prefix, $this->currentDir, true); + } catch (FileLocatorFileNotFoundException $e) { + if (!$ignoreErrors) { + throw $e; + } + + $resource = []; + foreach ($e->getPaths() as $path) { + $resource[] = new FileExistenceResource($path); + } + + return; + } + $resource = new GlobResource($prefix, $pattern, $recursive, $forExclusion, $excluded); + + yield from $resource; + } + + private function doImport($resource, string $type = null, bool $ignoreErrors = false, string $sourceResource = null) + { + try { + $loader = $this->resolve($resource, $type); + + if ($loader instanceof self && null !== $this->currentDir) { + $resource = $loader->getLocator()->locate($resource, $this->currentDir, false); + } + + $resources = \is_array($resource) ? $resource : [$resource]; + for ($i = 0; $i < $resourcesCount = \count($resources); ++$i) { + if (isset(self::$loading[$resources[$i]])) { + if ($i == $resourcesCount - 1) { + throw new FileLoaderImportCircularReferenceException(array_keys(self::$loading)); + } + } else { + $resource = $resources[$i]; + break; + } + } + self::$loading[$resource] = true; + + try { + $ret = $loader->load($resource, $type); + } finally { + unset(self::$loading[$resource]); + } + + return $ret; + } catch (FileLoaderImportCircularReferenceException $e) { + throw $e; + } catch (\Exception $e) { + if (!$ignoreErrors) { + // prevent embedded imports from nesting multiple exceptions + if ($e instanceof LoaderLoadException) { + throw $e; + } + + throw new LoaderLoadException($resource, $sourceResource, 0, $e, $type); + } + } + + return null; + } +} diff --git a/vendor/symfony/config/Loader/GlobFileLoader.php b/vendor/symfony/config/Loader/GlobFileLoader.php new file mode 100644 index 0000000..fecb1c5 --- /dev/null +++ b/vendor/symfony/config/Loader/GlobFileLoader.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +/** + * GlobFileLoader loads files from a glob pattern. + * + * @author Fabien Potencier + */ +class GlobFileLoader extends FileLoader +{ + /** + * {@inheritdoc} + */ + public function load($resource, string $type = null) + { + return $this->import($resource); + } + + /** + * {@inheritdoc} + */ + public function supports($resource, string $type = null) + { + return 'glob' === $type; + } +} diff --git a/vendor/symfony/config/Loader/Loader.php b/vendor/symfony/config/Loader/Loader.php new file mode 100644 index 0000000..e7d74b5 --- /dev/null +++ b/vendor/symfony/config/Loader/Loader.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +use Symfony\Component\Config\Exception\LoaderLoadException; + +/** + * Loader is the abstract class used by all built-in loaders. + * + * @author Fabien Potencier + */ +abstract class Loader implements LoaderInterface +{ + protected $resolver; + protected $env; + + public function __construct(string $env = null) + { + $this->env = $env; + } + + /** + * {@inheritdoc} + */ + public function getResolver() + { + return $this->resolver; + } + + /** + * {@inheritdoc} + */ + public function setResolver(LoaderResolverInterface $resolver) + { + $this->resolver = $resolver; + } + + /** + * Imports a resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return mixed + */ + public function import($resource, string $type = null) + { + return $this->resolve($resource, $type)->load($resource, $type); + } + + /** + * Finds a loader able to load an imported resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return LoaderInterface + * + * @throws LoaderLoadException If no loader is found + */ + public function resolve($resource, string $type = null) + { + if ($this->supports($resource, $type)) { + return $this; + } + + $loader = null === $this->resolver ? false : $this->resolver->resolve($resource, $type); + + if (false === $loader) { + throw new LoaderLoadException($resource, null, 0, null, $type); + } + + return $loader; + } +} diff --git a/vendor/symfony/config/Loader/LoaderInterface.php b/vendor/symfony/config/Loader/LoaderInterface.php new file mode 100644 index 0000000..93a160b --- /dev/null +++ b/vendor/symfony/config/Loader/LoaderInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +/** + * LoaderInterface is the interface implemented by all loader classes. + * + * @author Fabien Potencier + */ +interface LoaderInterface +{ + /** + * Loads a resource. + * + * @param mixed $resource The resource + * + * @return mixed + * + * @throws \Exception If something went wrong + */ + public function load($resource, string $type = null); + + /** + * Returns whether this class supports the given resource. + * + * @param mixed $resource A resource + * + * @return bool + */ + public function supports($resource, string $type = null); + + /** + * Gets the loader resolver. + * + * @return LoaderResolverInterface + */ + public function getResolver(); + + /** + * Sets the loader resolver. + */ + public function setResolver(LoaderResolverInterface $resolver); +} diff --git a/vendor/symfony/config/Loader/LoaderResolver.php b/vendor/symfony/config/Loader/LoaderResolver.php new file mode 100644 index 0000000..cce0702 --- /dev/null +++ b/vendor/symfony/config/Loader/LoaderResolver.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +/** + * LoaderResolver selects a loader for a given resource. + * + * A resource can be anything (e.g. a full path to a config file or a Closure). + * Each loader determines whether it can load a resource and how. + * + * @author Fabien Potencier + */ +class LoaderResolver implements LoaderResolverInterface +{ + /** + * @var LoaderInterface[] An array of LoaderInterface objects + */ + private $loaders = []; + + /** + * @param LoaderInterface[] $loaders An array of loaders + */ + public function __construct(array $loaders = []) + { + foreach ($loaders as $loader) { + $this->addLoader($loader); + } + } + + /** + * {@inheritdoc} + */ + public function resolve($resource, string $type = null) + { + foreach ($this->loaders as $loader) { + if ($loader->supports($resource, $type)) { + return $loader; + } + } + + return false; + } + + public function addLoader(LoaderInterface $loader) + { + $this->loaders[] = $loader; + $loader->setResolver($this); + } + + /** + * Returns the registered loaders. + * + * @return LoaderInterface[] + */ + public function getLoaders() + { + return $this->loaders; + } +} diff --git a/vendor/symfony/config/Loader/LoaderResolverInterface.php b/vendor/symfony/config/Loader/LoaderResolverInterface.php new file mode 100644 index 0000000..8a48419 --- /dev/null +++ b/vendor/symfony/config/Loader/LoaderResolverInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +/** + * LoaderResolverInterface selects a loader for a given resource. + * + * @author Fabien Potencier + */ +interface LoaderResolverInterface +{ + /** + * Returns a loader able to load the resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return LoaderInterface|false + */ + public function resolve($resource, string $type = null); +} diff --git a/vendor/symfony/config/Loader/ParamConfigurator.php b/vendor/symfony/config/Loader/ParamConfigurator.php new file mode 100644 index 0000000..70c3f79 --- /dev/null +++ b/vendor/symfony/config/Loader/ParamConfigurator.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +/** + * Placeholder for a parameter. + * + * @author Tobias Nyholm + */ +class ParamConfigurator +{ + private $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function __toString(): string + { + return '%'.$this->name.'%'; + } +} diff --git a/vendor/symfony/config/README.md b/vendor/symfony/config/README.md new file mode 100644 index 0000000..10c2ddd --- /dev/null +++ b/vendor/symfony/config/README.md @@ -0,0 +1,15 @@ +Config Component +================ + +The Config component helps find, load, combine, autofill and validate +configuration values of any kind, whatever their source may be (YAML, XML, INI +files, or for instance a database). + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/config.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/config/Resource/ClassExistenceResource.php b/vendor/symfony/config/Resource/ClassExistenceResource.php new file mode 100644 index 0000000..6aff151 --- /dev/null +++ b/vendor/symfony/config/Resource/ClassExistenceResource.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * ClassExistenceResource represents a class existence. + * Freshness is only evaluated against resource existence. + * + * The resource must be a fully-qualified class name. + * + * @author Fabien Potencier + * + * @final + */ +class ClassExistenceResource implements SelfCheckingResourceInterface +{ + private $resource; + private $exists; + + private static $autoloadLevel = 0; + private static $autoloadedClass; + private static $existsCache = []; + + /** + * @param string $resource The fully-qualified class name + * @param bool|null $exists Boolean when the existency check has already been done + */ + public function __construct(string $resource, bool $exists = null) + { + $this->resource = $resource; + if (null !== $exists) { + $this->exists = [$exists, null]; + } + } + + public function __toString(): string + { + return $this->resource; + } + + public function getResource(): string + { + return $this->resource; + } + + /** + * {@inheritdoc} + * + * @throws \ReflectionException when a parent class/interface/trait is not found + */ + public function isFresh(int $timestamp): bool + { + $loaded = class_exists($this->resource, false) || interface_exists($this->resource, false) || trait_exists($this->resource, false); + + if (null !== $exists = &self::$existsCache[$this->resource]) { + if ($loaded) { + $exists = [true, null]; + } elseif (0 >= $timestamp && !$exists[0] && null !== $exists[1]) { + throw new \ReflectionException($exists[1]); + } + } elseif ([false, null] === $exists = [$loaded, null]) { + if (!self::$autoloadLevel++) { + spl_autoload_register(__CLASS__.'::throwOnRequiredClass'); + } + $autoloadedClass = self::$autoloadedClass; + self::$autoloadedClass = ltrim($this->resource, '\\'); + + try { + $exists[0] = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false); + } catch (\Exception $e) { + $exists[1] = $e->getMessage(); + + try { + self::throwOnRequiredClass($this->resource, $e); + } catch (\ReflectionException $e) { + if (0 >= $timestamp) { + throw $e; + } + } + } catch (\Throwable $e) { + $exists[1] = $e->getMessage(); + + throw $e; + } finally { + self::$autoloadedClass = $autoloadedClass; + if (!--self::$autoloadLevel) { + spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass'); + } + } + } + + if (null === $this->exists) { + $this->exists = $exists; + } + + return $this->exists[0] xor !$exists[0]; + } + + /** + * @internal + */ + public function __sleep(): array + { + if (null === $this->exists) { + $this->isFresh(0); + } + + return ['resource', 'exists']; + } + + /** + * @internal + */ + public function __wakeup() + { + if (\is_bool($this->exists)) { + $this->exists = [$this->exists, null]; + } + } + + /** + * Throws a reflection exception when the passed class does not exist but is required. + * + * A class is considered "not required" when it's loaded as part of a "class_exists" or similar check. + * + * This function can be used as an autoload function to throw a reflection + * exception if the class was not found by previous autoload functions. + * + * A previous exception can be passed. In this case, the class is considered as being + * required totally, so if it doesn't exist, a reflection exception is always thrown. + * If it exists, the previous exception is rethrown. + * + * @throws \ReflectionException + * + * @internal + */ + public static function throwOnRequiredClass(string $class, \Exception $previous = null) + { + // If the passed class is the resource being checked, we shouldn't throw. + if (null === $previous && self::$autoloadedClass === $class) { + return; + } + + if (class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) { + if (null !== $previous) { + throw $previous; + } + + return; + } + + if ($previous instanceof \ReflectionException) { + throw $previous; + } + + $message = sprintf('Class "%s" not found.', $class); + + if (self::$autoloadedClass !== $class) { + $message = substr_replace($message, sprintf(' while loading "%s"', self::$autoloadedClass), -1, 0); + } + + if (null !== $previous) { + $message = $previous->getMessage(); + } + + $e = new \ReflectionException($message, 0, $previous); + + if (null !== $previous) { + throw $e; + } + + $trace = debug_backtrace(); + $autoloadFrame = [ + 'function' => 'spl_autoload_call', + 'args' => [$class], + ]; + + if (\PHP_VERSION_ID >= 80000 && isset($trace[1])) { + $callerFrame = $trace[1]; + $i = 2; + } elseif (false !== $i = array_search($autoloadFrame, $trace, true)) { + $callerFrame = $trace[++$i]; + } else { + throw $e; + } + + if (isset($callerFrame['function']) && !isset($callerFrame['class'])) { + switch ($callerFrame['function']) { + case 'get_class_methods': + case 'get_class_vars': + case 'get_parent_class': + case 'is_a': + case 'is_subclass_of': + case 'class_exists': + case 'class_implements': + case 'class_parents': + case 'trait_exists': + case 'defined': + case 'interface_exists': + case 'method_exists': + case 'property_exists': + case 'is_callable': + return; + } + + $props = [ + 'file' => $callerFrame['file'] ?? null, + 'line' => $callerFrame['line'] ?? null, + 'trace' => \array_slice($trace, 1 + $i), + ]; + + foreach ($props as $p => $v) { + if (null !== $v) { + $r = new \ReflectionProperty(\Exception::class, $p); + $r->setAccessible(true); + $r->setValue($e, $v); + } + } + } + + throw $e; + } +} diff --git a/vendor/symfony/config/Resource/ComposerResource.php b/vendor/symfony/config/Resource/ComposerResource.php new file mode 100644 index 0000000..f552f80 --- /dev/null +++ b/vendor/symfony/config/Resource/ComposerResource.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * ComposerResource tracks the PHP version and Composer dependencies. + * + * @author Nicolas Grekas + * + * @final + */ +class ComposerResource implements SelfCheckingResourceInterface +{ + private $vendors; + + private static $runtimeVendors; + + public function __construct() + { + self::refresh(); + $this->vendors = self::$runtimeVendors; + } + + public function getVendors(): array + { + return array_keys($this->vendors); + } + + public function __toString(): string + { + return __CLASS__; + } + + /** + * {@inheritdoc} + */ + public function isFresh(int $timestamp): bool + { + self::refresh(); + + return array_values(self::$runtimeVendors) === array_values($this->vendors); + } + + private static function refresh() + { + self::$runtimeVendors = []; + + foreach (get_declared_classes() as $class) { + if ('C' === $class[0] && str_starts_with($class, 'ComposerAutoloaderInit')) { + $r = new \ReflectionClass($class); + $v = \dirname($r->getFileName(), 2); + if (is_file($v.'/composer/installed.json')) { + self::$runtimeVendors[$v] = @filemtime($v.'/composer/installed.json'); + } + } + } + } +} diff --git a/vendor/symfony/config/Resource/DirectoryResource.php b/vendor/symfony/config/Resource/DirectoryResource.php new file mode 100644 index 0000000..035814a --- /dev/null +++ b/vendor/symfony/config/Resource/DirectoryResource.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * DirectoryResource represents a resources stored in a subdirectory tree. + * + * @author Fabien Potencier + * + * @final + */ +class DirectoryResource implements SelfCheckingResourceInterface +{ + private $resource; + private $pattern; + + /** + * @param string $resource The file path to the resource + * @param string|null $pattern A pattern to restrict monitored files + * + * @throws \InvalidArgumentException + */ + public function __construct(string $resource, string $pattern = null) + { + $this->resource = realpath($resource) ?: (file_exists($resource) ? $resource : false); + $this->pattern = $pattern; + + if (false === $this->resource || !is_dir($this->resource)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $resource)); + } + } + + public function __toString(): string + { + return md5(serialize([$this->resource, $this->pattern])); + } + + public function getResource(): string + { + return $this->resource; + } + + public function getPattern(): ?string + { + return $this->pattern; + } + + /** + * {@inheritdoc} + */ + public function isFresh(int $timestamp): bool + { + if (!is_dir($this->resource)) { + return false; + } + + if ($timestamp < filemtime($this->resource)) { + return false; + } + + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->resource), \RecursiveIteratorIterator::SELF_FIRST) as $file) { + // if regex filtering is enabled only check matching files + if ($this->pattern && $file->isFile() && !preg_match($this->pattern, $file->getBasename())) { + continue; + } + + // always monitor directories for changes, except the .. entries + // (otherwise deleted files wouldn't get detected) + if ($file->isDir() && str_ends_with($file, '/..')) { + continue; + } + + // for broken links + try { + $fileMTime = $file->getMTime(); + } catch (\RuntimeException $e) { + continue; + } + + // early return if a file's mtime exceeds the passed timestamp + if ($timestamp < $fileMTime) { + return false; + } + } + + return true; + } +} diff --git a/vendor/symfony/config/Resource/FileExistenceResource.php b/vendor/symfony/config/Resource/FileExistenceResource.php new file mode 100644 index 0000000..6d79d6d --- /dev/null +++ b/vendor/symfony/config/Resource/FileExistenceResource.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * FileExistenceResource represents a resource stored on the filesystem. + * Freshness is only evaluated against resource creation or deletion. + * + * The resource can be a file or a directory. + * + * @author Charles-Henri Bruyand + * + * @final + */ +class FileExistenceResource implements SelfCheckingResourceInterface +{ + private $resource; + + private $exists; + + /** + * @param string $resource The file path to the resource + */ + public function __construct(string $resource) + { + $this->resource = $resource; + $this->exists = file_exists($resource); + } + + public function __toString(): string + { + return $this->resource; + } + + public function getResource(): string + { + return $this->resource; + } + + /** + * {@inheritdoc} + */ + public function isFresh(int $timestamp): bool + { + return file_exists($this->resource) === $this->exists; + } +} diff --git a/vendor/symfony/config/Resource/FileResource.php b/vendor/symfony/config/Resource/FileResource.php new file mode 100644 index 0000000..ee6684c --- /dev/null +++ b/vendor/symfony/config/Resource/FileResource.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * FileResource represents a resource stored on the filesystem. + * + * The resource can be a file or a directory. + * + * @author Fabien Potencier + * + * @final + */ +class FileResource implements SelfCheckingResourceInterface +{ + /** + * @var string|false + */ + private $resource; + + /** + * @param string $resource The file path to the resource + * + * @throws \InvalidArgumentException + */ + public function __construct(string $resource) + { + $this->resource = realpath($resource) ?: (file_exists($resource) ? $resource : false); + + if (false === $this->resource) { + throw new \InvalidArgumentException(sprintf('The file "%s" does not exist.', $resource)); + } + } + + public function __toString(): string + { + return $this->resource; + } + + /** + * Returns the canonicalized, absolute path to the resource. + */ + public function getResource(): string + { + return $this->resource; + } + + /** + * {@inheritdoc} + */ + public function isFresh(int $timestamp): bool + { + return false !== ($filemtime = @filemtime($this->resource)) && $filemtime <= $timestamp; + } +} diff --git a/vendor/symfony/config/Resource/GlobResource.php b/vendor/symfony/config/Resource/GlobResource.php new file mode 100644 index 0000000..093f559 --- /dev/null +++ b/vendor/symfony/config/Resource/GlobResource.php @@ -0,0 +1,237 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\Glob; + +/** + * GlobResource represents a set of resources stored on the filesystem. + * + * Only existence/removal is tracked (not mtimes.) + * + * @author Nicolas Grekas + * + * @final + * + * @implements \IteratorAggregate + */ +class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface +{ + private $prefix; + private $pattern; + private $recursive; + private $hash; + private $forExclusion; + private $excludedPrefixes; + private $globBrace; + + /** + * @param string $prefix A directory prefix + * @param string $pattern A glob pattern + * @param bool $recursive Whether directories should be scanned recursively or not + * + * @throws \InvalidArgumentException + */ + public function __construct(string $prefix, string $pattern, bool $recursive, bool $forExclusion = false, array $excludedPrefixes = []) + { + ksort($excludedPrefixes); + $this->prefix = realpath($prefix) ?: (file_exists($prefix) ? $prefix : false); + $this->pattern = $pattern; + $this->recursive = $recursive; + $this->forExclusion = $forExclusion; + $this->excludedPrefixes = $excludedPrefixes; + $this->globBrace = \defined('GLOB_BRACE') ? \GLOB_BRACE : 0; + + if (false === $this->prefix) { + throw new \InvalidArgumentException(sprintf('The path "%s" does not exist.', $prefix)); + } + } + + public function getPrefix(): string + { + return $this->prefix; + } + + public function __toString(): string + { + return 'glob.'.$this->prefix.(int) $this->recursive.$this->pattern.(int) $this->forExclusion.implode("\0", $this->excludedPrefixes); + } + + /** + * {@inheritdoc} + */ + public function isFresh(int $timestamp): bool + { + $hash = $this->computeHash(); + + if (null === $this->hash) { + $this->hash = $hash; + } + + return $this->hash === $hash; + } + + /** + * @internal + */ + public function __sleep(): array + { + if (null === $this->hash) { + $this->hash = $this->computeHash(); + } + + return ['prefix', 'pattern', 'recursive', 'hash', 'forExclusion', 'excludedPrefixes']; + } + + /** + * @internal + */ + public function __wakeup(): void + { + $this->globBrace = \defined('GLOB_BRACE') ? \GLOB_BRACE : 0; + } + + public function getIterator(): \Traversable + { + if (!file_exists($this->prefix) || (!$this->recursive && '' === $this->pattern)) { + return; + } + $prefix = str_replace('\\', '/', $this->prefix); + $paths = null; + + if (!str_starts_with($this->prefix, 'phar://') && !str_contains($this->pattern, '/**/')) { + if ($this->globBrace || !str_contains($this->pattern, '{')) { + $paths = glob($this->prefix.$this->pattern, \GLOB_NOSORT | $this->globBrace); + } elseif (!str_contains($this->pattern, '\\') || !preg_match('/\\\\[,{}]/', $this->pattern)) { + foreach ($this->expandGlob($this->pattern) as $p) { + $paths[] = glob($this->prefix.$p, \GLOB_NOSORT); + } + $paths = array_merge(...$paths); + } + } + + if (null !== $paths) { + natsort($paths); + foreach ($paths as $path) { + if ($this->excludedPrefixes) { + $normalizedPath = str_replace('\\', '/', $path); + do { + if (isset($this->excludedPrefixes[$dirPath = $normalizedPath])) { + continue 2; + } + } while ($prefix !== $dirPath && $dirPath !== $normalizedPath = \dirname($dirPath)); + } + + if (is_file($path)) { + yield $path => new \SplFileInfo($path); + } + if (!is_dir($path)) { + continue; + } + if ($this->forExclusion) { + yield $path => new \SplFileInfo($path); + continue; + } + if (!$this->recursive || isset($this->excludedPrefixes[str_replace('\\', '/', $path)])) { + continue; + } + $files = iterator_to_array(new \RecursiveIteratorIterator( + new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), + function (\SplFileInfo $file, $path) { + return !isset($this->excludedPrefixes[str_replace('\\', '/', $path)]) && '.' !== $file->getBasename()[0]; + } + ), + \RecursiveIteratorIterator::LEAVES_ONLY + )); + uksort($files, 'strnatcmp'); + + foreach ($files as $path => $info) { + if ($info->isFile()) { + yield $path => $info; + } + } + } + + return; + } + + if (!class_exists(Finder::class)) { + throw new \LogicException(sprintf('Extended glob pattern "%s" cannot be used as the Finder component is not installed.', $this->pattern)); + } + + $finder = new Finder(); + $regex = Glob::toRegex($this->pattern); + if ($this->recursive) { + $regex = substr_replace($regex, '(/|$)', -2, 1); + } + + $prefixLen = \strlen($this->prefix); + foreach ($finder->followLinks()->sortByName()->in($this->prefix) as $path => $info) { + $normalizedPath = str_replace('\\', '/', $path); + if (!preg_match($regex, substr($normalizedPath, $prefixLen)) || !$info->isFile()) { + continue; + } + if ($this->excludedPrefixes) { + do { + if (isset($this->excludedPrefixes[$dirPath = $normalizedPath])) { + continue 2; + } + } while ($prefix !== $dirPath && $dirPath !== $normalizedPath = \dirname($dirPath)); + } + + yield $path => $info; + } + } + + private function computeHash(): string + { + $hash = hash_init('md5'); + + foreach ($this->getIterator() as $path => $info) { + hash_update($hash, $path."\n"); + } + + return hash_final($hash); + } + + private function expandGlob(string $pattern): array + { + $segments = preg_split('/\{([^{}]*+)\}/', $pattern, -1, \PREG_SPLIT_DELIM_CAPTURE); + $paths = [$segments[0]]; + $patterns = []; + + for ($i = 1; $i < \count($segments); $i += 2) { + $patterns = []; + + foreach (explode(',', $segments[$i]) as $s) { + foreach ($paths as $p) { + $patterns[] = $p.$s.$segments[1 + $i]; + } + } + + $paths = $patterns; + } + + $j = 0; + foreach ($patterns as $i => $p) { + if (str_contains($p, '{')) { + $p = $this->expandGlob($p); + array_splice($paths, $i + $j, 1, $p); + $j += \count($p) - 1; + } + } + + return $paths; + } +} diff --git a/vendor/symfony/config/Resource/ReflectionClassResource.php b/vendor/symfony/config/Resource/ReflectionClassResource.php new file mode 100644 index 0000000..06f1d76 --- /dev/null +++ b/vendor/symfony/config/Resource/ReflectionClassResource.php @@ -0,0 +1,267 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; + +/** + * @author Nicolas Grekas + * + * @final + */ +class ReflectionClassResource implements SelfCheckingResourceInterface +{ + private $files = []; + private $className; + private $classReflector; + private $excludedVendors = []; + private $hash; + + public function __construct(\ReflectionClass $classReflector, array $excludedVendors = []) + { + $this->className = $classReflector->name; + $this->classReflector = $classReflector; + $this->excludedVendors = $excludedVendors; + } + + /** + * {@inheritdoc} + */ + public function isFresh(int $timestamp): bool + { + if (null === $this->hash) { + $this->hash = $this->computeHash(); + $this->loadFiles($this->classReflector); + } + + foreach ($this->files as $file => $v) { + if (false === $filemtime = @filemtime($file)) { + return false; + } + + if ($filemtime > $timestamp) { + return $this->hash === $this->computeHash(); + } + } + + return true; + } + + public function __toString(): string + { + return 'reflection.'.$this->className; + } + + /** + * @internal + */ + public function __sleep(): array + { + if (null === $this->hash) { + $this->hash = $this->computeHash(); + $this->loadFiles($this->classReflector); + } + + return ['files', 'className', 'hash']; + } + + private function loadFiles(\ReflectionClass $class) + { + foreach ($class->getInterfaces() as $v) { + $this->loadFiles($v); + } + do { + $file = $class->getFileName(); + if (false !== $file && is_file($file)) { + foreach ($this->excludedVendors as $vendor) { + if (str_starts_with($file, $vendor) && false !== strpbrk(substr($file, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) { + $file = false; + break; + } + } + if ($file) { + $this->files[$file] = null; + } + } + foreach ($class->getTraits() as $v) { + $this->loadFiles($v); + } + } while ($class = $class->getParentClass()); + } + + private function computeHash(): string + { + if (null === $this->classReflector) { + try { + $this->classReflector = new \ReflectionClass($this->className); + } catch (\ReflectionException $e) { + // the class does not exist anymore + return false; + } + } + $hash = hash_init('md5'); + + foreach ($this->generateSignature($this->classReflector) as $info) { + hash_update($hash, $info); + } + + return hash_final($hash); + } + + private function generateSignature(\ReflectionClass $class): iterable + { + if (\PHP_VERSION_ID >= 80000) { + $attributes = []; + foreach ($class->getAttributes() as $a) { + $attributes[] = [$a->getName(), \PHP_VERSION_ID >= 80100 ? (string) $a : $a->getArguments()]; + } + yield print_r($attributes, true); + $attributes = []; + } + + yield $class->getDocComment(); + yield (int) $class->isFinal(); + yield (int) $class->isAbstract(); + + if ($class->isTrait()) { + yield print_r(class_uses($class->name), true); + } else { + yield print_r(class_parents($class->name), true); + yield print_r(class_implements($class->name), true); + yield print_r($class->getConstants(), true); + } + + if (!$class->isInterface()) { + $defaults = $class->getDefaultProperties(); + + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $p) { + if (\PHP_VERSION_ID >= 80000) { + foreach ($p->getAttributes() as $a) { + $attributes[] = [$a->getName(), \PHP_VERSION_ID >= 80100 ? (string) $a : $a->getArguments()]; + } + yield print_r($attributes, true); + $attributes = []; + } + + yield $p->getDocComment(); + yield $p->isDefault() ? '' : ''; + yield $p->isPublic() ? 'public' : 'protected'; + yield $p->isStatic() ? 'static' : ''; + yield '$'.$p->name; + yield print_r(isset($defaults[$p->name]) && !\is_object($defaults[$p->name]) ? $defaults[$p->name] : null, true); + } + } + + $defined = \Closure::bind(static function ($c) { return \defined($c); }, null, $class->name); + + foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) { + if (\PHP_VERSION_ID >= 80000) { + foreach ($m->getAttributes() as $a) { + $attributes[] = [$a->getName(), \PHP_VERSION_ID >= 80100 ? (string) $a : $a->getArguments()]; + } + yield print_r($attributes, true); + $attributes = []; + } + + $defaults = []; + $parametersWithUndefinedConstants = []; + foreach ($m->getParameters() as $p) { + if (\PHP_VERSION_ID >= 80000) { + foreach ($p->getAttributes() as $a) { + $attributes[] = [$a->getName(), \PHP_VERSION_ID >= 80100 ? (string) $a : $a->getArguments()]; + } + yield print_r($attributes, true); + $attributes = []; + } + + if (!$p->isDefaultValueAvailable()) { + $defaults[$p->name] = null; + + continue; + } + + if (\PHP_VERSION_ID >= 80100) { + $defaults[$p->name] = (string) $p; + + continue; + } + + if (!$p->isDefaultValueConstant() || $defined($p->getDefaultValueConstantName())) { + $defaults[$p->name] = $p->getDefaultValue(); + + continue; + } + + $defaults[$p->name] = $p->getDefaultValueConstantName(); + $parametersWithUndefinedConstants[$p->name] = true; + } + + if (!$parametersWithUndefinedConstants) { + yield preg_replace('/^ @@.*/m', '', $m); + } else { + $t = $m->getReturnType(); + $stack = [ + $m->getDocComment(), + $m->getName(), + $m->isAbstract(), + $m->isFinal(), + $m->isStatic(), + $m->isPublic(), + $m->isPrivate(), + $m->isProtected(), + $m->returnsReference(), + $t instanceof \ReflectionNamedType ? ((string) $t->allowsNull()).$t->getName() : (string) $t, + ]; + + foreach ($m->getParameters() as $p) { + if (!isset($parametersWithUndefinedConstants[$p->name])) { + $stack[] = (string) $p; + } else { + $t = $p->getType(); + $stack[] = $p->isOptional(); + $stack[] = $t instanceof \ReflectionNamedType ? ((string) $t->allowsNull()).$t->getName() : (string) $t; + $stack[] = $p->isPassedByReference(); + $stack[] = $p->isVariadic(); + $stack[] = $p->getName(); + } + } + + yield implode(',', $stack); + } + + yield print_r($defaults, true); + } + + if ($class->isAbstract() || $class->isInterface() || $class->isTrait()) { + return; + } + + if (interface_exists(EventSubscriberInterface::class, false) && $class->isSubclassOf(EventSubscriberInterface::class)) { + yield EventSubscriberInterface::class; + yield print_r($class->name::getSubscribedEvents(), true); + } + + if (interface_exists(MessageSubscriberInterface::class, false) && $class->isSubclassOf(MessageSubscriberInterface::class)) { + yield MessageSubscriberInterface::class; + foreach ($class->name::getHandledMessages() as $key => $value) { + yield $key.print_r($value, true); + } + } + + if (interface_exists(ServiceSubscriberInterface::class, false) && $class->isSubclassOf(ServiceSubscriberInterface::class)) { + yield ServiceSubscriberInterface::class; + yield print_r($class->name::getSubscribedServices(), true); + } + } +} diff --git a/vendor/symfony/config/Resource/ResourceInterface.php b/vendor/symfony/config/Resource/ResourceInterface.php new file mode 100644 index 0000000..9a0cd9a --- /dev/null +++ b/vendor/symfony/config/Resource/ResourceInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * ResourceInterface is the interface that must be implemented by all Resource classes. + * + * @author Fabien Potencier + */ +interface ResourceInterface +{ + /** + * Returns a string representation of the Resource. + * + * This method is necessary to allow for resource de-duplication, for example by means + * of array_unique(). The string returned need not have a particular meaning, but has + * to be identical for different ResourceInterface instances referring to the same + * resource; and it should be unlikely to collide with that of other, unrelated + * resource instances. + */ + public function __toString(); +} diff --git a/vendor/symfony/config/Resource/SelfCheckingResourceChecker.php b/vendor/symfony/config/Resource/SelfCheckingResourceChecker.php new file mode 100644 index 0000000..e1727b9 --- /dev/null +++ b/vendor/symfony/config/Resource/SelfCheckingResourceChecker.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +use Symfony\Component\Config\ResourceCheckerInterface; + +/** + * Resource checker for instances of SelfCheckingResourceInterface. + * + * As these resources perform the actual check themselves, we can provide + * this class as a standard way of validating them. + * + * @author Matthias Pigulla + */ +class SelfCheckingResourceChecker implements ResourceCheckerInterface +{ + // Common shared cache, because this checker can be used in different + // situations. For example, when using the full stack framework, the router + // and the container have their own cache. But they may check the very same + // resources + private static $cache = []; + + public function supports(ResourceInterface $metadata) + { + return $metadata instanceof SelfCheckingResourceInterface; + } + + /** + * @param SelfCheckingResourceInterface $resource + */ + public function isFresh(ResourceInterface $resource, int $timestamp) + { + $key = "$resource:$timestamp"; + + return self::$cache[$key] ?? self::$cache[$key] = $resource->isFresh($timestamp); + } +} diff --git a/vendor/symfony/config/Resource/SelfCheckingResourceInterface.php b/vendor/symfony/config/Resource/SelfCheckingResourceInterface.php new file mode 100644 index 0000000..2c1a378 --- /dev/null +++ b/vendor/symfony/config/Resource/SelfCheckingResourceInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * Interface for Resources that can check for freshness autonomously, + * without special support from external services. + * + * @author Matthias Pigulla + */ +interface SelfCheckingResourceInterface extends ResourceInterface +{ + /** + * Returns true if the resource has not been updated since the given timestamp. + * + * @param int $timestamp The last time the resource was loaded + * + * @return bool + */ + public function isFresh(int $timestamp); +} diff --git a/vendor/symfony/config/ResourceCheckerConfigCache.php b/vendor/symfony/config/ResourceCheckerConfigCache.php new file mode 100644 index 0000000..a0d301c --- /dev/null +++ b/vendor/symfony/config/ResourceCheckerConfigCache.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; + +/** + * ResourceCheckerConfigCache uses instances of ResourceCheckerInterface + * to check whether cached data is still fresh. + * + * @author Matthias Pigulla + */ +class ResourceCheckerConfigCache implements ConfigCacheInterface +{ + /** + * @var string + */ + private $file; + + /** + * @var iterable + */ + private $resourceCheckers; + + /** + * @param string $file The absolute cache path + * @param iterable $resourceCheckers The ResourceCheckers to use for the freshness check + */ + public function __construct(string $file, iterable $resourceCheckers = []) + { + $this->file = $file; + $this->resourceCheckers = $resourceCheckers; + } + + /** + * {@inheritdoc} + */ + public function getPath() + { + return $this->file; + } + + /** + * Checks if the cache is still fresh. + * + * This implementation will make a decision solely based on the ResourceCheckers + * passed in the constructor. + * + * The first ResourceChecker that supports a given resource is considered authoritative. + * Resources with no matching ResourceChecker will silently be ignored and considered fresh. + * + * @return bool + */ + public function isFresh() + { + if (!is_file($this->file)) { + return false; + } + + if ($this->resourceCheckers instanceof \Traversable && !$this->resourceCheckers instanceof \Countable) { + $this->resourceCheckers = iterator_to_array($this->resourceCheckers); + } + + if (!\count($this->resourceCheckers)) { + return true; // shortcut - if we don't have any checkers we don't need to bother with the meta file at all + } + + $metadata = $this->getMetaFile(); + + if (!is_file($metadata)) { + return false; + } + + $meta = $this->safelyUnserialize($metadata); + + if (false === $meta) { + return false; + } + + $time = filemtime($this->file); + + foreach ($meta as $resource) { + foreach ($this->resourceCheckers as $checker) { + if (!$checker->supports($resource)) { + continue; // next checker + } + if ($checker->isFresh($resource, $time)) { + break; // no need to further check this resource + } + + return false; // cache is stale + } + // no suitable checker found, ignore this resource + } + + return true; + } + + /** + * Writes cache. + * + * @param string $content The content to write in the cache + * @param ResourceInterface[] $metadata An array of metadata + * + * @throws \RuntimeException When cache file can't be written + */ + public function write(string $content, array $metadata = null) + { + $mode = 0666; + $umask = umask(); + $filesystem = new Filesystem(); + $filesystem->dumpFile($this->file, $content); + try { + $filesystem->chmod($this->file, $mode, $umask); + } catch (IOException $e) { + // discard chmod failure (some filesystem may not support it) + } + + if (null !== $metadata) { + $filesystem->dumpFile($this->getMetaFile(), serialize($metadata)); + try { + $filesystem->chmod($this->getMetaFile(), $mode, $umask); + } catch (IOException $e) { + // discard chmod failure (some filesystem may not support it) + } + } + + if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { + @opcache_invalidate($this->file, true); + } + } + + /** + * Gets the meta file path. + */ + private function getMetaFile(): string + { + return $this->file.'.meta'; + } + + private function safelyUnserialize(string $file) + { + $meta = false; + $content = file_get_contents($file); + $signalingException = new \UnexpectedValueException(); + $prevUnserializeHandler = ini_set('unserialize_callback_func', self::class.'::handleUnserializeCallback'); + $prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler, $signalingException) { + if (__FILE__ === $file) { + throw $signalingException; + } + + return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false; + }); + + try { + $meta = unserialize($content); + } catch (\Throwable $e) { + if ($e !== $signalingException) { + throw $e; + } + } finally { + restore_error_handler(); + ini_set('unserialize_callback_func', $prevUnserializeHandler); + } + + return $meta; + } + + /** + * @internal + */ + public static function handleUnserializeCallback(string $class) + { + trigger_error('Class not found: '.$class); + } +} diff --git a/vendor/symfony/config/ResourceCheckerConfigCacheFactory.php b/vendor/symfony/config/ResourceCheckerConfigCacheFactory.php new file mode 100644 index 0000000..21b7433 --- /dev/null +++ b/vendor/symfony/config/ResourceCheckerConfigCacheFactory.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +/** + * A ConfigCacheFactory implementation that validates the + * cache with an arbitrary set of ResourceCheckers. + * + * @author Matthias Pigulla + */ +class ResourceCheckerConfigCacheFactory implements ConfigCacheFactoryInterface +{ + private $resourceCheckers = []; + + /** + * @param iterable $resourceCheckers + */ + public function __construct(iterable $resourceCheckers = []) + { + $this->resourceCheckers = $resourceCheckers; + } + + /** + * {@inheritdoc} + */ + public function cache(string $file, callable $callable) + { + $cache = new ResourceCheckerConfigCache($file, $this->resourceCheckers); + if (!$cache->isFresh()) { + $callable($cache); + } + + return $cache; + } +} diff --git a/vendor/symfony/config/ResourceCheckerInterface.php b/vendor/symfony/config/ResourceCheckerInterface.php new file mode 100644 index 0000000..6b1c6c5 --- /dev/null +++ b/vendor/symfony/config/ResourceCheckerInterface.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +use Symfony\Component\Config\Resource\ResourceInterface; + +/** + * Interface for ResourceCheckers. + * + * When a ResourceCheckerConfigCache instance is checked for freshness, all its associated + * metadata resources are passed to ResourceCheckers. The ResourceCheckers + * can then inspect the resources and decide whether the cache can be considered + * fresh or not. + * + * @author Matthias Pigulla + * @author Benjamin Klotz + */ +interface ResourceCheckerInterface +{ + /** + * Queries the ResourceChecker whether it can validate a given + * resource or not. + * + * @return bool + */ + public function supports(ResourceInterface $metadata); + + /** + * Validates the resource. + * + * @param int $timestamp The timestamp at which the cache associated with this resource was created + * + * @return bool + */ + public function isFresh(ResourceInterface $resource, int $timestamp); +} diff --git a/vendor/symfony/config/Util/Exception/InvalidXmlException.php b/vendor/symfony/config/Util/Exception/InvalidXmlException.php new file mode 100644 index 0000000..a335bbd --- /dev/null +++ b/vendor/symfony/config/Util/Exception/InvalidXmlException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Util\Exception; + +/** + * Exception class for when XML parsing with an XSD schema file path or a callable validator produces errors unrelated + * to the actual XML parsing. + * + * @author Ole Rößner + */ +class InvalidXmlException extends XmlParsingException +{ +} diff --git a/vendor/symfony/config/Util/Exception/XmlParsingException.php b/vendor/symfony/config/Util/Exception/XmlParsingException.php new file mode 100644 index 0000000..9bceed6 --- /dev/null +++ b/vendor/symfony/config/Util/Exception/XmlParsingException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Util\Exception; + +/** + * Exception class for when XML cannot be parsed properly. + * + * @author Ole Rößner + */ +class XmlParsingException extends \InvalidArgumentException +{ +} diff --git a/vendor/symfony/config/Util/XmlUtils.php b/vendor/symfony/config/Util/XmlUtils.php new file mode 100644 index 0000000..8258a06 --- /dev/null +++ b/vendor/symfony/config/Util/XmlUtils.php @@ -0,0 +1,289 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Util; + +use Symfony\Component\Config\Util\Exception\InvalidXmlException; +use Symfony\Component\Config\Util\Exception\XmlParsingException; + +/** + * XMLUtils is a bunch of utility methods to XML operations. + * + * This class contains static methods only and is not meant to be instantiated. + * + * @author Fabien Potencier + * @author Martin Hasoň + * @author Ole Rößner + */ +class XmlUtils +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Parses an XML string. + * + * @param string $content An XML string + * @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation + * + * @return \DOMDocument + * + * @throws XmlParsingException When parsing of XML file returns error + * @throws InvalidXmlException When parsing of XML with schema or callable produces any errors unrelated to the XML parsing itself + * @throws \RuntimeException When DOM extension is missing + */ + public static function parse(string $content, $schemaOrCallable = null) + { + if (!\extension_loaded('dom')) { + throw new \LogicException('Extension DOM is required.'); + } + + $internalErrors = libxml_use_internal_errors(true); + if (\LIBXML_VERSION < 20900) { + $disableEntities = libxml_disable_entity_loader(true); + } + libxml_clear_errors(); + + $dom = new \DOMDocument(); + $dom->validateOnParse = true; + if (!$dom->loadXML($content, \LIBXML_NONET | (\defined('LIBXML_COMPACT') ? \LIBXML_COMPACT : 0))) { + if (\LIBXML_VERSION < 20900) { + libxml_disable_entity_loader($disableEntities); + } + + throw new XmlParsingException(implode("\n", static::getXmlErrors($internalErrors))); + } + + $dom->normalizeDocument(); + + libxml_use_internal_errors($internalErrors); + if (\LIBXML_VERSION < 20900) { + libxml_disable_entity_loader($disableEntities); + } + + foreach ($dom->childNodes as $child) { + if (\XML_DOCUMENT_TYPE_NODE === $child->nodeType) { + throw new XmlParsingException('Document types are not allowed.'); + } + } + + if (null !== $schemaOrCallable) { + $internalErrors = libxml_use_internal_errors(true); + libxml_clear_errors(); + + $e = null; + if (\is_callable($schemaOrCallable)) { + try { + $valid = $schemaOrCallable($dom, $internalErrors); + } catch (\Exception $e) { + $valid = false; + } + } elseif (!\is_array($schemaOrCallable) && is_file((string) $schemaOrCallable)) { + $schemaSource = file_get_contents((string) $schemaOrCallable); + $valid = @$dom->schemaValidateSource($schemaSource); + } else { + libxml_use_internal_errors($internalErrors); + + throw new XmlParsingException('The schemaOrCallable argument has to be a valid path to XSD file or callable.'); + } + + if (!$valid) { + $messages = static::getXmlErrors($internalErrors); + if (empty($messages)) { + throw new InvalidXmlException('The XML is not valid.', 0, $e); + } + throw new XmlParsingException(implode("\n", $messages), 0, $e); + } + } + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return $dom; + } + + /** + * Loads an XML file. + * + * @param string $file An XML file path + * @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation + * + * @return \DOMDocument + * + * @throws \InvalidArgumentException When loading of XML file returns error + * @throws XmlParsingException When XML parsing returns any errors + * @throws \RuntimeException When DOM extension is missing + */ + public static function loadFile(string $file, $schemaOrCallable = null) + { + if (!is_file($file)) { + throw new \InvalidArgumentException(sprintf('Resource "%s" is not a file.', $file)); + } + + if (!is_readable($file)) { + throw new \InvalidArgumentException(sprintf('File "%s" is not readable.', $file)); + } + + $content = @file_get_contents($file); + + if ('' === trim($content)) { + throw new \InvalidArgumentException(sprintf('File "%s" does not contain valid XML, it is empty.', $file)); + } + + try { + return static::parse($content, $schemaOrCallable); + } catch (InvalidXmlException $e) { + throw new XmlParsingException(sprintf('The XML file "%s" is not valid.', $file), 0, $e->getPrevious()); + } + } + + /** + * Converts a \DOMElement object to a PHP array. + * + * The following rules applies during the conversion: + * + * * Each tag is converted to a key value or an array + * if there is more than one "value" + * + * * The content of a tag is set under a "value" key (bar) + * if the tag also has some nested tags + * + * * The attributes are converted to keys () + * + * * The nested-tags are converted to keys (bar) + * + * @param \DOMElement $element A \DOMElement instance + * @param bool $checkPrefix Check prefix in an element or an attribute name + * + * @return mixed + */ + public static function convertDomElementToArray(\DOMElement $element, bool $checkPrefix = true) + { + $prefix = (string) $element->prefix; + $empty = true; + $config = []; + foreach ($element->attributes as $name => $node) { + if ($checkPrefix && !\in_array((string) $node->prefix, ['', $prefix], true)) { + continue; + } + $config[$name] = static::phpize($node->value); + $empty = false; + } + + $nodeValue = false; + foreach ($element->childNodes as $node) { + if ($node instanceof \DOMText) { + if ('' !== trim($node->nodeValue)) { + $nodeValue = trim($node->nodeValue); + $empty = false; + } + } elseif ($checkPrefix && $prefix != (string) $node->prefix) { + continue; + } elseif (!$node instanceof \DOMComment) { + $value = static::convertDomElementToArray($node, $checkPrefix); + + $key = $node->localName; + if (isset($config[$key])) { + if (!\is_array($config[$key]) || !\is_int(key($config[$key]))) { + $config[$key] = [$config[$key]]; + } + $config[$key][] = $value; + } else { + $config[$key] = $value; + } + + $empty = false; + } + } + + if (false !== $nodeValue) { + $value = static::phpize($nodeValue); + if (\count($config)) { + $config['value'] = $value; + } else { + $config = $value; + } + } + + return !$empty ? $config : null; + } + + /** + * Converts an xml value to a PHP type. + * + * @param mixed $value + * + * @return mixed + */ + public static function phpize($value) + { + $value = (string) $value; + $lowercaseValue = strtolower($value); + + switch (true) { + case 'null' === $lowercaseValue: + return null; + case ctype_digit($value): + case isset($value[1]) && '-' === $value[0] && ctype_digit(substr($value, 1)): + $raw = $value; + $cast = (int) $value; + + return self::isOctal($value) ? \intval($value, 8) : (($raw === (string) $cast) ? $cast : $raw); + case 'true' === $lowercaseValue: + return true; + case 'false' === $lowercaseValue: + return false; + case isset($value[1]) && '0b' == $value[0].$value[1] && preg_match('/^0b[01]*$/', $value): + return bindec($value); + case is_numeric($value): + return '0x' === $value[0].$value[1] ? hexdec($value) : (float) $value; + case preg_match('/^0x[0-9a-f]++$/i', $value): + return hexdec($value); + case preg_match('/^[+-]?[0-9]+(\.[0-9]+)?$/', $value): + return (float) $value; + default: + return $value; + } + } + + protected static function getXmlErrors(bool $internalErrors) + { + $errors = []; + foreach (libxml_get_errors() as $error) { + $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', + \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', + $error->code, + trim($error->message), + $error->file ?: 'n/a', + $error->line, + $error->column + ); + } + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return $errors; + } + + private static function isOctal(string $str): bool + { + if ('-' === $str[0]) { + $str = substr($str, 1); + } + + return $str === '0'.decoct(\intval($str, 8)); + } +} diff --git a/vendor/symfony/config/composer.json b/vendor/symfony/config/composer.json new file mode 100644 index 0000000..b357ed3 --- /dev/null +++ b/vendor/symfony/config/composer.json @@ -0,0 +1,46 @@ +{ + "name": "symfony/config", + "type": "library", + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "conflict": { + "symfony/finder": "<4.4" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Config\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/symfony/console/Application.php b/vendor/symfony/console/Application.php new file mode 100644 index 0000000..735b1ef --- /dev/null +++ b/vendor/symfony/console/Application.php @@ -0,0 +1,1266 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Command\CompleteCommand; +use Symfony\Component\Console\Command\DumpCompletionCommand; +use Symfony\Component\Console\Command\HelpCommand; +use Symfony\Component\Console\Command\LazyCommand; +use Symfony\Component\Console\Command\ListCommand; +use Symfony\Component\Console\Command\SignalableCommandInterface; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\NamespaceNotFoundException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\DebugFormatterHelper; +use Symfony\Component\Console\Helper\FormatterHelper; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\ProcessHelper; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputAwareInterface; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SignalRegistry\SignalRegistry; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * An Application is the container for a collection of commands. + * + * It is the main entry point of a Console application. + * + * This class is optimized for a standard CLI environment. + * + * Usage: + * + * $app = new Application('myapp', '1.0 (stable)'); + * $app->add(new SimpleCommand()); + * $app->run(); + * + * @author Fabien Potencier + */ +class Application implements ResetInterface +{ + private $commands = []; + private $wantHelps = false; + private $runningCommand; + private $name; + private $version; + private $commandLoader; + private $catchExceptions = true; + private $autoExit = true; + private $definition; + private $helperSet; + private $dispatcher; + private $terminal; + private $defaultCommand; + private $singleCommand = false; + private $initialized; + private $signalRegistry; + private $signalsToDispatchEvent = []; + + public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN') + { + $this->name = $name; + $this->version = $version; + $this->terminal = new Terminal(); + $this->defaultCommand = 'list'; + if (\defined('SIGINT') && SignalRegistry::isSupported()) { + $this->signalRegistry = new SignalRegistry(); + $this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2]; + } + } + + /** + * @final + */ + public function setDispatcher(EventDispatcherInterface $dispatcher) + { + $this->dispatcher = $dispatcher; + } + + public function setCommandLoader(CommandLoaderInterface $commandLoader) + { + $this->commandLoader = $commandLoader; + } + + public function getSignalRegistry(): SignalRegistry + { + if (!$this->signalRegistry) { + throw new RuntimeException('Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + } + + return $this->signalRegistry; + } + + public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent) + { + $this->signalsToDispatchEvent = $signalsToDispatchEvent; + } + + /** + * Runs the current application. + * + * @return int 0 if everything went fine, or an error code + * + * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}. + */ + public function run(InputInterface $input = null, OutputInterface $output = null) + { + if (\function_exists('putenv')) { + @putenv('LINES='.$this->terminal->getHeight()); + @putenv('COLUMNS='.$this->terminal->getWidth()); + } + + if (null === $input) { + $input = new ArgvInput(); + } + + if (null === $output) { + $output = new ConsoleOutput(); + } + + $renderException = function (\Throwable $e) use ($output) { + if ($output instanceof ConsoleOutputInterface) { + $this->renderThrowable($e, $output->getErrorOutput()); + } else { + $this->renderThrowable($e, $output); + } + }; + if ($phpHandler = set_exception_handler($renderException)) { + restore_exception_handler(); + if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) { + $errorHandler = true; + } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) { + $phpHandler[0]->setExceptionHandler($errorHandler); + } + } + + $this->configureIO($input, $output); + + try { + $exitCode = $this->doRun($input, $output); + } catch (\Exception $e) { + if (!$this->catchExceptions) { + throw $e; + } + + $renderException($e); + + $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if (0 === $exitCode) { + $exitCode = 1; + } + } else { + $exitCode = 1; + } + } finally { + // if the exception handler changed, keep it + // otherwise, unregister $renderException + if (!$phpHandler) { + if (set_exception_handler($renderException) === $renderException) { + restore_exception_handler(); + } + restore_exception_handler(); + } elseif (!$errorHandler) { + $finalHandler = $phpHandler[0]->setExceptionHandler(null); + if ($finalHandler !== $renderException) { + $phpHandler[0]->setExceptionHandler($finalHandler); + } + } + } + + if ($this->autoExit) { + if ($exitCode > 255) { + $exitCode = 255; + } + + exit($exitCode); + } + + return $exitCode; + } + + /** + * Runs the current application. + * + * @return int 0 if everything went fine, or an error code + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + if (true === $input->hasParameterOption(['--version', '-V'], true)) { + $output->writeln($this->getLongVersion()); + + return 0; + } + + try { + // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. + $input->bind($this->getDefinition()); + } catch (ExceptionInterface $e) { + // Errors must be ignored, full binding/validation happens later when the command is known. + } + + $name = $this->getCommandName($input); + if (true === $input->hasParameterOption(['--help', '-h'], true)) { + if (!$name) { + $name = 'help'; + $input = new ArrayInput(['command_name' => $this->defaultCommand]); + } else { + $this->wantHelps = true; + } + } + + if (!$name) { + $name = $this->defaultCommand; + $definition = $this->getDefinition(); + $definition->setArguments(array_merge( + $definition->getArguments(), + [ + 'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name), + ] + )); + } + + try { + $this->runningCommand = null; + // the command name MUST be the first element of the input + $command = $this->find($name); + } catch (\Throwable $e) { + if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + + if (0 === $event->getExitCode()) { + return 0; + } + + $e = $event->getError(); + } + + throw $e; + } + + $alternative = $alternatives[0]; + + $style = new SymfonyStyle($input, $output); + $style->block(sprintf("\nCommand \"%s\" is not defined.\n", $name), null, 'error'); + if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + + return $event->getExitCode(); + } + + return 1; + } + + $command = $this->find($alternative); + } + + if ($command instanceof LazyCommand) { + $command = $command->getCommand(); + } + + $this->runningCommand = $command; + $exitCode = $this->doRunCommand($command, $input, $output); + $this->runningCommand = null; + + return $exitCode; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + } + + public function setHelperSet(HelperSet $helperSet) + { + $this->helperSet = $helperSet; + } + + /** + * Get the helper set associated with the command. + * + * @return HelperSet + */ + public function getHelperSet() + { + if (!$this->helperSet) { + $this->helperSet = $this->getDefaultHelperSet(); + } + + return $this->helperSet; + } + + public function setDefinition(InputDefinition $definition) + { + $this->definition = $definition; + } + + /** + * Gets the InputDefinition related to this Application. + * + * @return InputDefinition + */ + public function getDefinition() + { + if (!$this->definition) { + $this->definition = $this->getDefaultInputDefinition(); + } + + if ($this->singleCommand) { + $inputDefinition = $this->definition; + $inputDefinition->setArguments(); + + return $inputDefinition; + } + + return $this->definition; + } + + /** + * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ( + CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() + && 'command' === $input->getCompletionName() + ) { + $suggestions->suggestValues(array_filter(array_map(function (Command $command) { + return $command->isHidden() ? null : $command->getName(); + }, $this->all()))); + + return; + } + + if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) { + $suggestions->suggestOptions($this->getDefinition()->getOptions()); + + return; + } + } + + /** + * Gets the help message. + * + * @return string + */ + public function getHelp() + { + return $this->getLongVersion(); + } + + /** + * Gets whether to catch exceptions or not during commands execution. + * + * @return bool + */ + public function areExceptionsCaught() + { + return $this->catchExceptions; + } + + /** + * Sets whether to catch exceptions or not during commands execution. + */ + public function setCatchExceptions(bool $boolean) + { + $this->catchExceptions = $boolean; + } + + /** + * Gets whether to automatically exit after a command execution or not. + * + * @return bool + */ + public function isAutoExitEnabled() + { + return $this->autoExit; + } + + /** + * Sets whether to automatically exit after a command execution or not. + */ + public function setAutoExit(bool $boolean) + { + $this->autoExit = $boolean; + } + + /** + * Gets the name of the application. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the application name. + **/ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * Gets the application version. + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Sets the application version. + */ + public function setVersion(string $version) + { + $this->version = $version; + } + + /** + * Returns the long version of the application. + * + * @return string + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName()) { + if ('UNKNOWN' !== $this->getVersion()) { + return sprintf('%s %s', $this->getName(), $this->getVersion()); + } + + return $this->getName(); + } + + return 'Console Tool'; + } + + /** + * Registers a new command. + * + * @return Command + */ + public function register(string $name) + { + return $this->add(new Command($name)); + } + + /** + * Adds an array of command objects. + * + * If a Command is not enabled it will not be added. + * + * @param Command[] $commands An array of commands + */ + public function addCommands(array $commands) + { + foreach ($commands as $command) { + $this->add($command); + } + } + + /** + * Adds a command object. + * + * If a command with the same name already exists, it will be overridden. + * If the command is not enabled it will not be added. + * + * @return Command|null + */ + public function add(Command $command) + { + $this->init(); + + $command->setApplication($this); + + if (!$command->isEnabled()) { + $command->setApplication(null); + + return null; + } + + if (!$command instanceof LazyCommand) { + // Will throw if the command is not correctly initialized. + $command->getDefinition(); + } + + if (!$command->getName()) { + throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command))); + } + + $this->commands[$command->getName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = $command; + } + + return $command; + } + + /** + * Returns a registered command by name or alias. + * + * @return Command + * + * @throws CommandNotFoundException When given command name does not exist + */ + public function get(string $name) + { + $this->init(); + + if (!$this->has($name)) { + throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); + } + + // When the command has a different name than the one used at the command loader level + if (!isset($this->commands[$name])) { + throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name)); + } + + $command = $this->commands[$name]; + + if ($this->wantHelps) { + $this->wantHelps = false; + + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * Returns true if the command exists, false otherwise. + * + * @return bool + */ + public function has(string $name) + { + $this->init(); + + return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name))); + } + + /** + * Returns an array of all unique namespaces used by currently registered commands. + * + * It does not return the global namespace which always exists. + * + * @return string[] + */ + public function getNamespaces() + { + $namespaces = []; + foreach ($this->all() as $command) { + if ($command->isHidden()) { + continue; + } + + $namespaces[] = $this->extractAllNamespaces($command->getName()); + + foreach ($command->getAliases() as $alias) { + $namespaces[] = $this->extractAllNamespaces($alias); + } + } + + return array_values(array_unique(array_filter(array_merge([], ...$namespaces)))); + } + + /** + * Finds a registered namespace by a name or an abbreviation. + * + * @return string + * + * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous + */ + public function findNamespace(string $namespace) + { + $allNamespaces = $this->getNamespaces(); + $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'; + $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); + + if (empty($namespaces)) { + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { + if (1 == \count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new NamespaceNotFoundException($message, $alternatives); + } + + $exact = \in_array($namespace, $namespaces, true); + if (\count($namespaces) > 1 && !$exact) { + throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); + } + + return $exact ? $namespace : reset($namespaces); + } + + /** + * Finds a command by name or alias. + * + * Contrary to get, this command tries to find the best + * match if you give it an abbreviation of a name or alias. + * + * @return Command + * + * @throws CommandNotFoundException When command name is incorrect or ambiguous + */ + public function find(string $name) + { + $this->init(); + + $aliases = []; + + foreach ($this->commands as $command) { + foreach ($command->getAliases() as $alias) { + if (!$this->has($alias)) { + $this->commands[$alias] = $command; + } + } + } + + if ($this->has($name)) { + return $this->get($name); + } + + $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands); + $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*'; + $commands = preg_grep('{^'.$expr.'}', $allCommands); + + if (empty($commands)) { + $commands = preg_grep('{^'.$expr.'}i', $allCommands); + } + + // if no commands matched or we just matched namespaces + if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + // check if a namespace exists and contains commands + $this->findNamespace(substr($name, 0, $pos)); + } + + $message = sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternatives($name, $allCommands)) { + // remove hidden commands + $alternatives = array_filter($alternatives, function ($name) { + return !$this->get($name)->isHidden(); + }); + + if (1 == \count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new CommandNotFoundException($message, array_values($alternatives)); + } + + // filter out aliases for commands which are already on the list + if (\count($commands) > 1) { + $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands; + $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) { + if (!$commandList[$nameOrAlias] instanceof Command) { + $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias); + } + + $commandName = $commandList[$nameOrAlias]->getName(); + + $aliases[$nameOrAlias] = $commandName; + + return $commandName === $nameOrAlias || !\in_array($commandName, $commands); + })); + } + + if (\count($commands) > 1) { + $usableWidth = $this->terminal->getWidth() - 10; + $abbrevs = array_values($commands); + $maxLen = 0; + foreach ($abbrevs as $abbrev) { + $maxLen = max(Helper::width($abbrev), $maxLen); + } + $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) { + if ($commandList[$cmd]->isHidden()) { + unset($commands[array_search($cmd, $commands)]); + + return false; + } + + $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); + + return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; + }, array_values($commands)); + + if (\count($commands) > 1) { + $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs)); + + throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands)); + } + } + + $command = $this->get(reset($commands)); + + if ($command->isHidden()) { + throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); + } + + return $command; + } + + /** + * Gets the commands (registered in the given namespace if provided). + * + * The array keys are the full names and the values the command instances. + * + * @return Command[] + */ + public function all(string $namespace = null) + { + $this->init(); + + if (null === $namespace) { + if (!$this->commandLoader) { + return $this->commands; + } + + $commands = $this->commands; + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name]) && $this->has($name)) { + $commands[$name] = $this->get($name); + } + } + + return $commands; + } + + $commands = []; + foreach ($this->commands as $name => $command) { + if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { + $commands[$name] = $command; + } + } + + if ($this->commandLoader) { + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) { + $commands[$name] = $this->get($name); + } + } + } + + return $commands; + } + + /** + * Returns an array of possible abbreviations given a set of names. + * + * @return string[][] + */ + public static function getAbbreviations(array $names) + { + $abbrevs = []; + foreach ($names as $name) { + for ($len = \strlen($name); $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + $abbrevs[$abbrev][] = $name; + } + } + + return $abbrevs; + } + + public function renderThrowable(\Throwable $e, OutputInterface $output): void + { + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + + $this->doRenderThrowable($e, $output); + + if (null !== $this->runningCommand) { + $output->writeln(sprintf('%s', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET); + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + } + } + + protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void + { + do { + $message = trim($e->getMessage()); + if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $class = get_debug_type($e); + $title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : ''); + $len = Helper::width($title); + } else { + $len = 0; + } + + if (str_contains($message, "@anonymous\0")) { + $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { + return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; + }, $message); + } + + $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX; + $lines = []; + foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { + // pre-format lines to get the right string length + $lineLength = Helper::width($line) + 4; + $lines[] = [$line, $lineLength]; + + $len = max($lineLength, $len); + } + } + + $messages = []; + if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $messages[] = sprintf('%s', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a'))); + } + $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); + if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::width($title)))); + } + foreach ($lines as $line) { + $messages[] = sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1])); + } + $messages[] = $emptyLine; + $messages[] = ''; + + $output->writeln($messages, OutputInterface::VERBOSITY_QUIET); + + if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $output->writeln('Exception trace:', OutputInterface::VERBOSITY_QUIET); + + // exception related properties + $trace = $e->getTrace(); + + array_unshift($trace, [ + 'function' => '', + 'file' => $e->getFile() ?: 'n/a', + 'line' => $e->getLine() ?: 'n/a', + 'args' => [], + ]); + + for ($i = 0, $count = \count($trace); $i < $count; ++$i) { + $class = $trace[$i]['class'] ?? ''; + $type = $trace[$i]['type'] ?? ''; + $function = $trace[$i]['function'] ?? ''; + $file = $trace[$i]['file'] ?? 'n/a'; + $line = $trace[$i]['line'] ?? 'n/a'; + + $output->writeln(sprintf(' %s%s at %s:%s', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET); + } + + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + } + } while ($e = $e->getPrevious()); + } + + /** + * Configures the input and output instances based on the user arguments and options. + */ + protected function configureIO(InputInterface $input, OutputInterface $output) + { + if (true === $input->hasParameterOption(['--ansi'], true)) { + $output->setDecorated(true); + } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) { + $output->setDecorated(false); + } + + if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) { + $input->setInteractive(false); + } + + switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) { + case -1: $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); break; + case 1: $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); break; + case 2: $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); break; + case 3: $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); break; + default: $shellVerbosity = 0; break; + } + + if (true === $input->hasParameterOption(['--quiet', '-q'], true)) { + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + $shellVerbosity = -1; + } else { + if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) { + $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + $shellVerbosity = 3; + } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) { + $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); + $shellVerbosity = 2; + } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) { + $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); + $shellVerbosity = 1; + } + } + + if (-1 === $shellVerbosity) { + $input->setInteractive(false); + } + + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY='.$shellVerbosity); + } + $_ENV['SHELL_VERBOSITY'] = $shellVerbosity; + $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity; + } + + /** + * Runs the current command. + * + * If an event dispatcher has been attached to the application, + * events are also dispatched during the life-cycle of the command. + * + * @return int 0 if everything went fine, or an error code + */ + protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) + { + foreach ($command->getHelperSet() as $helper) { + if ($helper instanceof InputAwareInterface) { + $helper->setInput($input); + } + } + + if ($command instanceof SignalableCommandInterface && ($this->signalsToDispatchEvent || $command->getSubscribedSignals())) { + if (!$this->signalRegistry) { + throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + } + + if ($this->dispatcher) { + foreach ($this->signalsToDispatchEvent as $signal) { + $event = new ConsoleSignalEvent($command, $input, $output, $signal); + + $this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) { + $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL); + + // No more handlers, we try to simulate PHP default behavior + if (!$hasNext) { + if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) { + exit(0); + } + } + }); + } + } + + foreach ($command->getSubscribedSignals() as $signal) { + $this->signalRegistry->register($signal, [$command, 'handleSignal']); + } + } + + if (null === $this->dispatcher) { + return $command->run($input, $output); + } + + // bind before the console.command event, so the listeners have access to input options/arguments + try { + $command->mergeApplicationDefinition(); + $input->bind($command->getDefinition()); + } catch (ExceptionInterface $e) { + // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition + } + + $event = new ConsoleCommandEvent($command, $input, $output); + $e = null; + + try { + $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND); + + if ($event->commandShouldRun()) { + $exitCode = $command->run($input, $output); + } else { + $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED; + } + } catch (\Throwable $e) { + $event = new ConsoleErrorEvent($input, $output, $e, $command); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + $e = $event->getError(); + + if (0 === $exitCode = $event->getExitCode()) { + $e = null; + } + } + + $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); + $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE); + + if (null !== $e) { + throw $e; + } + + return $event->getExitCode(); + } + + /** + * Gets the name of the command based on input. + * + * @return string|null + */ + protected function getCommandName(InputInterface $input) + { + return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument(); + } + + /** + * Gets the default input definition. + * + * @return InputDefinition + */ + protected function getDefaultInputDefinition() + { + return new InputDefinition([ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the '.$this->defaultCommand.' command'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), + new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), + new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), + ]); + } + + /** + * Gets the default commands that should always be available. + * + * @return Command[] + */ + protected function getDefaultCommands() + { + return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; + } + + /** + * Gets the default helper set with the helpers that should always be available. + * + * @return HelperSet + */ + protected function getDefaultHelperSet() + { + return new HelperSet([ + new FormatterHelper(), + new DebugFormatterHelper(), + new ProcessHelper(), + new QuestionHelper(), + ]); + } + + /** + * Returns abbreviated suggestions in string format. + */ + private function getAbbreviationSuggestions(array $abbrevs): string + { + return ' '.implode("\n ", $abbrevs); + } + + /** + * Returns the namespace part of the command name. + * + * This method is not part of public API and should not be used directly. + * + * @return string + */ + public function extractNamespace(string $name, int $limit = null) + { + $parts = explode(':', $name, -1); + + return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit)); + } + + /** + * Finds alternative of $name among $collection, + * if nothing is found in $collection, try in $abbrevs. + * + * @return string[] + */ + private function findAlternatives(string $name, iterable $collection): array + { + $threshold = 1e3; + $alternatives = []; + + $collectionParts = []; + foreach ($collection as $item) { + $collectionParts[$item] = explode(':', $item); + } + + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } + } + } + + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); + ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); + + return array_keys($alternatives); + } + + /** + * Sets the default Command name. + * + * @return $this + */ + public function setDefaultCommand(string $commandName, bool $isSingleCommand = false) + { + $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0]; + + if ($isSingleCommand) { + // Ensure the command exist + $this->find($commandName); + + $this->singleCommand = true; + } + + return $this; + } + + /** + * @internal + */ + public function isSingleCommand(): bool + { + return $this->singleCommand; + } + + private function splitStringByWidth(string $string, int $width): array + { + // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. + // additionally, array_slice() is not enough as some character has doubled width. + // we need a function to split string not by character count but by string width + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = []; + $line = ''; + + $offset = 0; + while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) { + $offset += \strlen($m[0]); + + foreach (preg_split('//u', $m[0]) as $char) { + // test if $char could be appended to current line + if (mb_strwidth($line.$char, 'utf8') <= $width) { + $line .= $char; + continue; + } + // if not, push current line to array and make new line + $lines[] = str_pad($line, $width); + $line = $char; + } + } + + $lines[] = \count($lines) ? str_pad($line, $width) : $line; + + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } + + /** + * Returns all namespaces of the command name. + * + * @return string[] + */ + private function extractAllNamespaces(string $name): array + { + // -1 as third argument is needed to skip the command short name when exploding + $parts = explode(':', $name, -1); + $namespaces = []; + + foreach ($parts as $part) { + if (\count($namespaces)) { + $namespaces[] = end($namespaces).':'.$part; + } else { + $namespaces[] = $part; + } + } + + return $namespaces; + } + + private function init() + { + if ($this->initialized) { + return; + } + $this->initialized = true; + + foreach ($this->getDefaultCommands() as $command) { + $this->add($command); + } + } +} diff --git a/vendor/symfony/console/Attribute/AsCommand.php b/vendor/symfony/console/Attribute/AsCommand.php new file mode 100644 index 0000000..b337f54 --- /dev/null +++ b/vendor/symfony/console/Attribute/AsCommand.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +/** + * Service tag to autoconfigure commands. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsCommand +{ + public function __construct( + public string $name, + public ?string $description = null, + array $aliases = [], + bool $hidden = false, + ) { + if (!$hidden && !$aliases) { + return; + } + + $name = explode('|', $name); + $name = array_merge($name, $aliases); + + if ($hidden && '' !== $name[0]) { + array_unshift($name, ''); + } + + $this->name = implode('|', $name); + } +} diff --git a/vendor/symfony/console/CHANGELOG.md b/vendor/symfony/console/CHANGELOG.md new file mode 100644 index 0000000..6662dd1 --- /dev/null +++ b/vendor/symfony/console/CHANGELOG.md @@ -0,0 +1,217 @@ +CHANGELOG +========= + +5.4 +--- + + * Add `TesterTrait::assertCommandIsSuccessful()` to test command + * Deprecate `HelperSet::setCommand()` and `getCommand()` without replacement + +5.3 +--- + + * Add `GithubActionReporter` to render annotations in a Github Action + * Add `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options + * Add the `Command::$defaultDescription` static property and the `description` attribute + on the `console.command` tag to allow the `list` command to instantiate commands lazily + * Add option `--short` to the `list` command + * Add support for bright colors + * Add `#[AsCommand]` attribute for declaring commands on PHP 8 + * Add `Helper::width()` and `Helper::length()` + * The `--ansi` and `--no-ansi` options now default to `null`. + +5.2.0 +----- + + * Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester` + * added support for multiline responses to questions through `Question::setMultiline()` + and `Question::isMultiline()` + * Added `SignalRegistry` class to stack signals handlers + * Added support for signals: + * Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods + * Added `SignalableCommandInterface` interface + * Added `TableCellStyle` class to customize table cell + * Removed `php ` prefix invocation from help messages. + +5.1.0 +----- + + * `Command::setHidden()` is final since Symfony 5.1 + * Add `SingleCommandApplication` + * Add `Cursor` class + +5.0.0 +----- + + * removed support for finding hidden commands using an abbreviation, use the full name instead + * removed `TableStyle::setCrossingChar()` method in favor of `TableStyle::setDefaultCrossingChar()` + * removed `TableStyle::setHorizontalBorderChar()` method in favor of `TableStyle::setDefaultCrossingChars()` + * removed `TableStyle::getHorizontalBorderChar()` method in favor of `TableStyle::getBorderChars()` + * removed `TableStyle::setVerticalBorderChar()` method in favor of `TableStyle::setVerticalBorderChars()` + * removed `TableStyle::getVerticalBorderChar()` method in favor of `TableStyle::getBorderChars()` + * removed support for returning `null` from `Command::execute()`, return `0` instead + * `ProcessHelper::run()` accepts only `array|Symfony\Component\Process\Process` for its `command` argument + * `Application::setDispatcher` accepts only `Symfony\Contracts\EventDispatcher\EventDispatcherInterface` + for its `dispatcher` argument + * renamed `Application::renderException()` and `Application::doRenderException()` + to `renderThrowable()` and `doRenderThrowable()` respectively. + +4.4.0 +----- + + * deprecated finding hidden commands using an abbreviation, use the full name instead + * added `Question::setTrimmable` default to true to allow the answer to be trimmed + * added method `minSecondsBetweenRedraws()` and `maxSecondsBetweenRedraws()` on `ProgressBar` + * `Application` implements `ResetInterface` + * marked all dispatched event classes as `@final` + * added support for displaying table horizontally + * deprecated returning `null` from `Command::execute()`, return `0` instead + * Deprecated the `Application::renderException()` and `Application::doRenderException()` methods, + use `renderThrowable()` and `doRenderThrowable()` instead. + * added support for the `NO_COLOR` env var (https://no-color.org/) + +4.3.0 +----- + + * added support for hyperlinks + * added `ProgressBar::iterate()` method that simplify updating the progress bar when iterating + * added `Question::setAutocompleterCallback()` to provide a callback function + that dynamically generates suggestions as the user types + +4.2.0 +----- + + * allowed passing commands as `[$process, 'ENV_VAR' => 'value']` to + `ProcessHelper::run()` to pass environment variables + * deprecated passing a command as a string to `ProcessHelper::run()`, + pass it the command as an array of its arguments instead + * made the `ProcessHelper` class final + * added `WrappableOutputFormatterInterface::formatAndWrap()` (implemented in `OutputFormatter`) + * added `capture_stderr_separately` option to `CommandTester::execute()` + +4.1.0 +----- + + * added option to run suggested command if command is not found and only 1 alternative is available + * added option to modify console output and print multiple modifiable sections + * added support for iterable messages in output `write` and `writeln` methods + +4.0.0 +----- + + * `OutputFormatter` throws an exception when unknown options are used + * removed `QuestionHelper::setInputStream()/getInputStream()` + * removed `Application::getTerminalWidth()/getTerminalHeight()` and + `Application::setTerminalDimensions()/getTerminalDimensions()` + * removed `ConsoleExceptionEvent` + * removed `ConsoleEvents::EXCEPTION` + +3.4.0 +----- + + * added `SHELL_VERBOSITY` env var to control verbosity + * added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11 + `ContainerCommandLoader` for commands lazy-loading + * added a case-insensitive command name matching fallback + * added static `Command::$defaultName/getDefaultName()`, allowing for + commands to be registered at compile time in the application command loader. + Setting the `$defaultName` property avoids the need for filling the `command` + attribute on the `console.command` tag when using `AddConsoleCommandPass`. + +3.3.0 +----- + + * added `ExceptionListener` + * added `AddConsoleCommandPass` (originally in FrameworkBundle) + * [BC BREAK] `Input::getOption()` no longer returns the default value for options + with value optional explicitly passed empty + * added console.error event to catch exceptions thrown by other listeners + * deprecated console.exception event in favor of console.error + * added ability to handle `CommandNotFoundException` through the + `console.error` event + * deprecated default validation in `SymfonyQuestionHelper::ask` + +3.2.0 +------ + + * added `setInputs()` method to CommandTester for ease testing of commands expecting inputs + * added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface) + * added StreamableInputInterface + * added LockableTrait + +3.1.0 +----- + + * added truncate method to FormatterHelper + * added setColumnWidth(s) method to Table + +2.8.3 +----- + + * remove readline support from the question helper as it caused issues + +2.8.0 +----- + + * use readline for user input in the question helper when available to allow + the use of arrow keys + +2.6.0 +----- + + * added a Process helper + * added a DebugFormatter helper + +2.5.0 +----- + + * deprecated the dialog helper (use the question helper instead) + * deprecated TableHelper in favor of Table + * deprecated ProgressHelper in favor of ProgressBar + * added ConsoleLogger + * added a question helper + * added a way to set the process name of a command + * added a way to set a default command instead of `ListCommand` + +2.4.0 +----- + + * added a way to force terminal dimensions + * added a convenient method to detect verbosity level + * [BC BREAK] made descriptors use output instead of returning a string + +2.3.0 +----- + + * added multiselect support to the select dialog helper + * added Table Helper for tabular data rendering + * added support for events in `Application` + * added a way to normalize EOLs in `ApplicationTester::getDisplay()` and `CommandTester::getDisplay()` + * added a way to set the progress bar progress via the `setCurrent` method + * added support for multiple InputOption shortcuts, written as `'-a|-b|-c'` + * added two additional verbosity levels, VERBOSITY_VERY_VERBOSE and VERBOSITY_DEBUG + +2.2.0 +----- + + * added support for colorization on Windows via ConEmu + * add a method to Dialog Helper to ask for a question and hide the response + * added support for interactive selections in console (DialogHelper::select()) + * added support for autocompletion as you type in Dialog Helper + +2.1.0 +----- + + * added ConsoleOutputInterface + * added the possibility to disable a command (Command::isEnabled()) + * added suggestions when a command does not exist + * added a --raw option to the list command + * added support for STDERR in the console output class (errors are now sent + to STDERR) + * made the defaults (helper set, commands, input definition) in Application + more easily customizable + * added support for the shell even if readline is not available + * added support for process isolation in Symfony shell via + `--process-isolation` switch + * added support for `--`, which disables options parsing after that point + (tokens will be parsed as arguments) diff --git a/vendor/symfony/console/CI/GithubActionReporter.php b/vendor/symfony/console/CI/GithubActionReporter.php new file mode 100644 index 0000000..a15c1ff --- /dev/null +++ b/vendor/symfony/console/CI/GithubActionReporter.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CI; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Utility class for Github actions. + * + * @author Maxime Steinhausser + */ +class GithubActionReporter +{ + private $output; + + /** + * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85 + */ + private const ESCAPED_DATA = [ + '%' => '%25', + "\r" => '%0D', + "\n" => '%0A', + ]; + + /** + * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94 + */ + private const ESCAPED_PROPERTIES = [ + '%' => '%25', + "\r" => '%0D', + "\n" => '%0A', + ':' => '%3A', + ',' => '%2C', + ]; + + public function __construct(OutputInterface $output) + { + $this->output = $output; + } + + public static function isGithubActionEnvironment(): bool + { + return false !== getenv('GITHUB_ACTIONS'); + } + + /** + * Output an error using the Github annotations format. + * + * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message + */ + public function error(string $message, string $file = null, int $line = null, int $col = null): void + { + $this->log('error', $message, $file, $line, $col); + } + + /** + * Output a warning using the Github annotations format. + * + * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message + */ + public function warning(string $message, string $file = null, int $line = null, int $col = null): void + { + $this->log('warning', $message, $file, $line, $col); + } + + /** + * Output a debug log using the Github annotations format. + * + * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message + */ + public function debug(string $message, string $file = null, int $line = null, int $col = null): void + { + $this->log('debug', $message, $file, $line, $col); + } + + private function log(string $type, string $message, string $file = null, int $line = null, int $col = null): void + { + // Some values must be encoded. + $message = strtr($message, self::ESCAPED_DATA); + + if (!$file) { + // No file provided, output the message solely: + $this->output->writeln(sprintf('::%s::%s', $type, $message)); + + return; + } + + $this->output->writeln(sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message)); + } +} diff --git a/vendor/symfony/console/Color.php b/vendor/symfony/console/Color.php new file mode 100644 index 0000000..22a4ce9 --- /dev/null +++ b/vendor/symfony/console/Color.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Fabien Potencier + */ +final class Color +{ + private const COLORS = [ + 'black' => 0, + 'red' => 1, + 'green' => 2, + 'yellow' => 3, + 'blue' => 4, + 'magenta' => 5, + 'cyan' => 6, + 'white' => 7, + 'default' => 9, + ]; + + private const BRIGHT_COLORS = [ + 'gray' => 0, + 'bright-red' => 1, + 'bright-green' => 2, + 'bright-yellow' => 3, + 'bright-blue' => 4, + 'bright-magenta' => 5, + 'bright-cyan' => 6, + 'bright-white' => 7, + ]; + + private const AVAILABLE_OPTIONS = [ + 'bold' => ['set' => 1, 'unset' => 22], + 'underscore' => ['set' => 4, 'unset' => 24], + 'blink' => ['set' => 5, 'unset' => 25], + 'reverse' => ['set' => 7, 'unset' => 27], + 'conceal' => ['set' => 8, 'unset' => 28], + ]; + + private $foreground; + private $background; + private $options = []; + + public function __construct(string $foreground = '', string $background = '', array $options = []) + { + $this->foreground = $this->parseColor($foreground); + $this->background = $this->parseColor($background, true); + + foreach ($options as $option) { + if (!isset(self::AVAILABLE_OPTIONS[$option])) { + throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS)))); + } + + $this->options[$option] = self::AVAILABLE_OPTIONS[$option]; + } + } + + public function apply(string $text): string + { + return $this->set().$text.$this->unset(); + } + + public function set(): string + { + $setCodes = []; + if ('' !== $this->foreground) { + $setCodes[] = $this->foreground; + } + if ('' !== $this->background) { + $setCodes[] = $this->background; + } + foreach ($this->options as $option) { + $setCodes[] = $option['set']; + } + if (0 === \count($setCodes)) { + return ''; + } + + return sprintf("\033[%sm", implode(';', $setCodes)); + } + + public function unset(): string + { + $unsetCodes = []; + if ('' !== $this->foreground) { + $unsetCodes[] = 39; + } + if ('' !== $this->background) { + $unsetCodes[] = 49; + } + foreach ($this->options as $option) { + $unsetCodes[] = $option['unset']; + } + if (0 === \count($unsetCodes)) { + return ''; + } + + return sprintf("\033[%sm", implode(';', $unsetCodes)); + } + + private function parseColor(string $color, bool $background = false): string + { + if ('' === $color) { + return ''; + } + + if ('#' === $color[0]) { + $color = substr($color, 1); + + if (3 === \strlen($color)) { + $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2]; + } + + if (6 !== \strlen($color)) { + throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color)); + } + + return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color)); + } + + if (isset(self::COLORS[$color])) { + return ($background ? '4' : '3').self::COLORS[$color]; + } + + if (isset(self::BRIGHT_COLORS[$color])) { + return ($background ? '10' : '9').self::BRIGHT_COLORS[$color]; + } + + throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS))))); + } + + private function convertHexColorToAnsi(int $color): string + { + $r = ($color >> 16) & 255; + $g = ($color >> 8) & 255; + $b = $color & 255; + + // see https://github.com/termstandard/colors/ for more information about true color support + if ('truecolor' !== getenv('COLORTERM')) { + return (string) $this->degradeHexColorToAnsi($r, $g, $b); + } + + return sprintf('8;2;%d;%d;%d', $r, $g, $b); + } + + private function degradeHexColorToAnsi(int $r, int $g, int $b): int + { + if (0 === round($this->getSaturation($r, $g, $b) / 50)) { + return 0; + } + + return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255); + } + + private function getSaturation(int $r, int $g, int $b): int + { + $r = $r / 255; + $g = $g / 255; + $b = $b / 255; + $v = max($r, $g, $b); + + if (0 === $diff = $v - min($r, $g, $b)) { + return 0; + } + + return (int) $diff * 100 / $v; + } +} diff --git a/vendor/symfony/console/Command/Command.php b/vendor/symfony/console/Command/Command.php new file mode 100644 index 0000000..146f601 --- /dev/null +++ b/vendor/symfony/console/Command/Command.php @@ -0,0 +1,710 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Base class for all commands. + * + * @author Fabien Potencier + */ +class Command +{ + // see https://tldp.org/LDP/abs/html/exitcodes.html + public const SUCCESS = 0; + public const FAILURE = 1; + public const INVALID = 2; + + /** + * @var string|null The default command name + */ + protected static $defaultName; + + /** + * @var string|null The default command description + */ + protected static $defaultDescription; + + private $application; + private $name; + private $processTitle; + private $aliases = []; + private $definition; + private $hidden = false; + private $help = ''; + private $description = ''; + private $fullDefinition; + private $ignoreValidationErrors = false; + private $code; + private $synopsis = []; + private $usages = []; + private $helperSet; + + /** + * @return string|null + */ + public static function getDefaultName() + { + $class = static::class; + + if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { + return $attribute[0]->newInstance()->name; + } + + $r = new \ReflectionProperty($class, 'defaultName'); + + return $class === $r->class ? static::$defaultName : null; + } + + public static function getDefaultDescription(): ?string + { + $class = static::class; + + if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { + return $attribute[0]->newInstance()->description; + } + + $r = new \ReflectionProperty($class, 'defaultDescription'); + + return $class === $r->class ? static::$defaultDescription : null; + } + + /** + * @param string|null $name The name of the command; passing null means it must be set in configure() + * + * @throws LogicException When the command name is empty + */ + public function __construct(string $name = null) + { + $this->definition = new InputDefinition(); + + if (null === $name && null !== $name = static::getDefaultName()) { + $aliases = explode('|', $name); + + if ('' === $name = array_shift($aliases)) { + $this->setHidden(true); + $name = array_shift($aliases); + } + + $this->setAliases($aliases); + } + + if (null !== $name) { + $this->setName($name); + } + + if ('' === $this->description) { + $this->setDescription(static::getDefaultDescription() ?? ''); + } + + $this->configure(); + } + + /** + * Ignores validation errors. + * + * This is mainly useful for the help command. + */ + public function ignoreValidationErrors() + { + $this->ignoreValidationErrors = true; + } + + public function setApplication(Application $application = null) + { + $this->application = $application; + if ($application) { + $this->setHelperSet($application->getHelperSet()); + } else { + $this->helperSet = null; + } + + $this->fullDefinition = null; + } + + public function setHelperSet(HelperSet $helperSet) + { + $this->helperSet = $helperSet; + } + + /** + * Gets the helper set. + * + * @return HelperSet|null + */ + public function getHelperSet() + { + return $this->helperSet; + } + + /** + * Gets the application instance for this command. + * + * @return Application|null + */ + public function getApplication() + { + return $this->application; + } + + /** + * Checks whether the command is enabled or not in the current environment. + * + * Override this to check for x or y and return false if the command cannot + * run properly under the current conditions. + * + * @return bool + */ + public function isEnabled() + { + return true; + } + + /** + * Configures the current command. + */ + protected function configure() + { + } + + /** + * Executes the current command. + * + * This method is not abstract because you can use this class + * as a concrete class. In this case, instead of defining the + * execute() method, you set the code to execute by passing + * a Closure to the setCode() method. + * + * @return int 0 if everything went fine, or an exit code + * + * @throws LogicException When this abstract method is not implemented + * + * @see setCode() + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + throw new LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * Interacts with the user. + * + * This method is executed before the InputDefinition is validated. + * This means that this is the only place where the command can + * interactively ask for values of missing required arguments. + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + } + + /** + * Initializes the command after the input has been bound and before the input + * is validated. + * + * This is mainly useful when a lot of commands extends one main command + * where some things need to be initialized based on the input arguments and options. + * + * @see InputInterface::bind() + * @see InputInterface::validate() + */ + protected function initialize(InputInterface $input, OutputInterface $output) + { + } + + /** + * Runs the command. + * + * The code to execute is either defined directly with the + * setCode() method or by overriding the execute() method + * in a sub-class. + * + * @return int The command exit code + * + * @throws \Exception When binding input fails. Bypass this by calling {@link ignoreValidationErrors()}. + * + * @see setCode() + * @see execute() + */ + public function run(InputInterface $input, OutputInterface $output) + { + // add the application arguments and options + $this->mergeApplicationDefinition(); + + // bind the input against the command specific arguments/options + try { + $input->bind($this->getDefinition()); + } catch (ExceptionInterface $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if (null !== $this->processTitle) { + if (\function_exists('cli_set_process_title')) { + if (!@cli_set_process_title($this->processTitle)) { + if ('Darwin' === \PHP_OS) { + $output->writeln('Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.', OutputInterface::VERBOSITY_VERY_VERBOSE); + } else { + cli_set_process_title($this->processTitle); + } + } + } elseif (\function_exists('setproctitle')) { + setproctitle($this->processTitle); + } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) { + $output->writeln('Install the proctitle PECL to be able to change the process title.'); + } + } + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + // The command name argument is often omitted when a command is executed directly with its run() method. + // It would fail the validation if we didn't make sure the command argument is present, + // since it's required by the application. + if ($input->hasArgument('command') && null === $input->getArgument('command')) { + $input->setArgument('command', $this->getName()); + } + + $input->validate(); + + if ($this->code) { + $statusCode = ($this->code)($input, $output); + } else { + $statusCode = $this->execute($input, $output); + + if (!\is_int($statusCode)) { + throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode))); + } + } + + return is_numeric($statusCode) ? (int) $statusCode : 0; + } + + /** + * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + } + + /** + * Sets the code to execute when running this command. + * + * If this method is used, it overrides the code defined + * in the execute() method. + * + * @param callable $code A callable(InputInterface $input, OutputInterface $output) + * + * @return $this + * + * @throws InvalidArgumentException + * + * @see execute() + */ + public function setCode(callable $code) + { + if ($code instanceof \Closure) { + $r = new \ReflectionFunction($code); + if (null === $r->getClosureThis()) { + set_error_handler(static function () {}); + try { + if ($c = \Closure::bind($code, $this)) { + $code = $c; + } + } finally { + restore_error_handler(); + } + } + } + + $this->code = $code; + + return $this; + } + + /** + * Merges the application definition with the command definition. + * + * This method is not part of public API and should not be used directly. + * + * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments + * + * @internal + */ + public function mergeApplicationDefinition(bool $mergeArgs = true) + { + if (null === $this->application) { + return; + } + + $this->fullDefinition = new InputDefinition(); + $this->fullDefinition->setOptions($this->definition->getOptions()); + $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions()); + + if ($mergeArgs) { + $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments()); + $this->fullDefinition->addArguments($this->definition->getArguments()); + } else { + $this->fullDefinition->setArguments($this->definition->getArguments()); + } + } + + /** + * Sets an array of argument and option instances. + * + * @param array|InputDefinition $definition An array of argument and option instances or a definition instance + * + * @return $this + */ + public function setDefinition($definition) + { + if ($definition instanceof InputDefinition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->fullDefinition = null; + + return $this; + } + + /** + * Gets the InputDefinition attached to this Command. + * + * @return InputDefinition + */ + public function getDefinition() + { + return $this->fullDefinition ?? $this->getNativeDefinition(); + } + + /** + * Gets the InputDefinition to be used to create representations of this Command. + * + * Can be overridden to provide the original command representation when it would otherwise + * be changed by merging with the application InputDefinition. + * + * This method is not part of public API and should not be used directly. + * + * @return InputDefinition + */ + public function getNativeDefinition() + { + if (null === $this->definition) { + throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); + } + + return $this->definition; + } + + /** + * Adds an argument. + * + * @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL + * @param mixed $default The default value (for InputArgument::OPTIONAL mode only) + * + * @throws InvalidArgumentException When argument mode is not valid + * + * @return $this + */ + public function addArgument(string $name, int $mode = null, string $description = '', $default = null) + { + $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); + if (null !== $this->fullDefinition) { + $this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default)); + } + + return $this; + } + + /** + * Adds an option. + * + * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants + * @param mixed $default The default value (must be null for InputOption::VALUE_NONE) + * + * @throws InvalidArgumentException If option mode is invalid or incompatible + * + * @return $this + */ + public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null) + { + $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); + if (null !== $this->fullDefinition) { + $this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); + } + + return $this; + } + + /** + * Sets the name of the command. + * + * This method can set both the namespace and the name if + * you separate them by a colon (:) + * + * $command->setName('foo:bar'); + * + * @return $this + * + * @throws InvalidArgumentException When the name is invalid + */ + public function setName(string $name) + { + $this->validateName($name); + + $this->name = $name; + + return $this; + } + + /** + * Sets the process title of the command. + * + * This feature should be used only when creating a long process command, + * like a daemon. + * + * @return $this + */ + public function setProcessTitle(string $title) + { + $this->processTitle = $title; + + return $this; + } + + /** + * Returns the command name. + * + * @return string|null + */ + public function getName() + { + return $this->name; + } + + /** + * @param bool $hidden Whether or not the command should be hidden from the list of commands + * The default value will be true in Symfony 6.0 + * + * @return $this + * + * @final since Symfony 5.1 + */ + public function setHidden(bool $hidden /*= true*/) + { + $this->hidden = $hidden; + + return $this; + } + + /** + * @return bool whether the command should be publicly shown or not + */ + public function isHidden() + { + return $this->hidden; + } + + /** + * Sets the description for the command. + * + * @return $this + */ + public function setDescription(string $description) + { + $this->description = $description; + + return $this; + } + + /** + * Returns the description for the command. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Sets the help for the command. + * + * @return $this + */ + public function setHelp(string $help) + { + $this->help = $help; + + return $this; + } + + /** + * Returns the help for the command. + * + * @return string + */ + public function getHelp() + { + return $this->help; + } + + /** + * Returns the processed help for the command replacing the %command.name% and + * %command.full_name% patterns with the real values dynamically. + * + * @return string + */ + public function getProcessedHelp() + { + $name = $this->name; + $isSingleCommand = $this->application && $this->application->isSingleCommand(); + + $placeholders = [ + '%command.name%', + '%command.full_name%', + ]; + $replacements = [ + $name, + $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name, + ]; + + return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription()); + } + + /** + * Sets the aliases for the command. + * + * @param string[] $aliases An array of aliases for the command + * + * @return $this + * + * @throws InvalidArgumentException When an alias is invalid + */ + public function setAliases(iterable $aliases) + { + $list = []; + + foreach ($aliases as $alias) { + $this->validateName($alias); + $list[] = $alias; + } + + $this->aliases = \is_array($aliases) ? $aliases : $list; + + return $this; + } + + /** + * Returns the aliases for the command. + * + * @return array + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * Returns the synopsis for the command. + * + * @param bool $short Whether to show the short version of the synopsis (with options folded) or not + * + * @return string + */ + public function getSynopsis(bool $short = false) + { + $key = $short ? 'short' : 'long'; + + if (!isset($this->synopsis[$key])) { + $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); + } + + return $this->synopsis[$key]; + } + + /** + * Add a command usage example, it'll be prefixed with the command name. + * + * @return $this + */ + public function addUsage(string $usage) + { + if (!str_starts_with($usage, $this->name)) { + $usage = sprintf('%s %s', $this->name, $usage); + } + + $this->usages[] = $usage; + + return $this; + } + + /** + * Returns alternative usages of the command. + * + * @return array + */ + public function getUsages() + { + return $this->usages; + } + + /** + * Gets a helper instance by name. + * + * @return mixed + * + * @throws LogicException if no HelperSet is defined + * @throws InvalidArgumentException if the helper is not defined + */ + public function getHelper(string $name) + { + if (null === $this->helperSet) { + throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name)); + } + + return $this->helperSet->get($name); + } + + /** + * Validates a command name. + * + * It must be non-empty and parts can optionally be separated by ":". + * + * @throws InvalidArgumentException When the name is invalid + */ + private function validateName(string $name) + { + if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { + throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); + } + } +} diff --git a/vendor/symfony/console/Command/CompleteCommand.php b/vendor/symfony/console/Command/CompleteCommand.php new file mode 100644 index 0000000..97357d6 --- /dev/null +++ b/vendor/symfony/console/Command/CompleteCommand.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Output\BashCompletionOutput; +use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; +use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Responsible for providing the values to the shell completion. + * + * @author Wouter de Jong + */ +final class CompleteCommand extends Command +{ + protected static $defaultName = '|_complete'; + protected static $defaultDescription = 'Internal command to provide shell completion suggestions'; + + private $completionOutputs; + + private $isDebug = false; + + /** + * @param array> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value + */ + public function __construct(array $completionOutputs = []) + { + // must be set before the parent constructor, as the property value is used in configure() + $this->completionOutputs = $completionOutputs + ['bash' => BashCompletionOutput::class]; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")') + ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)') + ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)') + ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'The version of the completion script') + ; + } + + protected function initialize(InputInterface $input, OutputInterface $output) + { + $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOLEAN); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + // uncomment when a bugfix or BC break has been introduced in the shell completion scripts + //$version = $input->getOption('symfony'); + //if ($version && version_compare($version, 'x.y', '>=')) { + // $message = sprintf('Completion script version is not supported ("%s" given, ">=x.y" required).', $version); + // $this->log($message); + + // $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); + + // return 126; + //} + + $shell = $input->getOption('shell'); + if (!$shell) { + throw new \RuntimeException('The "--shell" option must be set.'); + } + + if (!$completionOutput = $this->completionOutputs[$shell] ?? false) { + throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs)))); + } + + $completionInput = $this->createCompletionInput($input); + $suggestions = new CompletionSuggestions(); + + $this->log([ + '', + ''.date('Y-m-d H:i:s').'', + 'Input: ("|" indicates the cursor position)', + ' '.(string) $completionInput, + 'Command:', + ' '.(string) implode(' ', $_SERVER['argv']), + 'Messages:', + ]); + + $command = $this->findCommand($completionInput, $output); + if (null === $command) { + $this->log(' No command found, completing using the Application class.'); + + $this->getApplication()->complete($completionInput, $suggestions); + } elseif ( + $completionInput->mustSuggestArgumentValuesFor('command') + && $command->getName() !== $completionInput->getCompletionValue() + ) { + $this->log(' No command found, completing using the Application class.'); + + // expand shortcut names ("cache:cl") into their full name ("cache:clear") + $suggestions->suggestValue($command->getName()); + } else { + $command->mergeApplicationDefinition(); + $completionInput->bind($command->getDefinition()); + + if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) { + $this->log(' Completing option names for the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).' command.'); + + $suggestions->suggestOptions($command->getDefinition()->getOptions()); + } else { + $this->log([ + ' Completing using the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).' class.', + ' Completing '.$completionInput->getCompletionType().' for '.$completionInput->getCompletionName().'', + ]); + if (null !== $compval = $completionInput->getCompletionValue()) { + $this->log(' Current value: '.$compval.''); + } + + $command->complete($completionInput, $suggestions); + } + } + + /** @var CompletionOutputInterface $completionOutput */ + $completionOutput = new $completionOutput(); + + $this->log('Suggestions:'); + if ($options = $suggestions->getOptionSuggestions()) { + $this->log(' --'.implode(' --', array_map(function ($o) { return $o->getName(); }, $options))); + } elseif ($values = $suggestions->getValueSuggestions()) { + $this->log(' '.implode(' ', $values)); + } else { + $this->log(' No suggestions were provided'); + } + + $completionOutput->write($suggestions, $output); + } catch (\Throwable $e) { + $this->log([ + 'Error!', + (string) $e, + ]); + + if ($output->isDebug()) { + throw $e; + } + + return self::FAILURE; + } + + return self::SUCCESS; + } + + private function createCompletionInput(InputInterface $input): CompletionInput + { + $currentIndex = $input->getOption('current'); + if (!$currentIndex || !ctype_digit($currentIndex)) { + throw new \RuntimeException('The "--current" option must be set and it must be an integer.'); + } + + $completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex); + + try { + $completionInput->bind($this->getApplication()->getDefinition()); + } catch (ExceptionInterface $e) { + } + + return $completionInput; + } + + private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command + { + try { + $inputName = $completionInput->getFirstArgument(); + if (null === $inputName) { + return null; + } + + return $this->getApplication()->find($inputName); + } catch (CommandNotFoundException $e) { + } + + return null; + } + + private function log($messages): void + { + if (!$this->isDebug) { + return; + } + + $commandName = basename($_SERVER['argv'][0]); + file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND); + } +} diff --git a/vendor/symfony/console/Command/DumpCompletionCommand.php b/vendor/symfony/console/Command/DumpCompletionCommand.php new file mode 100644 index 0000000..697ade5 --- /dev/null +++ b/vendor/symfony/console/Command/DumpCompletionCommand.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; + +/** + * Dumps the completion script for the current shell. + * + * @author Wouter de Jong + */ +final class DumpCompletionCommand extends Command +{ + protected static $defaultName = 'completion'; + protected static $defaultDescription = 'Dump the shell completion script'; + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('shell')) { + $suggestions->suggestValues($this->getSupportedShells()); + } + } + + protected function configure() + { + $fullCommand = $_SERVER['PHP_SELF']; + $commandName = basename($fullCommand); + $fullCommand = realpath($fullCommand) ?: $fullCommand; + + $this + ->setHelp(<<%command.name% command dumps the shell completion script required +to use shell autocompletion (currently only bash completion is supported). + +Static installation +------------------- + +Dump the script to a global completion file and restart your shell: + + %command.full_name% bash | sudo tee /etc/bash_completion.d/${commandName} + +Or dump the script to a local file and source it: + + %command.full_name% bash > completion.sh + + # source the file whenever you use the project + source completion.sh + + # or add this line at the end of your "~/.bashrc" file: + source /path/to/completion.sh + +Dynamic installation +-------------------- + +Add this add the end of your shell configuration file (e.g. "~/.bashrc"): + + eval "$(${fullCommand} completion bash)" +EOH + ) + ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $commandName = basename($_SERVER['argv'][0]); + + if ($input->getOption('debug')) { + $this->tailDebugLog($commandName, $output); + + return self::SUCCESS; + } + + $shell = $input->getArgument('shell') ?? self::guessShell(); + $completionFile = __DIR__.'/../Resources/completion.'.$shell; + if (!file_exists($completionFile)) { + $supportedShells = $this->getSupportedShells(); + + ($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output) + ->writeln(sprintf('Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").', $shell, implode('", "', $supportedShells))); + + return self::INVALID; + } + + $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile))); + + return self::SUCCESS; + } + + private static function guessShell(): string + { + return basename($_SERVER['SHELL'] ?? ''); + } + + private function tailDebugLog(string $commandName, OutputInterface $output): void + { + $debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log'; + if (!file_exists($debugFile)) { + touch($debugFile); + } + $process = new Process(['tail', '-f', $debugFile], null, null, null, 0); + $process->run(function (string $type, string $line) use ($output): void { + $output->write($line); + }); + } + + /** + * @return string[] + */ + private function getSupportedShells(): array + { + return array_map(function ($f) { + return pathinfo($f, \PATHINFO_EXTENSION); + }, glob(__DIR__.'/../Resources/completion.*')); + } +} diff --git a/vendor/symfony/console/Command/HelpCommand.php b/vendor/symfony/console/Command/HelpCommand.php new file mode 100644 index 0000000..c66ef46 --- /dev/null +++ b/vendor/symfony/console/Command/HelpCommand.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Descriptor\ApplicationDescription; +use Symfony\Component\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * HelpCommand displays the help for a given command. + * + * @author Fabien Potencier + */ +class HelpCommand extends Command +{ + private $command; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->ignoreValidationErrors(); + + $this + ->setName('help') + ->setDefinition([ + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + ]) + ->setDescription('Display help for a command') + ->setHelp(<<<'EOF' +The %command.name% command displays help for a given command: + + %command.full_name% list + +You can also output the help in other formats by using the --format option: + + %command.full_name% --format=xml list + +To display the list of available commands, please use the list command. +EOF + ) + ; + } + + public function setCommand(Command $command) + { + $this->command = $command; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + if (null === $this->command) { + $this->command = $this->getApplication()->find($input->getArgument('command_name')); + } + + $helper = new DescriptorHelper(); + $helper->describe($output, $this->command, [ + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + ]); + + $this->command = null; + + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('command_name')) { + $descriptor = new ApplicationDescription($this->getApplication()); + $suggestions->suggestValues(array_keys($descriptor->getCommands())); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $helper = new DescriptorHelper(); + $suggestions->suggestValues($helper->getFormats()); + } + } +} diff --git a/vendor/symfony/console/Command/LazyCommand.php b/vendor/symfony/console/Command/LazyCommand.php new file mode 100644 index 0000000..e576ad0 --- /dev/null +++ b/vendor/symfony/console/Command/LazyCommand.php @@ -0,0 +1,218 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Nicolas Grekas + */ +final class LazyCommand extends Command +{ + private $command; + private $isEnabled; + + public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true) + { + $this->setName($name) + ->setAliases($aliases) + ->setHidden($isHidden) + ->setDescription($description); + + $this->command = $commandFactory; + $this->isEnabled = $isEnabled; + } + + public function ignoreValidationErrors(): void + { + $this->getCommand()->ignoreValidationErrors(); + } + + public function setApplication(Application $application = null): void + { + if ($this->command instanceof parent) { + $this->command->setApplication($application); + } + + parent::setApplication($application); + } + + public function setHelperSet(HelperSet $helperSet): void + { + if ($this->command instanceof parent) { + $this->command->setHelperSet($helperSet); + } + + parent::setHelperSet($helperSet); + } + + public function isEnabled(): bool + { + return $this->isEnabled ?? $this->getCommand()->isEnabled(); + } + + public function run(InputInterface $input, OutputInterface $output): int + { + return $this->getCommand()->run($input, $output); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->getCommand()->complete($input, $suggestions); + } + + /** + * @return $this + */ + public function setCode(callable $code): self + { + $this->getCommand()->setCode($code); + + return $this; + } + + /** + * @internal + */ + public function mergeApplicationDefinition(bool $mergeArgs = true): void + { + $this->getCommand()->mergeApplicationDefinition($mergeArgs); + } + + /** + * @return $this + */ + public function setDefinition($definition): self + { + $this->getCommand()->setDefinition($definition); + + return $this; + } + + public function getDefinition(): InputDefinition + { + return $this->getCommand()->getDefinition(); + } + + public function getNativeDefinition(): InputDefinition + { + return $this->getCommand()->getNativeDefinition(); + } + + /** + * @return $this + */ + public function addArgument(string $name, int $mode = null, string $description = '', $default = null): self + { + $this->getCommand()->addArgument($name, $mode, $description, $default); + + return $this; + } + + /** + * @return $this + */ + public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null): self + { + $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default); + + return $this; + } + + /** + * @return $this + */ + public function setProcessTitle(string $title): self + { + $this->getCommand()->setProcessTitle($title); + + return $this; + } + + /** + * @return $this + */ + public function setHelp(string $help): self + { + $this->getCommand()->setHelp($help); + + return $this; + } + + public function getHelp(): string + { + return $this->getCommand()->getHelp(); + } + + public function getProcessedHelp(): string + { + return $this->getCommand()->getProcessedHelp(); + } + + public function getSynopsis(bool $short = false): string + { + return $this->getCommand()->getSynopsis($short); + } + + /** + * @return $this + */ + public function addUsage(string $usage): self + { + $this->getCommand()->addUsage($usage); + + return $this; + } + + public function getUsages(): array + { + return $this->getCommand()->getUsages(); + } + + /** + * @return mixed + */ + public function getHelper(string $name) + { + return $this->getCommand()->getHelper($name); + } + + public function getCommand(): parent + { + if (!$this->command instanceof \Closure) { + return $this->command; + } + + $command = $this->command = ($this->command)(); + $command->setApplication($this->getApplication()); + + if (null !== $this->getHelperSet()) { + $command->setHelperSet($this->getHelperSet()); + } + + $command->setName($this->getName()) + ->setAliases($this->getAliases()) + ->setHidden($this->isHidden()) + ->setDescription($this->getDescription()); + + // Will throw if the command is not correctly initialized. + $command->getDefinition(); + + return $command; + } +} diff --git a/vendor/symfony/console/Command/ListCommand.php b/vendor/symfony/console/Command/ListCommand.php new file mode 100644 index 0000000..f04a4ef --- /dev/null +++ b/vendor/symfony/console/Command/ListCommand.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Descriptor\ApplicationDescription; +use Symfony\Component\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * ListCommand displays the list of all available commands for the application. + * + * @author Fabien Potencier + */ +class ListCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('list') + ->setDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), + ]) + ->setDescription('List commands') + ->setHelp(<<<'EOF' +The %command.name% command lists all commands: + + %command.full_name% + +You can also display the commands for a specific namespace: + + %command.full_name% test + +You can also output the information in other formats by using the --format option: + + %command.full_name% --format=xml + +It's also possible to get raw list of commands (useful for embedding command runner): + + %command.full_name% --raw +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $helper = new DescriptorHelper(); + $helper->describe($output, $this->getApplication(), [ + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + 'short' => $input->getOption('short'), + ]); + + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('namespace')) { + $descriptor = new ApplicationDescription($this->getApplication()); + $suggestions->suggestValues(array_keys($descriptor->getNamespaces())); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $helper = new DescriptorHelper(); + $suggestions->suggestValues($helper->getFormats()); + } + } +} diff --git a/vendor/symfony/console/Command/LockableTrait.php b/vendor/symfony/console/Command/LockableTrait.php new file mode 100644 index 0000000..b1856dc --- /dev/null +++ b/vendor/symfony/console/Command/LockableTrait.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\LockInterface; +use Symfony\Component\Lock\Store\FlockStore; +use Symfony\Component\Lock\Store\SemaphoreStore; + +/** + * Basic lock feature for commands. + * + * @author Geoffrey Brier + */ +trait LockableTrait +{ + /** @var LockInterface|null */ + private $lock; + + /** + * Locks a command. + */ + private function lock(string $name = null, bool $blocking = false): bool + { + if (!class_exists(SemaphoreStore::class)) { + throw new LogicException('To enable the locking feature you must install the symfony/lock component.'); + } + + if (null !== $this->lock) { + throw new LogicException('A lock is already in place.'); + } + + if (SemaphoreStore::isSupported()) { + $store = new SemaphoreStore(); + } else { + $store = new FlockStore(); + } + + $this->lock = (new LockFactory($store))->createLock($name ?: $this->getName()); + if (!$this->lock->acquire($blocking)) { + $this->lock = null; + + return false; + } + + return true; + } + + /** + * Releases the command lock if there is one. + */ + private function release() + { + if ($this->lock) { + $this->lock->release(); + $this->lock = null; + } + } +} diff --git a/vendor/symfony/console/Command/SignalableCommandInterface.php b/vendor/symfony/console/Command/SignalableCommandInterface.php new file mode 100644 index 0000000..d439728 --- /dev/null +++ b/vendor/symfony/console/Command/SignalableCommandInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +/** + * Interface for command reacting to signal. + * + * @author Grégoire Pineau + */ +interface SignalableCommandInterface +{ + /** + * Returns the list of signals to subscribe. + */ + public function getSubscribedSignals(): array; + + /** + * The method will be called when the application is signaled. + */ + public function handleSignal(int $signal): void; +} diff --git a/vendor/symfony/console/CommandLoader/CommandLoaderInterface.php b/vendor/symfony/console/CommandLoader/CommandLoaderInterface.php new file mode 100644 index 0000000..0adaf88 --- /dev/null +++ b/vendor/symfony/console/CommandLoader/CommandLoaderInterface.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CommandLoader; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * @author Robin Chalas + */ +interface CommandLoaderInterface +{ + /** + * Loads a command. + * + * @return Command + * + * @throws CommandNotFoundException + */ + public function get(string $name); + + /** + * Checks if a command exists. + * + * @return bool + */ + public function has(string $name); + + /** + * @return string[] + */ + public function getNames(); +} diff --git a/vendor/symfony/console/CommandLoader/ContainerCommandLoader.php b/vendor/symfony/console/CommandLoader/ContainerCommandLoader.php new file mode 100644 index 0000000..ddccb3d --- /dev/null +++ b/vendor/symfony/console/CommandLoader/ContainerCommandLoader.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CommandLoader; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * Loads commands from a PSR-11 container. + * + * @author Robin Chalas + */ +class ContainerCommandLoader implements CommandLoaderInterface +{ + private $container; + private $commandMap; + + /** + * @param array $commandMap An array with command names as keys and service ids as values + */ + public function __construct(ContainerInterface $container, array $commandMap) + { + $this->container = $container; + $this->commandMap = $commandMap; + } + + /** + * {@inheritdoc} + */ + public function get(string $name) + { + if (!$this->has($name)) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + return $this->container->get($this->commandMap[$name]); + } + + /** + * {@inheritdoc} + */ + public function has(string $name) + { + return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]); + } + + /** + * {@inheritdoc} + */ + public function getNames() + { + return array_keys($this->commandMap); + } +} diff --git a/vendor/symfony/console/CommandLoader/FactoryCommandLoader.php b/vendor/symfony/console/CommandLoader/FactoryCommandLoader.php new file mode 100644 index 0000000..7e2db34 --- /dev/null +++ b/vendor/symfony/console/CommandLoader/FactoryCommandLoader.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CommandLoader; + +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * A simple command loader using factories to instantiate commands lazily. + * + * @author Maxime Steinhausser + */ +class FactoryCommandLoader implements CommandLoaderInterface +{ + private $factories; + + /** + * @param callable[] $factories Indexed by command names + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + /** + * {@inheritdoc} + */ + public function has(string $name) + { + return isset($this->factories[$name]); + } + + /** + * {@inheritdoc} + */ + public function get(string $name) + { + if (!isset($this->factories[$name])) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + $factory = $this->factories[$name]; + + return $factory(); + } + + /** + * {@inheritdoc} + */ + public function getNames() + { + return array_keys($this->factories); + } +} diff --git a/vendor/symfony/console/Completion/CompletionInput.php b/vendor/symfony/console/Completion/CompletionInput.php new file mode 100644 index 0000000..368b945 --- /dev/null +++ b/vendor/symfony/console/Completion/CompletionInput.php @@ -0,0 +1,249 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * An input specialized for shell completion. + * + * This input allows unfinished option names or values and exposes what kind of + * completion is expected. + * + * @author Wouter de Jong + */ +final class CompletionInput extends ArgvInput +{ + public const TYPE_ARGUMENT_VALUE = 'argument_value'; + public const TYPE_OPTION_VALUE = 'option_value'; + public const TYPE_OPTION_NAME = 'option_name'; + public const TYPE_NONE = 'none'; + + private $tokens; + private $currentIndex; + private $completionType; + private $completionName = null; + private $completionValue = ''; + + /** + * Converts a terminal string into tokens. + * + * This is required for shell completions without COMP_WORDS support. + */ + public static function fromString(string $inputStr, int $currentIndex): self + { + preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?tokens = $tokens; + $input->currentIndex = $currentIndex; + + return $input; + } + + /** + * {@inheritdoc} + */ + public function bind(InputDefinition $definition): void + { + parent::bind($definition); + + $relevantToken = $this->getRelevantToken(); + if ('-' === $relevantToken[0]) { + // the current token is an input option: complete either option name or option value + [$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', '']; + + $option = $this->getOptionFromToken($optionToken); + if (null === $option && !$this->isCursorFree()) { + $this->completionType = self::TYPE_OPTION_NAME; + $this->completionValue = $relevantToken; + + return; + } + + if (null !== $option && $option->acceptValue()) { + $this->completionType = self::TYPE_OPTION_VALUE; + $this->completionName = $option->getName(); + $this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : ''); + + return; + } + } + + $previousToken = $this->tokens[$this->currentIndex - 1]; + if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) { + // check if previous option accepted a value + $previousOption = $this->getOptionFromToken($previousToken); + if (null !== $previousOption && $previousOption->acceptValue()) { + $this->completionType = self::TYPE_OPTION_VALUE; + $this->completionName = $previousOption->getName(); + $this->completionValue = $relevantToken; + + return; + } + } + + // complete argument value + $this->completionType = self::TYPE_ARGUMENT_VALUE; + + foreach ($this->definition->getArguments() as $argumentName => $argument) { + if (!isset($this->arguments[$argumentName])) { + break; + } + + $argumentValue = $this->arguments[$argumentName]; + $this->completionName = $argumentName; + if (\is_array($argumentValue)) { + $this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null; + } else { + $this->completionValue = $argumentValue; + } + } + + if ($this->currentIndex >= \count($this->tokens)) { + if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) { + $this->completionName = $argumentName; + $this->completionValue = ''; + } else { + // we've reached the end + $this->completionType = self::TYPE_NONE; + $this->completionName = null; + $this->completionValue = ''; + } + } + } + + /** + * Returns the type of completion required. + * + * TYPE_ARGUMENT_VALUE when completing the value of an input argument + * TYPE_OPTION_VALUE when completing the value of an input option + * TYPE_OPTION_NAME when completing the name of an input option + * TYPE_NONE when nothing should be completed + * + * @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component + */ + public function getCompletionType(): string + { + return $this->completionType; + } + + /** + * The name of the input option or argument when completing a value. + * + * @return string|null returns null when completing an option name + */ + public function getCompletionName(): ?string + { + return $this->completionName; + } + + /** + * The value already typed by the user (or empty string). + */ + public function getCompletionValue(): string + { + return $this->completionValue; + } + + public function mustSuggestOptionValuesFor(string $optionName): bool + { + return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName(); + } + + public function mustSuggestArgumentValuesFor(string $argumentName): bool + { + return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName(); + } + + protected function parseToken(string $token, bool $parseOptions): bool + { + try { + return parent::parseToken($token, $parseOptions); + } catch (RuntimeException $e) { + // suppress errors, completed input is almost never valid + } + + return $parseOptions; + } + + private function getOptionFromToken(string $optionToken): ?InputOption + { + $optionName = ltrim($optionToken, '-'); + if (!$optionName) { + return null; + } + + if ('-' === ($optionToken[1] ?? ' ')) { + // long option name + return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null; + } + + // short option name + return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null; + } + + /** + * The token of the cursor, or the last token if the cursor is at the end of the input. + */ + private function getRelevantToken(): string + { + return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex]; + } + + /** + * Whether the cursor is "free" (i.e. at the end of the input preceded by a space). + */ + private function isCursorFree(): bool + { + $nrOfTokens = \count($this->tokens); + if ($this->currentIndex > $nrOfTokens) { + throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.'); + } + + return $this->currentIndex >= $nrOfTokens; + } + + public function __toString() + { + $str = ''; + foreach ($this->tokens as $i => $token) { + $str .= $token; + + if ($this->currentIndex === $i) { + $str .= '|'; + } + + $str .= ' '; + } + + if ($this->currentIndex > $i) { + $str .= '|'; + } + + return rtrim($str); + } +} diff --git a/vendor/symfony/console/Completion/CompletionSuggestions.php b/vendor/symfony/console/Completion/CompletionSuggestions.php new file mode 100644 index 0000000..d8905e5 --- /dev/null +++ b/vendor/symfony/console/Completion/CompletionSuggestions.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +use Symfony\Component\Console\Input\InputOption; + +/** + * Stores all completion suggestions for the current input. + * + * @author Wouter de Jong + */ +final class CompletionSuggestions +{ + private $valueSuggestions = []; + private $optionSuggestions = []; + + /** + * Add a suggested value for an input option or argument. + * + * @param string|Suggestion $value + * + * @return $this + */ + public function suggestValue($value): self + { + $this->valueSuggestions[] = !$value instanceof Suggestion ? new Suggestion($value) : $value; + + return $this; + } + + /** + * Add multiple suggested values at once for an input option or argument. + * + * @param list $values + * + * @return $this + */ + public function suggestValues(array $values): self + { + foreach ($values as $value) { + $this->suggestValue($value); + } + + return $this; + } + + /** + * Add a suggestion for an input option name. + * + * @return $this + */ + public function suggestOption(InputOption $option): self + { + $this->optionSuggestions[] = $option; + + return $this; + } + + /** + * Add multiple suggestions for input option names at once. + * + * @param InputOption[] $options + * + * @return $this + */ + public function suggestOptions(array $options): self + { + foreach ($options as $option) { + $this->suggestOption($option); + } + + return $this; + } + + /** + * @return InputOption[] + */ + public function getOptionSuggestions(): array + { + return $this->optionSuggestions; + } + + /** + * @return Suggestion[] + */ + public function getValueSuggestions(): array + { + return $this->valueSuggestions; + } +} diff --git a/vendor/symfony/console/Completion/Output/BashCompletionOutput.php b/vendor/symfony/console/Completion/Output/BashCompletionOutput.php new file mode 100644 index 0000000..8d5ffa6 --- /dev/null +++ b/vendor/symfony/console/Completion/Output/BashCompletionOutput.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Wouter de Jong + */ +class BashCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + $values = $suggestions->getValueSuggestions(); + foreach ($suggestions->getOptionSuggestions() as $option) { + $values[] = '--'.$option->getName(); + } + $output->writeln(implode("\n", $values)); + } +} diff --git a/vendor/symfony/console/Completion/Output/CompletionOutputInterface.php b/vendor/symfony/console/Completion/Output/CompletionOutputInterface.php new file mode 100644 index 0000000..659e596 --- /dev/null +++ b/vendor/symfony/console/Completion/Output/CompletionOutputInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Transforms the {@see CompletionSuggestions} object into output readable by the shell completion. + * + * @author Wouter de Jong + */ +interface CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void; +} diff --git a/vendor/symfony/console/Completion/Suggestion.php b/vendor/symfony/console/Completion/Suggestion.php new file mode 100644 index 0000000..6c7bc4d --- /dev/null +++ b/vendor/symfony/console/Completion/Suggestion.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +/** + * Represents a single suggested value. + * + * @author Wouter de Jong + */ +class Suggestion +{ + private $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->getValue(); + } +} diff --git a/vendor/symfony/console/ConsoleEvents.php b/vendor/symfony/console/ConsoleEvents.php new file mode 100644 index 0000000..6ae8f32 --- /dev/null +++ b/vendor/symfony/console/ConsoleEvents.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; + +/** + * Contains all events dispatched by an Application. + * + * @author Francesco Levorato + */ +final class ConsoleEvents +{ + /** + * The COMMAND event allows you to attach listeners before any command is + * executed by the console. It also allows you to modify the command, input and output + * before they are handed to the command. + * + * @Event("Symfony\Component\Console\Event\ConsoleCommandEvent") + */ + public const COMMAND = 'console.command'; + + /** + * The SIGNAL event allows you to perform some actions + * after the command execution was interrupted. + * + * @Event("Symfony\Component\Console\Event\ConsoleSignalEvent") + */ + public const SIGNAL = 'console.signal'; + + /** + * The TERMINATE event allows you to attach listeners after a command is + * executed by the console. + * + * @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent") + */ + public const TERMINATE = 'console.terminate'; + + /** + * The ERROR event occurs when an uncaught exception or error appears. + * + * This event allows you to deal with the exception/error or + * to modify the thrown exception. + * + * @Event("Symfony\Component\Console\Event\ConsoleErrorEvent") + */ + public const ERROR = 'console.error'; + + /** + * Event aliases. + * + * These aliases can be consumed by RegisterListenersPass. + */ + public const ALIASES = [ + ConsoleCommandEvent::class => self::COMMAND, + ConsoleErrorEvent::class => self::ERROR, + ConsoleSignalEvent::class => self::SIGNAL, + ConsoleTerminateEvent::class => self::TERMINATE, + ]; +} diff --git a/vendor/symfony/console/Cursor.php b/vendor/symfony/console/Cursor.php new file mode 100644 index 0000000..0c4dafb --- /dev/null +++ b/vendor/symfony/console/Cursor.php @@ -0,0 +1,207 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Pierre du Plessis + */ +final class Cursor +{ + private $output; + private $input; + + /** + * @param resource|null $input + */ + public function __construct(OutputInterface $output, $input = null) + { + $this->output = $output; + $this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+')); + } + + /** + * @return $this + */ + public function moveUp(int $lines = 1): self + { + $this->output->write(sprintf("\x1b[%dA", $lines)); + + return $this; + } + + /** + * @return $this + */ + public function moveDown(int $lines = 1): self + { + $this->output->write(sprintf("\x1b[%dB", $lines)); + + return $this; + } + + /** + * @return $this + */ + public function moveRight(int $columns = 1): self + { + $this->output->write(sprintf("\x1b[%dC", $columns)); + + return $this; + } + + /** + * @return $this + */ + public function moveLeft(int $columns = 1): self + { + $this->output->write(sprintf("\x1b[%dD", $columns)); + + return $this; + } + + /** + * @return $this + */ + public function moveToColumn(int $column): self + { + $this->output->write(sprintf("\x1b[%dG", $column)); + + return $this; + } + + /** + * @return $this + */ + public function moveToPosition(int $column, int $row): self + { + $this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column)); + + return $this; + } + + /** + * @return $this + */ + public function savePosition(): self + { + $this->output->write("\x1b7"); + + return $this; + } + + /** + * @return $this + */ + public function restorePosition(): self + { + $this->output->write("\x1b8"); + + return $this; + } + + /** + * @return $this + */ + public function hide(): self + { + $this->output->write("\x1b[?25l"); + + return $this; + } + + /** + * @return $this + */ + public function show(): self + { + $this->output->write("\x1b[?25h\x1b[?0c"); + + return $this; + } + + /** + * Clears all the output from the current line. + * + * @return $this + */ + public function clearLine(): self + { + $this->output->write("\x1b[2K"); + + return $this; + } + + /** + * Clears all the output from the current line after the current position. + */ + public function clearLineAfter(): self + { + $this->output->write("\x1b[K"); + + return $this; + } + + /** + * Clears all the output from the cursors' current position to the end of the screen. + * + * @return $this + */ + public function clearOutput(): self + { + $this->output->write("\x1b[0J"); + + return $this; + } + + /** + * Clears the entire screen. + * + * @return $this + */ + public function clearScreen(): self + { + $this->output->write("\x1b[2J"); + + return $this; + } + + /** + * Returns the current cursor position as x,y coordinates. + */ + public function getCurrentPosition(): array + { + static $isTtySupported; + + if (null === $isTtySupported && \function_exists('proc_open')) { + $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); + } + + if (!$isTtySupported) { + return [1, 1]; + } + + $sttyMode = shell_exec('stty -g'); + shell_exec('stty -icanon -echo'); + + @fwrite($this->input, "\033[6n"); + + $code = trim(fread($this->input, 1024)); + + shell_exec(sprintf('stty %s', $sttyMode)); + + sscanf($code, "\033[%d;%dR", $row, $col); + + return [$col, $row]; + } +} diff --git a/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php b/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php new file mode 100644 index 0000000..743e306 --- /dev/null +++ b/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\DependencyInjection; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Command\LazyCommand; +use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\TypedReference; + +/** + * Registers console commands. + * + * @author Grégoire Pineau + */ +class AddConsoleCommandPass implements CompilerPassInterface +{ + private $commandLoaderServiceId; + private $commandTag; + private $noPreloadTag; + private $privateTagName; + + public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command', string $noPreloadTag = 'container.no_preload', string $privateTagName = 'container.private') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/console', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->commandLoaderServiceId = $commandLoaderServiceId; + $this->commandTag = $commandTag; + $this->noPreloadTag = $noPreloadTag; + $this->privateTagName = $privateTagName; + } + + public function process(ContainerBuilder $container) + { + $commandServices = $container->findTaggedServiceIds($this->commandTag, true); + $lazyCommandMap = []; + $lazyCommandRefs = []; + $serviceIds = []; + + foreach ($commandServices as $id => $tags) { + $definition = $container->getDefinition($id); + $definition->addTag($this->noPreloadTag); + $class = $container->getParameterBag()->resolveValue($definition->getClass()); + + if (isset($tags[0]['command'])) { + $aliases = $tags[0]['command']; + } else { + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + if (!$r->isSubclassOf(Command::class)) { + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); + } + $aliases = $class::getDefaultName(); + } + + $aliases = explode('|', $aliases ?? ''); + $commandName = array_shift($aliases); + + if ($isHidden = '' === $commandName) { + $commandName = array_shift($aliases); + } + + if (null === $commandName) { + if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag($this->privateTagName)) { + $commandId = 'console.command.public_alias.'.$id; + $container->setAlias($commandId, $id)->setPublic(true); + $id = $commandId; + } + $serviceIds[] = $id; + + continue; + } + + $description = $tags[0]['description'] ?? null; + + unset($tags[0]); + $lazyCommandMap[$commandName] = $id; + $lazyCommandRefs[$id] = new TypedReference($id, $class); + + foreach ($aliases as $alias) { + $lazyCommandMap[$alias] = $id; + } + + foreach ($tags as $tag) { + if (isset($tag['command'])) { + $aliases[] = $tag['command']; + $lazyCommandMap[$tag['command']] = $id; + } + + $description = $description ?? $tag['description'] ?? null; + } + + $definition->addMethodCall('setName', [$commandName]); + + if ($aliases) { + $definition->addMethodCall('setAliases', [$aliases]); + } + + if ($isHidden) { + $definition->addMethodCall('setHidden', [true]); + } + + if (!$description) { + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + if (!$r->isSubclassOf(Command::class)) { + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); + } + $description = $class::getDefaultDescription(); + } + + if ($description) { + $definition->addMethodCall('setDescription', [$description]); + + $container->register('.'.$id.'.lazy', LazyCommand::class) + ->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]); + + $lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy'); + } + } + + $container + ->register($this->commandLoaderServiceId, ContainerCommandLoader::class) + ->setPublic(true) + ->addTag($this->noPreloadTag) + ->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]); + + $container->setParameter('console.command.ids', $serviceIds); + } +} diff --git a/vendor/symfony/console/Descriptor/ApplicationDescription.php b/vendor/symfony/console/Descriptor/ApplicationDescription.php new file mode 100644 index 0000000..fac01ad --- /dev/null +++ b/vendor/symfony/console/Descriptor/ApplicationDescription.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * @author Jean-François Simon + * + * @internal + */ +class ApplicationDescription +{ + public const GLOBAL_NAMESPACE = '_global'; + + private $application; + private $namespace; + private $showHidden; + + /** + * @var array + */ + private $namespaces; + + /** + * @var array + */ + private $commands; + + /** + * @var array + */ + private $aliases; + + public function __construct(Application $application, string $namespace = null, bool $showHidden = false) + { + $this->application = $application; + $this->namespace = $namespace; + $this->showHidden = $showHidden; + } + + public function getNamespaces(): array + { + if (null === $this->namespaces) { + $this->inspectApplication(); + } + + return $this->namespaces; + } + + /** + * @return Command[] + */ + public function getCommands(): array + { + if (null === $this->commands) { + $this->inspectApplication(); + } + + return $this->commands; + } + + /** + * @throws CommandNotFoundException + */ + public function getCommand(string $name): Command + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + return $this->commands[$name] ?? $this->aliases[$name]; + } + + private function inspectApplication() + { + $this->commands = []; + $this->namespaces = []; + + $all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null); + foreach ($this->sortCommands($all) as $namespace => $commands) { + $names = []; + + /** @var Command $command */ + foreach ($commands as $name => $command) { + if (!$command->getName() || (!$this->showHidden && $command->isHidden())) { + continue; + } + + if ($command->getName() === $name) { + $this->commands[$name] = $command; + } else { + $this->aliases[$name] = $command; + } + + $names[] = $name; + } + + $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; + } + } + + private function sortCommands(array $commands): array + { + $namespacedCommands = []; + $globalCommands = []; + $sortedCommands = []; + foreach ($commands as $name => $command) { + $key = $this->application->extractNamespace($name, 1); + if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) { + $globalCommands[$name] = $command; + } else { + $namespacedCommands[$key][$name] = $command; + } + } + + if ($globalCommands) { + ksort($globalCommands); + $sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands; + } + + if ($namespacedCommands) { + ksort($namespacedCommands); + foreach ($namespacedCommands as $key => $commandsSet) { + ksort($commandsSet); + $sortedCommands[$key] = $commandsSet; + } + } + + return $sortedCommands; + } +} diff --git a/vendor/symfony/console/Descriptor/Descriptor.php b/vendor/symfony/console/Descriptor/Descriptor.php new file mode 100644 index 0000000..a364830 --- /dev/null +++ b/vendor/symfony/console/Descriptor/Descriptor.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jean-François Simon + * + * @internal + */ +abstract class Descriptor implements DescriptorInterface +{ + /** + * @var OutputInterface + */ + protected $output; + + /** + * {@inheritdoc} + */ + public function describe(OutputInterface $output, object $object, array $options = []) + { + $this->output = $output; + + switch (true) { + case $object instanceof InputArgument: + $this->describeInputArgument($object, $options); + break; + case $object instanceof InputOption: + $this->describeInputOption($object, $options); + break; + case $object instanceof InputDefinition: + $this->describeInputDefinition($object, $options); + break; + case $object instanceof Command: + $this->describeCommand($object, $options); + break; + case $object instanceof Application: + $this->describeApplication($object, $options); + break; + default: + throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))); + } + } + + /** + * Writes content to output. + */ + protected function write(string $content, bool $decorated = false) + { + $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); + } + + /** + * Describes an InputArgument instance. + */ + abstract protected function describeInputArgument(InputArgument $argument, array $options = []); + + /** + * Describes an InputOption instance. + */ + abstract protected function describeInputOption(InputOption $option, array $options = []); + + /** + * Describes an InputDefinition instance. + */ + abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []); + + /** + * Describes a Command instance. + */ + abstract protected function describeCommand(Command $command, array $options = []); + + /** + * Describes an Application instance. + */ + abstract protected function describeApplication(Application $application, array $options = []); +} diff --git a/vendor/symfony/console/Descriptor/DescriptorInterface.php b/vendor/symfony/console/Descriptor/DescriptorInterface.php new file mode 100644 index 0000000..ebea303 --- /dev/null +++ b/vendor/symfony/console/Descriptor/DescriptorInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Descriptor interface. + * + * @author Jean-François Simon + */ +interface DescriptorInterface +{ + public function describe(OutputInterface $output, object $object, array $options = []); +} diff --git a/vendor/symfony/console/Descriptor/JsonDescriptor.php b/vendor/symfony/console/Descriptor/JsonDescriptor.php new file mode 100644 index 0000000..1d28659 --- /dev/null +++ b/vendor/symfony/console/Descriptor/JsonDescriptor.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * JSON descriptor. + * + * @author Jean-François Simon + * + * @internal + */ +class JsonDescriptor extends Descriptor +{ + /** + * {@inheritdoc} + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + $this->writeData($this->getInputArgumentData($argument), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + $this->writeData($this->getInputOptionData($option), $options); + if ($option->isNegatable()) { + $this->writeData($this->getInputOptionData($option, true), $options); + } + } + + /** + * {@inheritdoc} + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + $this->writeData($this->getInputDefinitionData($definition), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeCommand(Command $command, array $options = []) + { + $this->writeData($this->getCommandData($command, $options['short'] ?? false), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeApplication(Application $application, array $options = []) + { + $describedNamespace = $options['namespace'] ?? null; + $description = new ApplicationDescription($application, $describedNamespace, true); + $commands = []; + + foreach ($description->getCommands() as $command) { + $commands[] = $this->getCommandData($command, $options['short'] ?? false); + } + + $data = []; + if ('UNKNOWN' !== $application->getName()) { + $data['application']['name'] = $application->getName(); + if ('UNKNOWN' !== $application->getVersion()) { + $data['application']['version'] = $application->getVersion(); + } + } + + $data['commands'] = $commands; + + if ($describedNamespace) { + $data['namespace'] = $describedNamespace; + } else { + $data['namespaces'] = array_values($description->getNamespaces()); + } + + $this->writeData($data, $options); + } + + /** + * Writes data as json. + */ + private function writeData(array $data, array $options) + { + $flags = $options['json_encoding'] ?? 0; + + $this->write(json_encode($data, $flags)); + } + + private function getInputArgumentData(InputArgument $argument): array + { + return [ + 'name' => $argument->getName(), + 'is_required' => $argument->isRequired(), + 'is_array' => $argument->isArray(), + 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()), + 'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(), + ]; + } + + private function getInputOptionData(InputOption $option, bool $negated = false): array + { + return $negated ? [ + 'name' => '--no-'.$option->getName(), + 'shortcut' => '', + 'accept_value' => false, + 'is_value_required' => false, + 'is_multiple' => false, + 'description' => 'Negate the "--'.$option->getName().'" option', + 'default' => false, + ] : [ + 'name' => '--'.$option->getName(), + 'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '', + 'accept_value' => $option->acceptValue(), + 'is_value_required' => $option->isValueRequired(), + 'is_multiple' => $option->isArray(), + 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()), + 'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(), + ]; + } + + private function getInputDefinitionData(InputDefinition $definition): array + { + $inputArguments = []; + foreach ($definition->getArguments() as $name => $argument) { + $inputArguments[$name] = $this->getInputArgumentData($argument); + } + + $inputOptions = []; + foreach ($definition->getOptions() as $name => $option) { + $inputOptions[$name] = $this->getInputOptionData($option); + if ($option->isNegatable()) { + $inputOptions['no-'.$name] = $this->getInputOptionData($option, true); + } + } + + return ['arguments' => $inputArguments, 'options' => $inputOptions]; + } + + private function getCommandData(Command $command, bool $short = false): array + { + $data = [ + 'name' => $command->getName(), + 'description' => $command->getDescription(), + ]; + + if ($short) { + $data += [ + 'usage' => $command->getAliases(), + ]; + } else { + $command->mergeApplicationDefinition(false); + + $data += [ + 'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()), + 'help' => $command->getProcessedHelp(), + 'definition' => $this->getInputDefinitionData($command->getDefinition()), + ]; + } + + $data['hidden'] = $command->isHidden(); + + return $data; + } +} diff --git a/vendor/symfony/console/Descriptor/MarkdownDescriptor.php b/vendor/symfony/console/Descriptor/MarkdownDescriptor.php new file mode 100644 index 0000000..21ceca6 --- /dev/null +++ b/vendor/symfony/console/Descriptor/MarkdownDescriptor.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Markdown descriptor. + * + * @author Jean-François Simon + * + * @internal + */ +class MarkdownDescriptor extends Descriptor +{ + /** + * {@inheritdoc} + */ + public function describe(OutputInterface $output, object $object, array $options = []) + { + $decorated = $output->isDecorated(); + $output->setDecorated(false); + + parent::describe($output, $object, $options); + + $output->setDecorated($decorated); + } + + /** + * {@inheritdoc} + */ + protected function write(string $content, bool $decorated = true) + { + parent::write($content, $decorated); + } + + /** + * {@inheritdoc} + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + $this->write( + '#### `'.($argument->getName() ?: '')."`\n\n" + .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '') + .'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n" + .'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n" + .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`' + ); + } + + /** + * {@inheritdoc} + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + $name = '--'.$option->getName(); + if ($option->isNegatable()) { + $name .= '|--no-'.$option->getName(); + } + if ($option->getShortcut()) { + $name .= '|-'.str_replace('|', '|-', $option->getShortcut()).''; + } + + $this->write( + '#### `'.$name.'`'."\n\n" + .($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '') + .'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n" + .'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n" + .'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n" + .'* Is negatable: '.($option->isNegatable() ? 'yes' : 'no')."\n" + .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`' + ); + } + + /** + * {@inheritdoc} + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + if ($showArguments = \count($definition->getArguments()) > 0) { + $this->write('### Arguments'); + foreach ($definition->getArguments() as $argument) { + $this->write("\n\n"); + if (null !== $describeInputArgument = $this->describeInputArgument($argument)) { + $this->write($describeInputArgument); + } + } + } + + if (\count($definition->getOptions()) > 0) { + if ($showArguments) { + $this->write("\n\n"); + } + + $this->write('### Options'); + foreach ($definition->getOptions() as $option) { + $this->write("\n\n"); + if (null !== $describeInputOption = $this->describeInputOption($option)) { + $this->write($describeInputOption); + } + } + } + } + + /** + * {@inheritdoc} + */ + protected function describeCommand(Command $command, array $options = []) + { + if ($options['short'] ?? false) { + $this->write( + '`'.$command->getName()."`\n" + .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + .'### Usage'."\n\n" + .array_reduce($command->getAliases(), function ($carry, $usage) { + return $carry.'* `'.$usage.'`'."\n"; + }) + ); + + return; + } + + $command->mergeApplicationDefinition(false); + + $this->write( + '`'.$command->getName()."`\n" + .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + .'### Usage'."\n\n" + .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) { + return $carry.'* `'.$usage.'`'."\n"; + }) + ); + + if ($help = $command->getProcessedHelp()) { + $this->write("\n"); + $this->write($help); + } + + $definition = $command->getDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->write("\n\n"); + $this->describeInputDefinition($definition); + } + } + + /** + * {@inheritdoc} + */ + protected function describeApplication(Application $application, array $options = []) + { + $describedNamespace = $options['namespace'] ?? null; + $description = new ApplicationDescription($application, $describedNamespace); + $title = $this->getApplicationTitle($application); + + $this->write($title."\n".str_repeat('=', Helper::width($title))); + + foreach ($description->getNamespaces() as $namespace) { + if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->write("\n\n"); + $this->write('**'.$namespace['id'].':**'); + } + + $this->write("\n\n"); + $this->write(implode("\n", array_map(function ($commandName) use ($description) { + return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())); + }, $namespace['commands']))); + } + + foreach ($description->getCommands() as $command) { + $this->write("\n\n"); + if (null !== $describeCommand = $this->describeCommand($command, $options)) { + $this->write($describeCommand); + } + } + } + + private function getApplicationTitle(Application $application): string + { + if ('UNKNOWN' !== $application->getName()) { + if ('UNKNOWN' !== $application->getVersion()) { + return sprintf('%s %s', $application->getName(), $application->getVersion()); + } + + return $application->getName(); + } + + return 'Console Tool'; + } +} diff --git a/vendor/symfony/console/Descriptor/TextDescriptor.php b/vendor/symfony/console/Descriptor/TextDescriptor.php new file mode 100644 index 0000000..fbb140a --- /dev/null +++ b/vendor/symfony/console/Descriptor/TextDescriptor.php @@ -0,0 +1,341 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * Text descriptor. + * + * @author Jean-François Simon + * + * @internal + */ +class TextDescriptor extends Descriptor +{ + /** + * {@inheritdoc} + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); + } else { + $default = ''; + } + + $totalWidth = $options['total_width'] ?? Helper::width($argument->getName()); + $spacingWidth = $totalWidth - \strlen($argument->getName()); + + $this->writeText(sprintf(' %s %s%s%s', + $argument->getName(), + str_repeat(' ', $spacingWidth), + // + 4 = 2 spaces before , 2 spaces after + preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()), + $default + ), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); + } else { + $default = ''; + } + + $value = ''; + if ($option->acceptValue()) { + $value = '='.strtoupper($option->getName()); + + if ($option->isValueOptional()) { + $value = '['.$value.']'; + } + } + + $totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]); + $synopsis = sprintf('%s%s', + $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', + sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value) + ); + + $spacingWidth = $totalWidth - Helper::width($synopsis); + + $this->writeText(sprintf(' %s %s%s%s%s', + $synopsis, + str_repeat(' ', $spacingWidth), + // + 4 = 2 spaces before , 2 spaces after + preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()), + $default, + $option->isArray() ? ' (multiple values allowed)' : '' + ), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); + foreach ($definition->getArguments() as $argument) { + $totalWidth = max($totalWidth, Helper::width($argument->getName())); + } + + if ($definition->getArguments()) { + $this->writeText('Arguments:', $options); + $this->writeText("\n"); + foreach ($definition->getArguments() as $argument) { + $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); + $this->writeText("\n"); + } + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); + } + + if ($definition->getOptions()) { + $laterOptions = []; + + $this->writeText('Options:', $options); + foreach ($definition->getOptions() as $option) { + if (\strlen($option->getShortcut() ?? '') > 1) { + $laterOptions[] = $option; + continue; + } + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + foreach ($laterOptions as $option) { + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + } + } + + /** + * {@inheritdoc} + */ + protected function describeCommand(Command $command, array $options = []) + { + $command->mergeApplicationDefinition(false); + + if ($description = $command->getDescription()) { + $this->writeText('Description:', $options); + $this->writeText("\n"); + $this->writeText(' '.$description); + $this->writeText("\n\n"); + } + + $this->writeText('Usage:', $options); + foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { + $this->writeText("\n"); + $this->writeText(' '.OutputFormatter::escape($usage), $options); + } + $this->writeText("\n"); + + $definition = $command->getDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); + $this->writeText("\n"); + } + + $help = $command->getProcessedHelp(); + if ($help && $help !== $description) { + $this->writeText("\n"); + $this->writeText('Help:', $options); + $this->writeText("\n"); + $this->writeText(' '.str_replace("\n", "\n ", $help), $options); + $this->writeText("\n"); + } + } + + /** + * {@inheritdoc} + */ + protected function describeApplication(Application $application, array $options = []) + { + $describedNamespace = $options['namespace'] ?? null; + $description = new ApplicationDescription($application, $describedNamespace); + + if (isset($options['raw_text']) && $options['raw_text']) { + $width = $this->getColumnWidth($description->getCommands()); + + foreach ($description->getCommands() as $command) { + $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options); + $this->writeText("\n"); + } + } else { + if ('' != $help = $application->getHelp()) { + $this->writeText("$help\n\n", $options); + } + + $this->writeText("Usage:\n", $options); + $this->writeText(" command [options] [arguments]\n\n", $options); + + $this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options); + + $this->writeText("\n"); + $this->writeText("\n"); + + $commands = $description->getCommands(); + $namespaces = $description->getNamespaces(); + if ($describedNamespace && $namespaces) { + // make sure all alias commands are included when describing a specific namespace + $describedNamespaceInfo = reset($namespaces); + foreach ($describedNamespaceInfo['commands'] as $name) { + $commands[$name] = $description->getCommand($name); + } + } + + // calculate max. width based on available commands per namespace + $width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) { + return array_intersect($namespace['commands'], array_keys($commands)); + }, array_values($namespaces))))); + + if ($describedNamespace) { + $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); + } else { + $this->writeText('Available commands:', $options); + } + + foreach ($namespaces as $namespace) { + $namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) { + return isset($commands[$name]); + }); + + if (!$namespace['commands']) { + continue; + } + + if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->writeText("\n"); + $this->writeText(' '.$namespace['id'].'', $options); + } + + foreach ($namespace['commands'] as $name) { + $this->writeText("\n"); + $spacingWidth = $width - Helper::width($name); + $command = $commands[$name]; + $commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : ''; + $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options); + } + } + + $this->writeText("\n"); + } + } + + /** + * {@inheritdoc} + */ + private function writeText(string $content, array $options = []) + { + $this->write( + isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, + isset($options['raw_output']) ? !$options['raw_output'] : true + ); + } + + /** + * Formats command aliases to show them in the command description. + */ + private function getCommandAliasesText(Command $command): string + { + $text = ''; + $aliases = $command->getAliases(); + + if ($aliases) { + $text = '['.implode('|', $aliases).'] '; + } + + return $text; + } + + /** + * Formats input option/argument default value. + * + * @param mixed $default + */ + private function formatDefaultValue($default): string + { + if (\INF === $default) { + return 'INF'; + } + + if (\is_string($default)) { + $default = OutputFormatter::escape($default); + } elseif (\is_array($default)) { + foreach ($default as $key => $value) { + if (\is_string($value)) { + $default[$key] = OutputFormatter::escape($value); + } + } + } + + return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); + } + + /** + * @param array $commands + */ + private function getColumnWidth(array $commands): int + { + $widths = []; + + foreach ($commands as $command) { + if ($command instanceof Command) { + $widths[] = Helper::width($command->getName()); + foreach ($command->getAliases() as $alias) { + $widths[] = Helper::width($alias); + } + } else { + $widths[] = Helper::width($command); + } + } + + return $widths ? max($widths) + 2 : 0; + } + + /** + * @param InputOption[] $options + */ + private function calculateTotalWidthForOptions(array $options): int + { + $totalWidth = 0; + foreach ($options as $option) { + // "-" + shortcut + ", --" + name + $nameLength = 1 + max(Helper::width($option->getShortcut()), 1) + 4 + Helper::width($option->getName()); + if ($option->isNegatable()) { + $nameLength += 6 + Helper::width($option->getName()); // |--no- + name + } elseif ($option->acceptValue()) { + $valueLength = 1 + Helper::width($option->getName()); // = + value + $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] + + $nameLength += $valueLength; + } + $totalWidth = max($totalWidth, $nameLength); + } + + return $totalWidth; + } +} diff --git a/vendor/symfony/console/Descriptor/XmlDescriptor.php b/vendor/symfony/console/Descriptor/XmlDescriptor.php new file mode 100644 index 0000000..4f7cd8b --- /dev/null +++ b/vendor/symfony/console/Descriptor/XmlDescriptor.php @@ -0,0 +1,247 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * XML descriptor. + * + * @author Jean-François Simon + * + * @internal + */ +class XmlDescriptor extends Descriptor +{ + public function getInputDefinitionDocument(InputDefinition $definition): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($definitionXML = $dom->createElement('definition')); + + $definitionXML->appendChild($argumentsXML = $dom->createElement('arguments')); + foreach ($definition->getArguments() as $argument) { + $this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument)); + } + + $definitionXML->appendChild($optionsXML = $dom->createElement('options')); + foreach ($definition->getOptions() as $option) { + $this->appendDocument($optionsXML, $this->getInputOptionDocument($option)); + } + + return $dom; + } + + public function getCommandDocument(Command $command, bool $short = false): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($commandXML = $dom->createElement('command')); + + $commandXML->setAttribute('id', $command->getName()); + $commandXML->setAttribute('name', $command->getName()); + $commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0); + + $commandXML->appendChild($usagesXML = $dom->createElement('usages')); + + $commandXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription()))); + + if ($short) { + foreach ($command->getAliases() as $usage) { + $usagesXML->appendChild($dom->createElement('usage', $usage)); + } + } else { + $command->mergeApplicationDefinition(false); + + foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) { + $usagesXML->appendChild($dom->createElement('usage', $usage)); + } + + $commandXML->appendChild($helpXML = $dom->createElement('help')); + $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp()))); + + $definitionXML = $this->getInputDefinitionDocument($command->getDefinition()); + $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0)); + } + + return $dom; + } + + public function getApplicationDocument(Application $application, string $namespace = null, bool $short = false): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($rootXml = $dom->createElement('symfony')); + + if ('UNKNOWN' !== $application->getName()) { + $rootXml->setAttribute('name', $application->getName()); + if ('UNKNOWN' !== $application->getVersion()) { + $rootXml->setAttribute('version', $application->getVersion()); + } + } + + $rootXml->appendChild($commandsXML = $dom->createElement('commands')); + + $description = new ApplicationDescription($application, $namespace, true); + + if ($namespace) { + $commandsXML->setAttribute('namespace', $namespace); + } + + foreach ($description->getCommands() as $command) { + $this->appendDocument($commandsXML, $this->getCommandDocument($command, $short)); + } + + if (!$namespace) { + $rootXml->appendChild($namespacesXML = $dom->createElement('namespaces')); + + foreach ($description->getNamespaces() as $namespaceDescription) { + $namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace')); + $namespaceArrayXML->setAttribute('id', $namespaceDescription['id']); + + foreach ($namespaceDescription['commands'] as $name) { + $namespaceArrayXML->appendChild($commandXML = $dom->createElement('command')); + $commandXML->appendChild($dom->createTextNode($name)); + } + } + } + + return $dom; + } + + /** + * {@inheritdoc} + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + $this->writeDocument($this->getInputArgumentDocument($argument)); + } + + /** + * {@inheritdoc} + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + $this->writeDocument($this->getInputOptionDocument($option)); + } + + /** + * {@inheritdoc} + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + $this->writeDocument($this->getInputDefinitionDocument($definition)); + } + + /** + * {@inheritdoc} + */ + protected function describeCommand(Command $command, array $options = []) + { + $this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false)); + } + + /** + * {@inheritdoc} + */ + protected function describeApplication(Application $application, array $options = []) + { + $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false)); + } + + /** + * Appends document children to parent node. + */ + private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent) + { + foreach ($importedParent->childNodes as $childNode) { + $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true)); + } + } + + /** + * Writes DOM document. + */ + private function writeDocument(\DOMDocument $dom) + { + $dom->formatOutput = true; + $this->write($dom->saveXML()); + } + + private function getInputArgumentDocument(InputArgument $argument): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $dom->appendChild($objectXML = $dom->createElement('argument')); + $objectXML->setAttribute('name', $argument->getName()); + $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0); + $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0); + $objectXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode($argument->getDescription())); + + $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); + $defaults = \is_array($argument->getDefault()) ? $argument->getDefault() : (\is_bool($argument->getDefault()) ? [var_export($argument->getDefault(), true)] : ($argument->getDefault() ? [$argument->getDefault()] : [])); + foreach ($defaults as $default) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->appendChild($dom->createTextNode($default)); + } + + return $dom; + } + + private function getInputOptionDocument(InputOption $option): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $dom->appendChild($objectXML = $dom->createElement('option')); + $objectXML->setAttribute('name', '--'.$option->getName()); + $pos = strpos($option->getShortcut() ?? '', '|'); + if (false !== $pos) { + $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos)); + $objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut())); + } else { + $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); + } + $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); + $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); + $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); + $objectXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); + + if ($option->acceptValue()) { + $defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : [])); + $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); + + if (!empty($defaults)) { + foreach ($defaults as $default) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->appendChild($dom->createTextNode($default)); + } + } + } + + if ($option->isNegatable()) { + $dom->appendChild($objectXML = $dom->createElement('option')); + $objectXML->setAttribute('name', '--no-'.$option->getName()); + $objectXML->setAttribute('shortcut', ''); + $objectXML->setAttribute('accept_value', 0); + $objectXML->setAttribute('is_value_required', 0); + $objectXML->setAttribute('is_multiple', 0); + $objectXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option')); + } + + return $dom; + } +} diff --git a/vendor/symfony/console/Event/ConsoleCommandEvent.php b/vendor/symfony/console/Event/ConsoleCommandEvent.php new file mode 100644 index 0000000..08bd18f --- /dev/null +++ b/vendor/symfony/console/Event/ConsoleCommandEvent.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +/** + * Allows to do things before the command is executed, like skipping the command or changing the input. + * + * @author Fabien Potencier + */ +final class ConsoleCommandEvent extends ConsoleEvent +{ + /** + * The return code for skipped commands, this will also be passed into the terminate event. + */ + public const RETURN_CODE_DISABLED = 113; + + /** + * Indicates if the command should be run or skipped. + */ + private $commandShouldRun = true; + + /** + * Disables the command, so it won't be run. + */ + public function disableCommand(): bool + { + return $this->commandShouldRun = false; + } + + public function enableCommand(): bool + { + return $this->commandShouldRun = true; + } + + /** + * Returns true if the command is runnable, false otherwise. + */ + public function commandShouldRun(): bool + { + return $this->commandShouldRun; + } +} diff --git a/vendor/symfony/console/Event/ConsoleErrorEvent.php b/vendor/symfony/console/Event/ConsoleErrorEvent.php new file mode 100644 index 0000000..57d9b38 --- /dev/null +++ b/vendor/symfony/console/Event/ConsoleErrorEvent.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Allows to handle throwables thrown while running a command. + * + * @author Wouter de Jong + */ +final class ConsoleErrorEvent extends ConsoleEvent +{ + private $error; + private $exitCode; + + public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, Command $command = null) + { + parent::__construct($command, $input, $output); + + $this->error = $error; + } + + public function getError(): \Throwable + { + return $this->error; + } + + public function setError(\Throwable $error): void + { + $this->error = $error; + } + + public function setExitCode(int $exitCode): void + { + $this->exitCode = $exitCode; + + $r = new \ReflectionProperty($this->error, 'code'); + $r->setAccessible(true); + $r->setValue($this->error, $this->exitCode); + } + + public function getExitCode(): int + { + return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1); + } +} diff --git a/vendor/symfony/console/Event/ConsoleEvent.php b/vendor/symfony/console/Event/ConsoleEvent.php new file mode 100644 index 0000000..be7937d --- /dev/null +++ b/vendor/symfony/console/Event/ConsoleEvent.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Allows to inspect input and output of a command. + * + * @author Francesco Levorato + */ +class ConsoleEvent extends Event +{ + protected $command; + + private $input; + private $output; + + public function __construct(?Command $command, InputInterface $input, OutputInterface $output) + { + $this->command = $command; + $this->input = $input; + $this->output = $output; + } + + /** + * Gets the command that is executed. + * + * @return Command|null + */ + public function getCommand() + { + return $this->command; + } + + /** + * Gets the input instance. + * + * @return InputInterface + */ + public function getInput() + { + return $this->input; + } + + /** + * Gets the output instance. + * + * @return OutputInterface + */ + public function getOutput() + { + return $this->output; + } +} diff --git a/vendor/symfony/console/Event/ConsoleSignalEvent.php b/vendor/symfony/console/Event/ConsoleSignalEvent.php new file mode 100644 index 0000000..ef13ed2 --- /dev/null +++ b/vendor/symfony/console/Event/ConsoleSignalEvent.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author marie + */ +final class ConsoleSignalEvent extends ConsoleEvent +{ + private $handlingSignal; + + public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal) + { + parent::__construct($command, $input, $output); + $this->handlingSignal = $handlingSignal; + } + + public function getHandlingSignal(): int + { + return $this->handlingSignal; + } +} diff --git a/vendor/symfony/console/Event/ConsoleTerminateEvent.php b/vendor/symfony/console/Event/ConsoleTerminateEvent.php new file mode 100644 index 0000000..190038d --- /dev/null +++ b/vendor/symfony/console/Event/ConsoleTerminateEvent.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Allows to manipulate the exit code of a command after its execution. + * + * @author Francesco Levorato + */ +final class ConsoleTerminateEvent extends ConsoleEvent +{ + private $exitCode; + + public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $exitCode) + { + parent::__construct($command, $input, $output); + + $this->setExitCode($exitCode); + } + + public function setExitCode(int $exitCode): void + { + $this->exitCode = $exitCode; + } + + public function getExitCode(): int + { + return $this->exitCode; + } +} diff --git a/vendor/symfony/console/EventListener/ErrorListener.php b/vendor/symfony/console/EventListener/ErrorListener.php new file mode 100644 index 0000000..897d985 --- /dev/null +++ b/vendor/symfony/console/EventListener/ErrorListener.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * @author James Halsall + * @author Robin Chalas + */ +class ErrorListener implements EventSubscriberInterface +{ + private $logger; + + public function __construct(LoggerInterface $logger = null) + { + $this->logger = $logger; + } + + public function onConsoleError(ConsoleErrorEvent $event) + { + if (null === $this->logger) { + return; + } + + $error = $event->getError(); + + if (!$inputString = $this->getInputString($event)) { + $this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]); + + return; + } + + $this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]); + } + + public function onConsoleTerminate(ConsoleTerminateEvent $event) + { + if (null === $this->logger) { + return; + } + + $exitCode = $event->getExitCode(); + + if (0 === $exitCode) { + return; + } + + if (!$inputString = $this->getInputString($event)) { + $this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]); + + return; + } + + $this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]); + } + + public static function getSubscribedEvents() + { + return [ + ConsoleEvents::ERROR => ['onConsoleError', -128], + ConsoleEvents::TERMINATE => ['onConsoleTerminate', -128], + ]; + } + + private static function getInputString(ConsoleEvent $event): ?string + { + $commandName = $event->getCommand() ? $event->getCommand()->getName() : null; + $input = $event->getInput(); + + if (method_exists($input, '__toString')) { + if ($commandName) { + return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input); + } + + return (string) $input; + } + + return $commandName; + } +} diff --git a/vendor/symfony/console/Exception/CommandNotFoundException.php b/vendor/symfony/console/Exception/CommandNotFoundException.php new file mode 100644 index 0000000..910ae19 --- /dev/null +++ b/vendor/symfony/console/Exception/CommandNotFoundException.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents an incorrect command name typed in the console. + * + * @author Jérôme Tamarelle + */ +class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface +{ + private $alternatives; + + /** + * @param string $message Exception message to throw + * @param string[] $alternatives List of similar defined names + * @param int $code Exception code + * @param \Throwable|null $previous Previous exception used for the exception chaining + */ + public function __construct(string $message, array $alternatives = [], int $code = 0, \Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + + $this->alternatives = $alternatives; + } + + /** + * @return string[] + */ + public function getAlternatives() + { + return $this->alternatives; + } +} diff --git a/vendor/symfony/console/Exception/ExceptionInterface.php b/vendor/symfony/console/Exception/ExceptionInterface.php new file mode 100644 index 0000000..1624e13 --- /dev/null +++ b/vendor/symfony/console/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * ExceptionInterface. + * + * @author Jérôme Tamarelle + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/vendor/symfony/console/Exception/InvalidArgumentException.php b/vendor/symfony/console/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..07cc0b6 --- /dev/null +++ b/vendor/symfony/console/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Jérôme Tamarelle + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/console/Exception/InvalidOptionException.php b/vendor/symfony/console/Exception/InvalidOptionException.php new file mode 100644 index 0000000..b2eec61 --- /dev/null +++ b/vendor/symfony/console/Exception/InvalidOptionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents an incorrect option name typed in the console. + * + * @author Jérôme Tamarelle + */ +class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/console/Exception/LogicException.php b/vendor/symfony/console/Exception/LogicException.php new file mode 100644 index 0000000..fc37b8d --- /dev/null +++ b/vendor/symfony/console/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Jérôme Tamarelle + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/console/Exception/MissingInputException.php b/vendor/symfony/console/Exception/MissingInputException.php new file mode 100644 index 0000000..04f02ad --- /dev/null +++ b/vendor/symfony/console/Exception/MissingInputException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents failure to read input from stdin. + * + * @author Gabriel Ostrolucký + */ +class MissingInputException extends RuntimeException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/console/Exception/NamespaceNotFoundException.php b/vendor/symfony/console/Exception/NamespaceNotFoundException.php new file mode 100644 index 0000000..dd16e45 --- /dev/null +++ b/vendor/symfony/console/Exception/NamespaceNotFoundException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents an incorrect namespace typed in the console. + * + * @author Pierre du Plessis + */ +class NamespaceNotFoundException extends CommandNotFoundException +{ +} diff --git a/vendor/symfony/console/Exception/RuntimeException.php b/vendor/symfony/console/Exception/RuntimeException.php new file mode 100644 index 0000000..51d7d80 --- /dev/null +++ b/vendor/symfony/console/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Jérôme Tamarelle + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/console/Formatter/NullOutputFormatter.php b/vendor/symfony/console/Formatter/NullOutputFormatter.php new file mode 100644 index 0000000..d770e14 --- /dev/null +++ b/vendor/symfony/console/Formatter/NullOutputFormatter.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * @author Tien Xuan Vo + */ +final class NullOutputFormatter implements OutputFormatterInterface +{ + private $style; + + /** + * {@inheritdoc} + */ + public function format(?string $message): ?string + { + return null; + } + + /** + * {@inheritdoc} + */ + public function getStyle(string $name): OutputFormatterStyleInterface + { + // to comply with the interface we must return a OutputFormatterStyleInterface + return $this->style ?? $this->style = new NullOutputFormatterStyle(); + } + + /** + * {@inheritdoc} + */ + public function hasStyle(string $name): bool + { + return false; + } + + /** + * {@inheritdoc} + */ + public function isDecorated(): bool + { + return false; + } + + /** + * {@inheritdoc} + */ + public function setDecorated(bool $decorated): void + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function setStyle(string $name, OutputFormatterStyleInterface $style): void + { + // do nothing + } +} diff --git a/vendor/symfony/console/Formatter/NullOutputFormatterStyle.php b/vendor/symfony/console/Formatter/NullOutputFormatterStyle.php new file mode 100644 index 0000000..bfd0afe --- /dev/null +++ b/vendor/symfony/console/Formatter/NullOutputFormatterStyle.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * @author Tien Xuan Vo + */ +final class NullOutputFormatterStyle implements OutputFormatterStyleInterface +{ + /** + * {@inheritdoc} + */ + public function apply(string $text): string + { + return $text; + } + + /** + * {@inheritdoc} + */ + public function setBackground(string $color = null): void + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function setForeground(string $color = null): void + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function setOption(string $option): void + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function setOptions(array $options): void + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function unsetOption(string $option): void + { + // do nothing + } +} diff --git a/vendor/symfony/console/Formatter/OutputFormatter.php b/vendor/symfony/console/Formatter/OutputFormatter.php new file mode 100644 index 0000000..83ec49a --- /dev/null +++ b/vendor/symfony/console/Formatter/OutputFormatter.php @@ -0,0 +1,283 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * Formatter class for console output. + * + * @author Konstantin Kudryashov + * @author Roland Franssen + */ +class OutputFormatter implements WrappableOutputFormatterInterface +{ + private $decorated; + private $styles = []; + private $styleStack; + + public function __clone() + { + $this->styleStack = clone $this->styleStack; + foreach ($this->styles as $key => $value) { + $this->styles[$key] = clone $value; + } + } + + /** + * Escapes "<" special char in given text. + * + * @return string + */ + public static function escape(string $text) + { + $text = preg_replace('/([^\\\\]?) FormatterStyle" instances + */ + public function __construct(bool $decorated = false, array $styles = []) + { + $this->decorated = $decorated; + + $this->setStyle('error', new OutputFormatterStyle('white', 'red')); + $this->setStyle('info', new OutputFormatterStyle('green')); + $this->setStyle('comment', new OutputFormatterStyle('yellow')); + $this->setStyle('question', new OutputFormatterStyle('black', 'cyan')); + + foreach ($styles as $name => $style) { + $this->setStyle($name, $style); + } + + $this->styleStack = new OutputFormatterStyleStack(); + } + + /** + * {@inheritdoc} + */ + public function setDecorated(bool $decorated) + { + $this->decorated = $decorated; + } + + /** + * {@inheritdoc} + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * {@inheritdoc} + */ + public function setStyle(string $name, OutputFormatterStyleInterface $style) + { + $this->styles[strtolower($name)] = $style; + } + + /** + * {@inheritdoc} + */ + public function hasStyle(string $name) + { + return isset($this->styles[strtolower($name)]); + } + + /** + * {@inheritdoc} + */ + public function getStyle(string $name) + { + if (!$this->hasStyle($name)) { + throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name)); + } + + return $this->styles[strtolower($name)]; + } + + /** + * {@inheritdoc} + */ + public function format(?string $message) + { + return $this->formatAndWrap($message, 0); + } + + /** + * {@inheritdoc} + */ + public function formatAndWrap(?string $message, int $width) + { + $offset = 0; + $output = ''; + $tagRegex = '[a-z][^<>]*+'; + $currentLineLength = 0; + preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + if (0 != $pos && '\\' == $message[$pos - 1]) { + continue; + } + + // add the text up to the next tag + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); + $offset = $pos + \strlen($text); + + // opening tag? + if ($open = '/' != $text[1]) { + $tag = $matches[1][$i][0]; + } else { + $tag = $matches[3][$i][0] ?? ''; + } + + if (!$open && !$tag) { + // + $this->styleStack->pop(); + } elseif (null === $style = $this->createStyleFromString($tag)) { + $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength); + } elseif ($open) { + $this->styleStack->push($style); + } else { + $this->styleStack->pop($style); + } + } + + $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); + + if (str_contains($output, "\0")) { + return strtr($output, ["\0" => '\\', '\\<' => '<']); + } + + return str_replace('\\<', '<', $output); + } + + /** + * @return OutputFormatterStyleStack + */ + public function getStyleStack() + { + return $this->styleStack; + } + + /** + * Tries to create new style instance from string. + */ + private function createStyleFromString(string $string): ?OutputFormatterStyleInterface + { + if (isset($this->styles[$string])) { + return $this->styles[$string]; + } + + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) { + return null; + } + + $style = new OutputFormatterStyle(); + foreach ($matches as $match) { + array_shift($match); + $match[0] = strtolower($match[0]); + + if ('fg' == $match[0]) { + $style->setForeground(strtolower($match[1])); + } elseif ('bg' == $match[0]) { + $style->setBackground(strtolower($match[1])); + } elseif ('href' === $match[0]) { + $style->setHref($match[1]); + } elseif ('options' === $match[0]) { + preg_match_all('([^,;]+)', strtolower($match[1]), $options); + $options = array_shift($options); + foreach ($options as $option) { + $style->setOption($option); + } + } else { + return null; + } + } + + return $style; + } + + /** + * Applies current style from stack to text, if must be applied. + */ + private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string + { + if ('' === $text) { + return ''; + } + + if (!$width) { + return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text; + } + + if (!$currentLineLength && '' !== $current) { + $text = ltrim($text); + } + + if ($currentLineLength) { + $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n"; + $text = substr($text, $i); + } else { + $prefix = ''; + } + + preg_match('~(\\n)$~', $text, $matches); + $text = $prefix.preg_replace('~([^\\n]{'.$width.'})\\ *~', "\$1\n", $text); + $text = rtrim($text, "\n").($matches[1] ?? ''); + + if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) { + $text = "\n".$text; + } + + $lines = explode("\n", $text); + + foreach ($lines as $line) { + $currentLineLength += \strlen($line); + if ($width <= $currentLineLength) { + $currentLineLength = 0; + } + } + + if ($this->isDecorated()) { + foreach ($lines as $i => $line) { + $lines[$i] = $this->styleStack->getCurrent()->apply($line); + } + } + + return implode("\n", $lines); + } +} diff --git a/vendor/symfony/console/Formatter/OutputFormatterInterface.php b/vendor/symfony/console/Formatter/OutputFormatterInterface.php new file mode 100644 index 0000000..0b5f839 --- /dev/null +++ b/vendor/symfony/console/Formatter/OutputFormatterInterface.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * Formatter interface for console output. + * + * @author Konstantin Kudryashov + */ +interface OutputFormatterInterface +{ + /** + * Sets the decorated flag. + */ + public function setDecorated(bool $decorated); + + /** + * Whether the output will decorate messages. + * + * @return bool + */ + public function isDecorated(); + + /** + * Sets a new style. + */ + public function setStyle(string $name, OutputFormatterStyleInterface $style); + + /** + * Checks if output formatter has style with specified name. + * + * @return bool + */ + public function hasStyle(string $name); + + /** + * Gets style options from style with specified name. + * + * @return OutputFormatterStyleInterface + * + * @throws \InvalidArgumentException When style isn't defined + */ + public function getStyle(string $name); + + /** + * Formats a message according to the given styles. + * + * @return string|null + */ + public function format(?string $message); +} diff --git a/vendor/symfony/console/Formatter/OutputFormatterStyle.php b/vendor/symfony/console/Formatter/OutputFormatterStyle.php new file mode 100644 index 0000000..0fb36ac --- /dev/null +++ b/vendor/symfony/console/Formatter/OutputFormatterStyle.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +use Symfony\Component\Console\Color; + +/** + * Formatter style class for defining styles. + * + * @author Konstantin Kudryashov + */ +class OutputFormatterStyle implements OutputFormatterStyleInterface +{ + private $color; + private $foreground; + private $background; + private $options; + private $href; + private $handlesHrefGracefully; + + /** + * Initializes output formatter style. + * + * @param string|null $foreground The style foreground color name + * @param string|null $background The style background color name + */ + public function __construct(string $foreground = null, string $background = null, array $options = []) + { + $this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options); + } + + /** + * {@inheritdoc} + */ + public function setForeground(string $color = null) + { + $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options); + } + + /** + * {@inheritdoc} + */ + public function setBackground(string $color = null) + { + $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options); + } + + public function setHref(string $url): void + { + $this->href = $url; + } + + /** + * {@inheritdoc} + */ + public function setOption(string $option) + { + $this->options[] = $option; + $this->color = new Color($this->foreground, $this->background, $this->options); + } + + /** + * {@inheritdoc} + */ + public function unsetOption(string $option) + { + $pos = array_search($option, $this->options); + if (false !== $pos) { + unset($this->options[$pos]); + } + + $this->color = new Color($this->foreground, $this->background, $this->options); + } + + /** + * {@inheritdoc} + */ + public function setOptions(array $options) + { + $this->color = new Color($this->foreground, $this->background, $this->options = $options); + } + + /** + * {@inheritdoc} + */ + public function apply(string $text) + { + if (null === $this->handlesHrefGracefully) { + $this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') + && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100); + } + + if (null !== $this->href && $this->handlesHrefGracefully) { + $text = "\033]8;;$this->href\033\\$text\033]8;;\033\\"; + } + + return $this->color->apply($text); + } +} diff --git a/vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php b/vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php new file mode 100644 index 0000000..b30560d --- /dev/null +++ b/vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * Formatter style interface for defining styles. + * + * @author Konstantin Kudryashov + */ +interface OutputFormatterStyleInterface +{ + /** + * Sets style foreground color. + */ + public function setForeground(string $color = null); + + /** + * Sets style background color. + */ + public function setBackground(string $color = null); + + /** + * Sets some specific style option. + */ + public function setOption(string $option); + + /** + * Unsets some specific style option. + */ + public function unsetOption(string $option); + + /** + * Sets multiple style options at once. + */ + public function setOptions(array $options); + + /** + * Applies the style to a given text. + * + * @return string + */ + public function apply(string $text); +} diff --git a/vendor/symfony/console/Formatter/OutputFormatterStyleStack.php b/vendor/symfony/console/Formatter/OutputFormatterStyleStack.php new file mode 100644 index 0000000..fc48dc0 --- /dev/null +++ b/vendor/symfony/console/Formatter/OutputFormatterStyleStack.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Contracts\Service\ResetInterface; + +/** + * @author Jean-François Simon + */ +class OutputFormatterStyleStack implements ResetInterface +{ + /** + * @var OutputFormatterStyleInterface[] + */ + private $styles; + + private $emptyStyle; + + public function __construct(OutputFormatterStyleInterface $emptyStyle = null) + { + $this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle(); + $this->reset(); + } + + /** + * Resets stack (ie. empty internal arrays). + */ + public function reset() + { + $this->styles = []; + } + + /** + * Pushes a style in the stack. + */ + public function push(OutputFormatterStyleInterface $style) + { + $this->styles[] = $style; + } + + /** + * Pops a style from the stack. + * + * @return OutputFormatterStyleInterface + * + * @throws InvalidArgumentException When style tags incorrectly nested + */ + public function pop(OutputFormatterStyleInterface $style = null) + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + if (null === $style) { + return array_pop($this->styles); + } + + foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { + if ($style->apply('') === $stackedStyle->apply('')) { + $this->styles = \array_slice($this->styles, 0, $index); + + return $stackedStyle; + } + } + + throw new InvalidArgumentException('Incorrectly nested style tag found.'); + } + + /** + * Computes current style with stacks top codes. + * + * @return OutputFormatterStyle + */ + public function getCurrent() + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + return $this->styles[\count($this->styles) - 1]; + } + + /** + * @return $this + */ + public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle) + { + $this->emptyStyle = $emptyStyle; + + return $this; + } + + /** + * @return OutputFormatterStyleInterface + */ + public function getEmptyStyle() + { + return $this->emptyStyle; + } +} diff --git a/vendor/symfony/console/Formatter/WrappableOutputFormatterInterface.php b/vendor/symfony/console/Formatter/WrappableOutputFormatterInterface.php new file mode 100644 index 0000000..42319ee --- /dev/null +++ b/vendor/symfony/console/Formatter/WrappableOutputFormatterInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * Formatter interface for console output that supports word wrapping. + * + * @author Roland Franssen + */ +interface WrappableOutputFormatterInterface extends OutputFormatterInterface +{ + /** + * Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping). + */ + public function formatAndWrap(?string $message, int $width); +} diff --git a/vendor/symfony/console/Helper/DebugFormatterHelper.php b/vendor/symfony/console/Helper/DebugFormatterHelper.php new file mode 100644 index 0000000..e258ba0 --- /dev/null +++ b/vendor/symfony/console/Helper/DebugFormatterHelper.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * Helps outputting debug information when running an external program from a command. + * + * An external program can be a Process, an HTTP request, or anything else. + * + * @author Fabien Potencier + */ +class DebugFormatterHelper extends Helper +{ + private const COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default']; + private $started = []; + private $count = -1; + + /** + * Starts a debug formatting session. + * + * @return string + */ + public function start(string $id, string $message, string $prefix = 'RUN') + { + $this->started[$id] = ['border' => ++$this->count % \count(self::COLORS)]; + + return sprintf("%s %s %s\n", $this->getBorder($id), $prefix, $message); + } + + /** + * Adds progress to a formatting session. + * + * @return string + */ + public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR') + { + $message = ''; + + if ($error) { + if (isset($this->started[$id]['out'])) { + $message .= "\n"; + unset($this->started[$id]['out']); + } + if (!isset($this->started[$id]['err'])) { + $message .= sprintf('%s %s ', $this->getBorder($id), $errorPrefix); + $this->started[$id]['err'] = true; + } + + $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $errorPrefix), $buffer); + } else { + if (isset($this->started[$id]['err'])) { + $message .= "\n"; + unset($this->started[$id]['err']); + } + if (!isset($this->started[$id]['out'])) { + $message .= sprintf('%s %s ', $this->getBorder($id), $prefix); + $this->started[$id]['out'] = true; + } + + $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $prefix), $buffer); + } + + return $message; + } + + /** + * Stops a formatting session. + * + * @return string + */ + public function stop(string $id, string $message, bool $successful, string $prefix = 'RES') + { + $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : ''; + + if ($successful) { + return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); + } + + $message = sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); + + unset($this->started[$id]['out'], $this->started[$id]['err']); + + return $message; + } + + private function getBorder(string $id): string + { + return sprintf(' ', self::COLORS[$this->started[$id]['border']]); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'debug_formatter'; + } +} diff --git a/vendor/symfony/console/Helper/DescriptorHelper.php b/vendor/symfony/console/Helper/DescriptorHelper.php new file mode 100644 index 0000000..af85e9c --- /dev/null +++ b/vendor/symfony/console/Helper/DescriptorHelper.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Descriptor\DescriptorInterface; +use Symfony\Component\Console\Descriptor\JsonDescriptor; +use Symfony\Component\Console\Descriptor\MarkdownDescriptor; +use Symfony\Component\Console\Descriptor\TextDescriptor; +use Symfony\Component\Console\Descriptor\XmlDescriptor; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * This class adds helper method to describe objects in various formats. + * + * @author Jean-François Simon + */ +class DescriptorHelper extends Helper +{ + /** + * @var DescriptorInterface[] + */ + private $descriptors = []; + + public function __construct() + { + $this + ->register('txt', new TextDescriptor()) + ->register('xml', new XmlDescriptor()) + ->register('json', new JsonDescriptor()) + ->register('md', new MarkdownDescriptor()) + ; + } + + /** + * Describes an object if supported. + * + * Available options are: + * * format: string, the output format name + * * raw_text: boolean, sets output type as raw + * + * @throws InvalidArgumentException when the given format is not supported + */ + public function describe(OutputInterface $output, ?object $object, array $options = []) + { + $options = array_merge([ + 'raw_text' => false, + 'format' => 'txt', + ], $options); + + if (!isset($this->descriptors[$options['format']])) { + throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format'])); + } + + $descriptor = $this->descriptors[$options['format']]; + $descriptor->describe($output, $object, $options); + } + + /** + * Registers a descriptor. + * + * @return $this + */ + public function register(string $format, DescriptorInterface $descriptor) + { + $this->descriptors[$format] = $descriptor; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'descriptor'; + } + + public function getFormats(): array + { + return array_keys($this->descriptors); + } +} diff --git a/vendor/symfony/console/Helper/Dumper.php b/vendor/symfony/console/Helper/Dumper.php new file mode 100644 index 0000000..b013b6c --- /dev/null +++ b/vendor/symfony/console/Helper/Dumper.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\VarDumper\Cloner\ClonerInterface; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; + +/** + * @author Roland Franssen + */ +final class Dumper +{ + private $output; + private $dumper; + private $cloner; + private $handler; + + public function __construct(OutputInterface $output, CliDumper $dumper = null, ClonerInterface $cloner = null) + { + $this->output = $output; + $this->dumper = $dumper; + $this->cloner = $cloner; + + if (class_exists(CliDumper::class)) { + $this->handler = function ($var): string { + $dumper = $this->dumper ?? $this->dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR); + $dumper->setColors($this->output->isDecorated()); + + return rtrim($dumper->dump(($this->cloner ?? $this->cloner = new VarCloner())->cloneVar($var)->withRefHandles(false), true)); + }; + } else { + $this->handler = function ($var): string { + switch (true) { + case null === $var: + return 'null'; + case true === $var: + return 'true'; + case false === $var: + return 'false'; + case \is_string($var): + return '"'.$var.'"'; + default: + return rtrim(print_r($var, true)); + } + }; + } + } + + public function __invoke($var): string + { + return ($this->handler)($var); + } +} diff --git a/vendor/symfony/console/Helper/FormatterHelper.php b/vendor/symfony/console/Helper/FormatterHelper.php new file mode 100644 index 0000000..92d8dc7 --- /dev/null +++ b/vendor/symfony/console/Helper/FormatterHelper.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Formatter\OutputFormatter; + +/** + * The Formatter class provides helpers to format messages. + * + * @author Fabien Potencier + */ +class FormatterHelper extends Helper +{ + /** + * Formats a message within a section. + * + * @return string + */ + public function formatSection(string $section, string $message, string $style = 'info') + { + return sprintf('<%s>[%s] %s', $style, $section, $style, $message); + } + + /** + * Formats a message as a block of text. + * + * @param string|array $messages The message to write in the block + * + * @return string + */ + public function formatBlock($messages, string $style, bool $large = false) + { + if (!\is_array($messages)) { + $messages = [$messages]; + } + + $len = 0; + $lines = []; + foreach ($messages as $message) { + $message = OutputFormatter::escape($message); + $lines[] = sprintf($large ? ' %s ' : ' %s ', $message); + $len = max(self::width($message) + ($large ? 4 : 2), $len); + } + + $messages = $large ? [str_repeat(' ', $len)] : []; + for ($i = 0; isset($lines[$i]); ++$i) { + $messages[] = $lines[$i].str_repeat(' ', $len - self::width($lines[$i])); + } + if ($large) { + $messages[] = str_repeat(' ', $len); + } + + for ($i = 0; isset($messages[$i]); ++$i) { + $messages[$i] = sprintf('<%s>%s', $style, $messages[$i], $style); + } + + return implode("\n", $messages); + } + + /** + * Truncates a message to the given length. + * + * @return string + */ + public function truncate(string $message, int $length, string $suffix = '...') + { + $computedLength = $length - self::width($suffix); + + if ($computedLength > self::width($message)) { + return $message; + } + + return self::substr($message, 0, $length).$suffix; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'formatter'; + } +} diff --git a/vendor/symfony/console/Helper/Helper.php b/vendor/symfony/console/Helper/Helper.php new file mode 100644 index 0000000..50f51c7 --- /dev/null +++ b/vendor/symfony/console/Helper/Helper.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\String\UnicodeString; + +/** + * Helper is the base class for all helper classes. + * + * @author Fabien Potencier + */ +abstract class Helper implements HelperInterface +{ + protected $helperSet = null; + + /** + * {@inheritdoc} + */ + public function setHelperSet(HelperSet $helperSet = null) + { + $this->helperSet = $helperSet; + } + + /** + * {@inheritdoc} + */ + public function getHelperSet() + { + return $this->helperSet; + } + + /** + * Returns the length of a string, using mb_strwidth if it is available. + * + * @deprecated since Symfony 5.3 + * + * @return int + */ + public static function strlen(?string $string) + { + trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.', __METHOD__); + + return self::width($string); + } + + /** + * Returns the width of a string, using mb_strwidth if it is available. + * The width is how many characters positions the string will use. + */ + public static function width(?string $string): int + { + $string ?? $string = ''; + + if (preg_match('//u', $string)) { + return (new UnicodeString($string))->width(false); + } + + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return \strlen($string); + } + + return mb_strwidth($string, $encoding); + } + + /** + * Returns the length of a string, using mb_strlen if it is available. + * The length is related to how many bytes the string will use. + */ + public static function length(?string $string): int + { + $string ?? $string = ''; + + if (preg_match('//u', $string)) { + return (new UnicodeString($string))->length(); + } + + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return \strlen($string); + } + + return mb_strlen($string, $encoding); + } + + /** + * Returns the subset of a string, using mb_substr if it is available. + * + * @return string + */ + public static function substr(?string $string, int $from, int $length = null) + { + $string ?? $string = ''; + + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return substr($string, $from, $length); + } + + return mb_substr($string, $from, $length, $encoding); + } + + public static function formatTime($secs) + { + static $timeFormats = [ + [0, '< 1 sec'], + [1, '1 sec'], + [2, 'secs', 1], + [60, '1 min'], + [120, 'mins', 60], + [3600, '1 hr'], + [7200, 'hrs', 3600], + [86400, '1 day'], + [172800, 'days', 86400], + ]; + + foreach ($timeFormats as $index => $format) { + if ($secs >= $format[0]) { + if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0]) + || $index == \count($timeFormats) - 1 + ) { + if (2 == \count($format)) { + return $format[1]; + } + + return floor($secs / $format[2]).' '.$format[1]; + } + } + } + } + + public static function formatMemory(int $memory) + { + if ($memory >= 1024 * 1024 * 1024) { + return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); + } + + if ($memory >= 1024 * 1024) { + return sprintf('%.1f MiB', $memory / 1024 / 1024); + } + + if ($memory >= 1024) { + return sprintf('%d KiB', $memory / 1024); + } + + return sprintf('%d B', $memory); + } + + /** + * @deprecated since Symfony 5.3 + */ + public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string) + { + trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.', __METHOD__); + + return self::width(self::removeDecoration($formatter, $string)); + } + + public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string) + { + $isDecorated = $formatter->isDecorated(); + $formatter->setDecorated(false); + // remove <...> formatting + $string = $formatter->format($string ?? ''); + // remove already formatted characters + $string = preg_replace("/\033\[[^m]*m/", '', $string ?? ''); + $formatter->setDecorated($isDecorated); + + return $string; + } +} diff --git a/vendor/symfony/console/Helper/HelperInterface.php b/vendor/symfony/console/Helper/HelperInterface.php new file mode 100644 index 0000000..fc952b4 --- /dev/null +++ b/vendor/symfony/console/Helper/HelperInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * HelperInterface is the interface all helpers must implement. + * + * @author Fabien Potencier + */ +interface HelperInterface +{ + /** + * Sets the helper set associated with this helper. + */ + public function setHelperSet(HelperSet $helperSet = null); + + /** + * Gets the helper set associated with this helper. + * + * @return HelperSet|null + */ + public function getHelperSet(); + + /** + * Returns the canonical name of this helper. + * + * @return string + */ + public function getName(); +} diff --git a/vendor/symfony/console/Helper/HelperSet.php b/vendor/symfony/console/Helper/HelperSet.php new file mode 100644 index 0000000..719762d --- /dev/null +++ b/vendor/symfony/console/Helper/HelperSet.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * HelperSet represents a set of helpers to be used with a command. + * + * @author Fabien Potencier + * + * @implements \IteratorAggregate + */ +class HelperSet implements \IteratorAggregate +{ + /** @var array */ + private $helpers = []; + private $command; + + /** + * @param Helper[] $helpers An array of helper + */ + public function __construct(array $helpers = []) + { + foreach ($helpers as $alias => $helper) { + $this->set($helper, \is_int($alias) ? null : $alias); + } + } + + public function set(HelperInterface $helper, string $alias = null) + { + $this->helpers[$helper->getName()] = $helper; + if (null !== $alias) { + $this->helpers[$alias] = $helper; + } + + $helper->setHelperSet($this); + } + + /** + * Returns true if the helper if defined. + * + * @return bool + */ + public function has(string $name) + { + return isset($this->helpers[$name]); + } + + /** + * Gets a helper value. + * + * @return HelperInterface + * + * @throws InvalidArgumentException if the helper is not defined + */ + public function get(string $name) + { + if (!$this->has($name)) { + throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); + } + + return $this->helpers[$name]; + } + + /** + * @deprecated since Symfony 5.4 + */ + public function setCommand(Command $command = null) + { + trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__); + + $this->command = $command; + } + + /** + * Gets the command associated with this helper set. + * + * @return Command + * + * @deprecated since Symfony 5.4 + */ + public function getCommand() + { + trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__); + + return $this->command; + } + + /** + * @return \Traversable + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->helpers); + } +} diff --git a/vendor/symfony/console/Helper/InputAwareHelper.php b/vendor/symfony/console/Helper/InputAwareHelper.php new file mode 100644 index 0000000..0d0dba2 --- /dev/null +++ b/vendor/symfony/console/Helper/InputAwareHelper.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Input\InputAwareInterface; +use Symfony\Component\Console\Input\InputInterface; + +/** + * An implementation of InputAwareInterface for Helpers. + * + * @author Wouter J + */ +abstract class InputAwareHelper extends Helper implements InputAwareInterface +{ + protected $input; + + /** + * {@inheritdoc} + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + } +} diff --git a/vendor/symfony/console/Helper/ProcessHelper.php b/vendor/symfony/console/Helper/ProcessHelper.php new file mode 100644 index 0000000..4ea3d72 --- /dev/null +++ b/vendor/symfony/console/Helper/ProcessHelper.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; + +/** + * The ProcessHelper class provides helpers to run external processes. + * + * @author Fabien Potencier + * + * @final + */ +class ProcessHelper extends Helper +{ + /** + * Runs an external process. + * + * @param array|Process $cmd An instance of Process or an array of the command and arguments + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + */ + public function run(OutputInterface $output, $cmd, string $error = null, callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process + { + if (!class_exists(Process::class)) { + throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".'); + } + + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $formatter = $this->getHelperSet()->get('debug_formatter'); + + if ($cmd instanceof Process) { + $cmd = [$cmd]; + } + + if (!\is_array($cmd)) { + throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, get_debug_type($cmd))); + } + + if (\is_string($cmd[0] ?? null)) { + $process = new Process($cmd); + $cmd = []; + } elseif (($cmd[0] ?? null) instanceof Process) { + $process = $cmd[0]; + unset($cmd[0]); + } else { + throw new \InvalidArgumentException(sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__)); + } + + if ($verbosity <= $output->getVerbosity()) { + $output->write($formatter->start(spl_object_hash($process), $this->escapeString($process->getCommandLine()))); + } + + if ($output->isDebug()) { + $callback = $this->wrapCallback($output, $process, $callback); + } + + $process->run($callback, $cmd); + + if ($verbosity <= $output->getVerbosity()) { + $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode()); + $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful())); + } + + if (!$process->isSuccessful() && null !== $error) { + $output->writeln(sprintf('%s', $this->escapeString($error))); + } + + return $process; + } + + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @param array|Process $cmd An instance of Process or a command to run + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws ProcessFailedException + * + * @see run() + */ + public function mustRun(OutputInterface $output, $cmd, string $error = null, callable $callback = null): Process + { + $process = $this->run($output, $cmd, $error, $callback); + + if (!$process->isSuccessful()) { + throw new ProcessFailedException($process); + } + + return $process; + } + + /** + * Wraps a Process callback to add debugging output. + */ + public function wrapCallback(OutputInterface $output, Process $process, callable $callback = null): callable + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $formatter = $this->getHelperSet()->get('debug_formatter'); + + return function ($type, $buffer) use ($output, $process, $callback, $formatter) { + $output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), Process::ERR === $type)); + + if (null !== $callback) { + $callback($type, $buffer); + } + }; + } + + private function escapeString(string $str): string + { + return str_replace('<', '\\<', $str); + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'process'; + } +} diff --git a/vendor/symfony/console/Helper/ProgressBar.php b/vendor/symfony/console/Helper/ProgressBar.php new file mode 100644 index 0000000..b1fb213 --- /dev/null +++ b/vendor/symfony/console/Helper/ProgressBar.php @@ -0,0 +1,611 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Cursor; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Terminal; + +/** + * The ProgressBar provides helpers to display progress output. + * + * @author Fabien Potencier + * @author Chris Jones + */ +final class ProgressBar +{ + public const FORMAT_VERBOSE = 'verbose'; + public const FORMAT_VERY_VERBOSE = 'very_verbose'; + public const FORMAT_DEBUG = 'debug'; + public const FORMAT_NORMAL = 'normal'; + + private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax'; + private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax'; + private const FORMAT_DEBUG_NOMAX = 'debug_nomax'; + private const FORMAT_NORMAL_NOMAX = 'normal_nomax'; + + private $barWidth = 28; + private $barChar; + private $emptyBarChar = '-'; + private $progressChar = '>'; + private $format; + private $internalFormat; + private $redrawFreq = 1; + private $writeCount; + private $lastWriteTime; + private $minSecondsBetweenRedraws = 0; + private $maxSecondsBetweenRedraws = 1; + private $output; + private $step = 0; + private $max; + private $startTime; + private $stepWidth; + private $percent = 0.0; + private $formatLineCount; + private $messages = []; + private $overwrite = true; + private $terminal; + private $previousMessage; + private $cursor; + + private static $formatters; + private static $formats; + + /** + * @param int $max Maximum steps (0 if unknown) + */ + public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25) + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $this->output = $output; + $this->setMaxSteps($max); + $this->terminal = new Terminal(); + + if (0 < $minSecondsBetweenRedraws) { + $this->redrawFreq = null; + $this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws; + } + + if (!$this->output->isDecorated()) { + // disable overwrite when output does not support ANSI codes. + $this->overwrite = false; + + // set a reasonable redraw frequency so output isn't flooded + $this->redrawFreq = null; + } + + $this->startTime = time(); + $this->cursor = new Cursor($output); + } + + /** + * Sets a placeholder formatter for a given name. + * + * This method also allow you to override an existing placeholder. + * + * @param string $name The placeholder name (including the delimiter char like %) + * @param callable $callable A PHP callable + */ + public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void + { + if (!self::$formatters) { + self::$formatters = self::initPlaceholderFormatters(); + } + + self::$formatters[$name] = $callable; + } + + /** + * Gets the placeholder formatter for a given name. + * + * @param string $name The placeholder name (including the delimiter char like %) + */ + public static function getPlaceholderFormatterDefinition(string $name): ?callable + { + if (!self::$formatters) { + self::$formatters = self::initPlaceholderFormatters(); + } + + return self::$formatters[$name] ?? null; + } + + /** + * Sets a format for a given name. + * + * This method also allow you to override an existing format. + * + * @param string $name The format name + * @param string $format A format string + */ + public static function setFormatDefinition(string $name, string $format): void + { + if (!self::$formats) { + self::$formats = self::initFormats(); + } + + self::$formats[$name] = $format; + } + + /** + * Gets the format for a given name. + * + * @param string $name The format name + */ + public static function getFormatDefinition(string $name): ?string + { + if (!self::$formats) { + self::$formats = self::initFormats(); + } + + return self::$formats[$name] ?? null; + } + + /** + * Associates a text with a named placeholder. + * + * The text is displayed when the progress bar is rendered but only + * when the corresponding placeholder is part of the custom format line + * (by wrapping the name with %). + * + * @param string $message The text to associate with the placeholder + * @param string $name The name of the placeholder + */ + public function setMessage(string $message, string $name = 'message') + { + $this->messages[$name] = $message; + } + + public function getMessage(string $name = 'message') + { + return $this->messages[$name]; + } + + public function getStartTime(): int + { + return $this->startTime; + } + + public function getMaxSteps(): int + { + return $this->max; + } + + public function getProgress(): int + { + return $this->step; + } + + private function getStepWidth(): int + { + return $this->stepWidth; + } + + public function getProgressPercent(): float + { + return $this->percent; + } + + public function getBarOffset(): float + { + return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth); + } + + public function getEstimated(): float + { + if (!$this->step) { + return 0; + } + + return round((time() - $this->startTime) / $this->step * $this->max); + } + + public function getRemaining(): float + { + if (!$this->step) { + return 0; + } + + return round((time() - $this->startTime) / $this->step * ($this->max - $this->step)); + } + + public function setBarWidth(int $size) + { + $this->barWidth = max(1, $size); + } + + public function getBarWidth(): int + { + return $this->barWidth; + } + + public function setBarCharacter(string $char) + { + $this->barChar = $char; + } + + public function getBarCharacter(): string + { + return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar); + } + + public function setEmptyBarCharacter(string $char) + { + $this->emptyBarChar = $char; + } + + public function getEmptyBarCharacter(): string + { + return $this->emptyBarChar; + } + + public function setProgressCharacter(string $char) + { + $this->progressChar = $char; + } + + public function getProgressCharacter(): string + { + return $this->progressChar; + } + + public function setFormat(string $format) + { + $this->format = null; + $this->internalFormat = $format; + } + + /** + * Sets the redraw frequency. + * + * @param int|null $freq The frequency in steps + */ + public function setRedrawFrequency(?int $freq) + { + $this->redrawFreq = null !== $freq ? max(1, $freq) : null; + } + + public function minSecondsBetweenRedraws(float $seconds): void + { + $this->minSecondsBetweenRedraws = $seconds; + } + + public function maxSecondsBetweenRedraws(float $seconds): void + { + $this->maxSecondsBetweenRedraws = $seconds; + } + + /** + * Returns an iterator that will automatically update the progress bar when iterated. + * + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable + */ + public function iterate(iterable $iterable, int $max = null): iterable + { + $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0)); + + foreach ($iterable as $key => $value) { + yield $key => $value; + + $this->advance(); + } + + $this->finish(); + } + + /** + * Starts the progress output. + * + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged + */ + public function start(int $max = null) + { + $this->startTime = time(); + $this->step = 0; + $this->percent = 0.0; + + if (null !== $max) { + $this->setMaxSteps($max); + } + + $this->display(); + } + + /** + * Advances the progress output X steps. + * + * @param int $step Number of steps to advance + */ + public function advance(int $step = 1) + { + $this->setProgress($this->step + $step); + } + + /** + * Sets whether to overwrite the progressbar, false for new line. + */ + public function setOverwrite(bool $overwrite) + { + $this->overwrite = $overwrite; + } + + public function setProgress(int $step) + { + if ($this->max && $step > $this->max) { + $this->max = $step; + } elseif ($step < 0) { + $step = 0; + } + + $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10); + $prevPeriod = (int) ($this->step / $redrawFreq); + $currPeriod = (int) ($step / $redrawFreq); + $this->step = $step; + $this->percent = $this->max ? (float) $this->step / $this->max : 0; + $timeInterval = microtime(true) - $this->lastWriteTime; + + // Draw regardless of other limits + if ($this->max === $step) { + $this->display(); + + return; + } + + // Throttling + if ($timeInterval < $this->minSecondsBetweenRedraws) { + return; + } + + // Draw each step period, but not too late + if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) { + $this->display(); + } + } + + public function setMaxSteps(int $max) + { + $this->format = null; + $this->max = max(0, $max); + $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4; + } + + /** + * Finishes the progress output. + */ + public function finish(): void + { + if (!$this->max) { + $this->max = $this->step; + } + + if ($this->step === $this->max && !$this->overwrite) { + // prevent double 100% output + return; + } + + $this->setProgress($this->max); + } + + /** + * Outputs the current progress string. + */ + public function display(): void + { + if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + if (null === $this->format) { + $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); + } + + $this->overwrite($this->buildLine()); + } + + /** + * Removes the progress bar from the current line. + * + * This is useful if you wish to write some output + * while a progress bar is running. + * Call display() to show the progress bar again. + */ + public function clear(): void + { + if (!$this->overwrite) { + return; + } + + if (null === $this->format) { + $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); + } + + $this->overwrite(''); + } + + private function setRealFormat(string $format) + { + // try to use the _nomax variant if available + if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { + $this->format = self::getFormatDefinition($format.'_nomax'); + } elseif (null !== self::getFormatDefinition($format)) { + $this->format = self::getFormatDefinition($format); + } else { + $this->format = $format; + } + + $this->formatLineCount = substr_count($this->format, "\n"); + } + + /** + * Overwrites a previous message to the output. + */ + private function overwrite(string $message): void + { + if ($this->previousMessage === $message) { + return; + } + + $originalMessage = $message; + + if ($this->overwrite) { + if (null !== $this->previousMessage) { + if ($this->output instanceof ConsoleSectionOutput) { + $messageLines = explode("\n", $message); + $lineCount = \count($messageLines); + foreach ($messageLines as $messageLine) { + $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine)); + if ($messageLineLength > $this->terminal->getWidth()) { + $lineCount += floor($messageLineLength / $this->terminal->getWidth()); + } + } + $this->output->clear($lineCount); + } else { + for ($i = 0; $i < $this->formatLineCount; ++$i) { + $this->cursor->moveToColumn(1); + $this->cursor->clearLine(); + $this->cursor->moveUp(); + } + + $this->cursor->moveToColumn(1); + $this->cursor->clearLine(); + } + } + } elseif ($this->step > 0) { + $message = \PHP_EOL.$message; + } + + $this->previousMessage = $originalMessage; + $this->lastWriteTime = microtime(true); + + $this->output->write($message); + ++$this->writeCount; + } + + private function determineBestFormat(): string + { + switch ($this->output->getVerbosity()) { + // OutputInterface::VERBOSITY_QUIET: display is disabled anyway + case OutputInterface::VERBOSITY_VERBOSE: + return $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX; + case OutputInterface::VERBOSITY_VERY_VERBOSE: + return $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX; + case OutputInterface::VERBOSITY_DEBUG: + return $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX; + default: + return $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX; + } + } + + private static function initPlaceholderFormatters(): array + { + return [ + 'bar' => function (self $bar, OutputInterface $output) { + $completeBars = $bar->getBarOffset(); + $display = str_repeat($bar->getBarCharacter(), $completeBars); + if ($completeBars < $bar->getBarWidth()) { + $emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter())); + $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); + } + + return $display; + }, + 'elapsed' => function (self $bar) { + return Helper::formatTime(time() - $bar->getStartTime()); + }, + 'remaining' => function (self $bar) { + if (!$bar->getMaxSteps()) { + throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); + } + + return Helper::formatTime($bar->getRemaining()); + }, + 'estimated' => function (self $bar) { + if (!$bar->getMaxSteps()) { + throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); + } + + return Helper::formatTime($bar->getEstimated()); + }, + 'memory' => function (self $bar) { + return Helper::formatMemory(memory_get_usage(true)); + }, + 'current' => function (self $bar) { + return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT); + }, + 'max' => function (self $bar) { + return $bar->getMaxSteps(); + }, + 'percent' => function (self $bar) { + return floor($bar->getProgressPercent() * 100); + }, + ]; + } + + private static function initFormats(): array + { + return [ + self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%', + self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]', + + self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', + self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%', + + self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', + self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%', + + self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', + self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%', + ]; + } + + private function buildLine(): string + { + $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; + $callback = function ($matches) { + if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) { + $text = $formatter($this, $this->output); + } elseif (isset($this->messages[$matches[1]])) { + $text = $this->messages[$matches[1]]; + } else { + return $matches[0]; + } + + if (isset($matches[2])) { + $text = sprintf('%'.$matches[2], $text); + } + + return $text; + }; + $line = preg_replace_callback($regex, $callback, $this->format); + + // gets string length for each sub line with multiline format + $linesLength = array_map(function ($subLine) { + return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))); + }, explode("\n", $line)); + + $linesWidth = max($linesLength); + + $terminalWidth = $this->terminal->getWidth(); + if ($linesWidth <= $terminalWidth) { + return $line; + } + + $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth); + + return preg_replace_callback($regex, $callback, $this->format); + } +} diff --git a/vendor/symfony/console/Helper/ProgressIndicator.php b/vendor/symfony/console/Helper/ProgressIndicator.php new file mode 100644 index 0000000..3482343 --- /dev/null +++ b/vendor/symfony/console/Helper/ProgressIndicator.php @@ -0,0 +1,249 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Kevin Bond + */ +class ProgressIndicator +{ + private const FORMATS = [ + 'normal' => ' %indicator% %message%', + 'normal_no_ansi' => ' %message%', + + 'verbose' => ' %indicator% %message% (%elapsed:6s%)', + 'verbose_no_ansi' => ' %message% (%elapsed:6s%)', + + 'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)', + 'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)', + ]; + + private $output; + private $startTime; + private $format; + private $message; + private $indicatorValues; + private $indicatorCurrent; + private $indicatorChangeInterval; + private $indicatorUpdateTime; + private $started = false; + + /** + * @var array + */ + private static $formatters; + + /** + * @param int $indicatorChangeInterval Change interval in milliseconds + * @param array|null $indicatorValues Animated indicator characters + */ + public function __construct(OutputInterface $output, string $format = null, int $indicatorChangeInterval = 100, array $indicatorValues = null) + { + $this->output = $output; + + if (null === $format) { + $format = $this->determineBestFormat(); + } + + if (null === $indicatorValues) { + $indicatorValues = ['-', '\\', '|', '/']; + } + + $indicatorValues = array_values($indicatorValues); + + if (2 > \count($indicatorValues)) { + throw new InvalidArgumentException('Must have at least 2 indicator value characters.'); + } + + $this->format = self::getFormatDefinition($format); + $this->indicatorChangeInterval = $indicatorChangeInterval; + $this->indicatorValues = $indicatorValues; + $this->startTime = time(); + } + + /** + * Sets the current indicator message. + */ + public function setMessage(?string $message) + { + $this->message = $message; + + $this->display(); + } + + /** + * Starts the indicator output. + */ + public function start(string $message) + { + if ($this->started) { + throw new LogicException('Progress indicator already started.'); + } + + $this->message = $message; + $this->started = true; + $this->startTime = time(); + $this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval; + $this->indicatorCurrent = 0; + + $this->display(); + } + + /** + * Advances the indicator. + */ + public function advance() + { + if (!$this->started) { + throw new LogicException('Progress indicator has not yet been started.'); + } + + if (!$this->output->isDecorated()) { + return; + } + + $currentTime = $this->getCurrentTimeInMilliseconds(); + + if ($currentTime < $this->indicatorUpdateTime) { + return; + } + + $this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval; + ++$this->indicatorCurrent; + + $this->display(); + } + + /** + * Finish the indicator with message. + * + * @param $message + */ + public function finish(string $message) + { + if (!$this->started) { + throw new LogicException('Progress indicator has not yet been started.'); + } + + $this->message = $message; + $this->display(); + $this->output->writeln(''); + $this->started = false; + } + + /** + * Gets the format for a given name. + * + * @return string|null + */ + public static function getFormatDefinition(string $name) + { + return self::FORMATS[$name] ?? null; + } + + /** + * Sets a placeholder formatter for a given name. + * + * This method also allow you to override an existing placeholder. + */ + public static function setPlaceholderFormatterDefinition(string $name, callable $callable) + { + if (!self::$formatters) { + self::$formatters = self::initPlaceholderFormatters(); + } + + self::$formatters[$name] = $callable; + } + + /** + * Gets the placeholder formatter for a given name (including the delimiter char like %). + * + * @return callable|null + */ + public static function getPlaceholderFormatterDefinition(string $name) + { + if (!self::$formatters) { + self::$formatters = self::initPlaceholderFormatters(); + } + + return self::$formatters[$name] ?? null; + } + + private function display() + { + if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) { + if ($formatter = self::getPlaceholderFormatterDefinition($matches[1])) { + return $formatter($this); + } + + return $matches[0]; + }, $this->format ?? '')); + } + + private function determineBestFormat(): string + { + switch ($this->output->getVerbosity()) { + // OutputInterface::VERBOSITY_QUIET: display is disabled anyway + case OutputInterface::VERBOSITY_VERBOSE: + return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi'; + case OutputInterface::VERBOSITY_VERY_VERBOSE: + case OutputInterface::VERBOSITY_DEBUG: + return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi'; + default: + return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi'; + } + } + + /** + * Overwrites a previous message to the output. + */ + private function overwrite(string $message) + { + if ($this->output->isDecorated()) { + $this->output->write("\x0D\x1B[2K"); + $this->output->write($message); + } else { + $this->output->writeln($message); + } + } + + private function getCurrentTimeInMilliseconds(): float + { + return round(microtime(true) * 1000); + } + + private static function initPlaceholderFormatters(): array + { + return [ + 'indicator' => function (self $indicator) { + return $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)]; + }, + 'message' => function (self $indicator) { + return $indicator->message; + }, + 'elapsed' => function (self $indicator) { + return Helper::formatTime(time() - $indicator->startTime); + }, + 'memory' => function () { + return Helper::formatMemory(memory_get_usage(true)); + }, + ]; + } +} diff --git a/vendor/symfony/console/Helper/QuestionHelper.php b/vendor/symfony/console/Helper/QuestionHelper.php new file mode 100644 index 0000000..7abd092 --- /dev/null +++ b/vendor/symfony/console/Helper/QuestionHelper.php @@ -0,0 +1,608 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Cursor; +use Symfony\Component\Console\Exception\MissingInputException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StreamableInputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Terminal; +use function Symfony\Component\String\s; + +/** + * The QuestionHelper class provides helpers to interact with the user. + * + * @author Fabien Potencier + */ +class QuestionHelper extends Helper +{ + /** + * @var resource|null + */ + private $inputStream; + + private static $stty = true; + private static $stdinIsInteractive; + + /** + * Asks a question to the user. + * + * @return mixed The user answer + * + * @throws RuntimeException If there is no data to read in the input stream + */ + public function ask(InputInterface $input, OutputInterface $output, Question $question) + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + if (!$input->isInteractive()) { + return $this->getDefaultAnswer($question); + } + + if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { + $this->inputStream = $stream; + } + + try { + if (!$question->getValidator()) { + return $this->doAsk($output, $question); + } + + $interviewer = function () use ($output, $question) { + return $this->doAsk($output, $question); + }; + + return $this->validateAttempts($interviewer, $output, $question); + } catch (MissingInputException $exception) { + $input->setInteractive(false); + + if (null === $fallbackOutput = $this->getDefaultAnswer($question)) { + throw $exception; + } + + return $fallbackOutput; + } + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'question'; + } + + /** + * Prevents usage of stty. + */ + public static function disableStty() + { + self::$stty = false; + } + + /** + * Asks the question to the user. + * + * @return mixed + * + * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + */ + private function doAsk(OutputInterface $output, Question $question) + { + $this->writePrompt($output, $question); + + $inputStream = $this->inputStream ?: \STDIN; + $autocomplete = $question->getAutocompleterCallback(); + + if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) { + $ret = false; + if ($question->isHidden()) { + try { + $hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable()); + $ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse; + } catch (RuntimeException $e) { + if (!$question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $ret = $this->readInput($inputStream, $question); + if (false === $ret) { + throw new MissingInputException('Aborted.'); + } + if ($question->isTrimmable()) { + $ret = trim($ret); + } + } + } else { + $autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete); + $ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete; + } + + if ($output instanceof ConsoleSectionOutput) { + $output->addContent($ret); + } + + $ret = \strlen($ret) > 0 ? $ret : $question->getDefault(); + + if ($normalizer = $question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + /** + * @return mixed + */ + private function getDefaultAnswer(Question $question) + { + $default = $question->getDefault(); + + if (null === $default) { + return $default; + } + + if ($validator = $question->getValidator()) { + return \call_user_func($question->getValidator(), $default); + } elseif ($question instanceof ChoiceQuestion) { + $choices = $question->getChoices(); + + if (!$question->isMultiselect()) { + return $choices[$default] ?? $default; + } + + $default = explode(',', $default); + foreach ($default as $k => $v) { + $v = $question->isTrimmable() ? trim($v) : $v; + $default[$k] = $choices[$v] ?? $v; + } + } + + return $default; + } + + /** + * Outputs the question prompt. + */ + protected function writePrompt(OutputInterface $output, Question $question) + { + $message = $question->getQuestion(); + + if ($question instanceof ChoiceQuestion) { + $output->writeln(array_merge([ + $question->getQuestion(), + ], $this->formatChoiceQuestionChoices($question, 'info'))); + + $message = $question->getPrompt(); + } + + $output->write($message); + } + + /** + * @return string[] + */ + protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag) + { + $messages = []; + + $maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices()))); + + foreach ($choices as $key => $value) { + $padding = str_repeat(' ', $maxWidth - self::width($key)); + + $messages[] = sprintf(" [<$tag>%s$padding] %s", $key, $value); + } + + return $messages; + } + + /** + * Outputs an error message. + */ + protected function writeError(OutputInterface $output, \Exception $error) + { + if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) { + $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'); + } else { + $message = ''.$error->getMessage().''; + } + + $output->writeln($message); + } + + /** + * Autocompletes a question. + * + * @param resource $inputStream + */ + private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string + { + $cursor = new Cursor($output, $inputStream); + + $fullChoice = ''; + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete($ret); + $numMatches = \count($matches); + + $sttyMode = shell_exec('stty -g'); + + // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) + shell_exec('stty -icanon -echo'); + + // Add highlighted text style + $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); + + // Read a keypress + while (!feof($inputStream)) { + $c = fread($inputStream, 1); + + // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. + if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { + shell_exec(sprintf('stty %s', $sttyMode)); + throw new MissingInputException('Aborted.'); + } elseif ("\177" === $c) { // Backspace Character + if (0 === $numMatches && 0 !== $i) { + --$i; + $cursor->moveLeft(s($fullChoice)->slice(-1)->width(false)); + + $fullChoice = self::substr($fullChoice, 0, $i); + } + + if (0 === $i) { + $ofs = -1; + $matches = $autocomplete($ret); + $numMatches = \count($matches); + } else { + $numMatches = 0; + } + + // Pop the last character off the end of our string + $ret = self::substr($ret, 0, $i); + } elseif ("\033" === $c) { + // Did we read an escape sequence? + $c .= fread($inputStream, 2); + + // A = Up Arrow. B = Down Arrow + if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (\ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = (string) $matches[$ofs]; + // Echo out remaining chars for current match + $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)))); + $output->write($remainingCharacters); + $fullChoice .= $remainingCharacters; + $i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding); + + $matches = array_filter( + $autocomplete($ret), + function ($match) use ($ret) { + return '' === $ret || str_starts_with($match, $ret); + } + ); + $numMatches = \count($matches); + $ofs = -1; + } + + if ("\n" === $c) { + $output->write($c); + break; + } + + $numMatches = 0; + } + + continue; + } else { + if ("\x80" <= $c) { + $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]); + } + + $output->write($c); + $ret .= $c; + $fullChoice .= $c; + ++$i; + + $tempRet = $ret; + + if ($question instanceof ChoiceQuestion && $question->isMultiselect()) { + $tempRet = $this->mostRecentlyEnteredValue($fullChoice); + } + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete($ret) as $value) { + // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) + if (str_starts_with($value, $tempRet)) { + $matches[$numMatches++] = $value; + } + } + } + + $cursor->clearLineAfter(); + + if ($numMatches > 0 && -1 !== $ofs) { + $cursor->savePosition(); + // Write highlighted text, complete the partially entered response + $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))); + $output->write(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).''); + $cursor->restorePosition(); + } + } + + // Reset stty so it behaves normally again + shell_exec(sprintf('stty %s', $sttyMode)); + + return $fullChoice; + } + + private function mostRecentlyEnteredValue(string $entered): string + { + // Determine the most recent value that the user entered + if (!str_contains($entered, ',')) { + return $entered; + } + + $choices = explode(',', $entered); + if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) { + return $lastChoice; + } + + return $entered; + } + + /** + * Gets a hidden response from user. + * + * @param resource $inputStream The handler resource + * @param bool $trimmable Is the answer trimmable + * + * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + */ + private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; + + // handle code running from a phar + if ('phar:' === substr(__FILE__, 0, 5)) { + $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; + copy($exe, $tmpExe); + $exe = $tmpExe; + } + + $sExec = shell_exec('"'.$exe.'"'); + $value = $trimmable ? rtrim($sExec) : $sExec; + $output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if (self::$stty && Terminal::hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + shell_exec('stty -echo'); + } elseif ($this->isInteractiveInput($inputStream)) { + throw new RuntimeException('Unable to hide the response.'); + } + + $value = fgets($inputStream, 4096); + + if (self::$stty && Terminal::hasSttyAvailable()) { + shell_exec(sprintf('stty %s', $sttyMode)); + } + + if (false === $value) { + throw new MissingInputException('Aborted.'); + } + if ($trimmable) { + $value = trim($value); + } + $output->writeln(''); + + return $value; + } + + /** + * Validates an attempt. + * + * @param callable $interviewer A callable that will ask for a question and return the result + * + * @return mixed The validated response + * + * @throws \Exception In case the max number of attempts has been reached and no valid response has been given + */ + private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question) + { + $error = null; + $attempts = $question->getMaxAttempts(); + + while (null === $attempts || $attempts--) { + if (null !== $error) { + $this->writeError($output, $error); + } + + try { + return $question->getValidator()($interviewer()); + } catch (RuntimeException $e) { + throw $e; + } catch (\Exception $error) { + } + } + + throw $error; + } + + private function isInteractiveInput($inputStream): bool + { + if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) { + return false; + } + + if (null !== self::$stdinIsInteractive) { + return self::$stdinIsInteractive; + } + + if (\function_exists('stream_isatty')) { + return self::$stdinIsInteractive = stream_isatty(fopen('php://stdin', 'r')); + } + + if (\function_exists('posix_isatty')) { + return self::$stdinIsInteractive = posix_isatty(fopen('php://stdin', 'r')); + } + + if (!\function_exists('exec')) { + return self::$stdinIsInteractive = true; + } + + exec('stty 2> /dev/null', $output, $status); + + return self::$stdinIsInteractive = 1 !== $status; + } + + /** + * Reads one or more lines of input and returns what is read. + * + * @param resource $inputStream The handler resource + * @param Question $question The question being asked + * + * @return string|false The input received, false in case input could not be read + */ + private function readInput($inputStream, Question $question) + { + if (!$question->isMultiline()) { + $cp = $this->setIOCodepage(); + $ret = fgets($inputStream, 4096); + + return $this->resetIOCodepage($cp, $ret); + } + + $multiLineStreamReader = $this->cloneInputStream($inputStream); + if (null === $multiLineStreamReader) { + return false; + } + + $ret = ''; + $cp = $this->setIOCodepage(); + while (false !== ($char = fgetc($multiLineStreamReader))) { + if (\PHP_EOL === "{$ret}{$char}") { + break; + } + $ret .= $char; + } + + return $this->resetIOCodepage($cp, $ret); + } + + /** + * Sets console I/O to the host code page. + * + * @return int Previous code page in IBM/EBCDIC format + */ + private function setIOCodepage(): int + { + if (\function_exists('sapi_windows_cp_set')) { + $cp = sapi_windows_cp_get(); + sapi_windows_cp_set(sapi_windows_cp_get('oem')); + + return $cp; + } + + return 0; + } + + /** + * Sets console I/O to the specified code page and converts the user input. + * + * @param string|false $input + * + * @return string|false + */ + private function resetIOCodepage(int $cp, $input) + { + if (0 !== $cp) { + sapi_windows_cp_set($cp); + + if (false !== $input && '' !== $input) { + $input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input); + } + } + + return $input; + } + + /** + * Clones an input stream in order to act on one instance of the same + * stream without affecting the other instance. + * + * @param resource $inputStream The handler resource + * + * @return resource|null The cloned resource, null in case it could not be cloned + */ + private function cloneInputStream($inputStream) + { + $streamMetaData = stream_get_meta_data($inputStream); + $seekable = $streamMetaData['seekable'] ?? false; + $mode = $streamMetaData['mode'] ?? 'rb'; + $uri = $streamMetaData['uri'] ?? null; + + if (null === $uri) { + return null; + } + + $cloneStream = fopen($uri, $mode); + + // For seekable and writable streams, add all the same data to the + // cloned stream and then seek to the same offset. + if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) { + $offset = ftell($inputStream); + rewind($inputStream); + stream_copy_to_stream($inputStream, $cloneStream); + fseek($inputStream, $offset); + fseek($cloneStream, $offset); + } + + return $cloneStream; + } +} diff --git a/vendor/symfony/console/Helper/SymfonyQuestionHelper.php b/vendor/symfony/console/Helper/SymfonyQuestionHelper.php new file mode 100644 index 0000000..01f94ab --- /dev/null +++ b/vendor/symfony/console/Helper/SymfonyQuestionHelper.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Symfony Style Guide compliant question helper. + * + * @author Kevin Bond + */ +class SymfonyQuestionHelper extends QuestionHelper +{ + /** + * {@inheritdoc} + */ + protected function writePrompt(OutputInterface $output, Question $question) + { + $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); + $default = $question->getDefault(); + + if ($question->isMultiline()) { + $text .= sprintf(' (press %s to continue)', $this->getEofShortcut()); + } + + switch (true) { + case null === $default: + $text = sprintf(' %s:', $text); + + break; + + case $question instanceof ConfirmationQuestion: + $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); + + break; + + case $question instanceof ChoiceQuestion && $question->isMultiselect(): + $choices = $question->getChoices(); + $default = explode(',', $default); + + foreach ($default as $key => $value) { + $default[$key] = $choices[trim($value)]; + } + + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(implode(', ', $default))); + + break; + + case $question instanceof ChoiceQuestion: + $choices = $question->getChoices(); + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($choices[$default] ?? $default)); + + break; + + default: + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($default)); + } + + $output->writeln($text); + + $prompt = ' > '; + + if ($question instanceof ChoiceQuestion) { + $output->writeln($this->formatChoiceQuestionChoices($question, 'comment')); + + $prompt = $question->getPrompt(); + } + + $output->write($prompt); + } + + /** + * {@inheritdoc} + */ + protected function writeError(OutputInterface $output, \Exception $error) + { + if ($output instanceof SymfonyStyle) { + $output->newLine(); + $output->error($error->getMessage()); + + return; + } + + parent::writeError($output, $error); + } + + private function getEofShortcut(): string + { + if ('Windows' === \PHP_OS_FAMILY) { + return 'Ctrl+Z then Enter'; + } + + return 'Ctrl+D'; + } +} diff --git a/vendor/symfony/console/Helper/Table.php b/vendor/symfony/console/Helper/Table.php new file mode 100644 index 0000000..6ade136 --- /dev/null +++ b/vendor/symfony/console/Helper/Table.php @@ -0,0 +1,894 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Provides helpers to display a table. + * + * @author Fabien Potencier + * @author Саша Стаменковић + * @author Abdellatif Ait boudad + * @author Max Grigorian + * @author Dany Maillard + */ +class Table +{ + private const SEPARATOR_TOP = 0; + private const SEPARATOR_TOP_BOTTOM = 1; + private const SEPARATOR_MID = 2; + private const SEPARATOR_BOTTOM = 3; + private const BORDER_OUTSIDE = 0; + private const BORDER_INSIDE = 1; + + private $headerTitle; + private $footerTitle; + + /** + * Table headers. + */ + private $headers = []; + + /** + * Table rows. + */ + private $rows = []; + private $horizontal = false; + + /** + * Column widths cache. + */ + private $effectiveColumnWidths = []; + + /** + * Number of columns cache. + * + * @var int + */ + private $numberOfColumns; + + /** + * @var OutputInterface + */ + private $output; + + /** + * @var TableStyle + */ + private $style; + + /** + * @var array + */ + private $columnStyles = []; + + /** + * User set column widths. + * + * @var array + */ + private $columnWidths = []; + private $columnMaxWidths = []; + + /** + * @var array|null + */ + private static $styles; + + private $rendered = false; + + public function __construct(OutputInterface $output) + { + $this->output = $output; + + if (!self::$styles) { + self::$styles = self::initStyles(); + } + + $this->setStyle('default'); + } + + /** + * Sets a style definition. + */ + public static function setStyleDefinition(string $name, TableStyle $style) + { + if (!self::$styles) { + self::$styles = self::initStyles(); + } + + self::$styles[$name] = $style; + } + + /** + * Gets a style definition by name. + * + * @return TableStyle + */ + public static function getStyleDefinition(string $name) + { + if (!self::$styles) { + self::$styles = self::initStyles(); + } + + if (isset(self::$styles[$name])) { + return self::$styles[$name]; + } + + throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); + } + + /** + * Sets table style. + * + * @param TableStyle|string $name The style name or a TableStyle instance + * + * @return $this + */ + public function setStyle($name) + { + $this->style = $this->resolveStyle($name); + + return $this; + } + + /** + * Gets the current table style. + * + * @return TableStyle + */ + public function getStyle() + { + return $this->style; + } + + /** + * Sets table column style. + * + * @param TableStyle|string $name The style name or a TableStyle instance + * + * @return $this + */ + public function setColumnStyle(int $columnIndex, $name) + { + $this->columnStyles[$columnIndex] = $this->resolveStyle($name); + + return $this; + } + + /** + * Gets the current style for a column. + * + * If style was not set, it returns the global table style. + * + * @return TableStyle + */ + public function getColumnStyle(int $columnIndex) + { + return $this->columnStyles[$columnIndex] ?? $this->getStyle(); + } + + /** + * Sets the minimum width of a column. + * + * @return $this + */ + public function setColumnWidth(int $columnIndex, int $width) + { + $this->columnWidths[$columnIndex] = $width; + + return $this; + } + + /** + * Sets the minimum width of all columns. + * + * @return $this + */ + public function setColumnWidths(array $widths) + { + $this->columnWidths = []; + foreach ($widths as $index => $width) { + $this->setColumnWidth($index, $width); + } + + return $this; + } + + /** + * Sets the maximum width of a column. + * + * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while + * formatted strings are preserved. + * + * @return $this + */ + public function setColumnMaxWidth(int $columnIndex, int $width): self + { + if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) { + throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter()))); + } + + $this->columnMaxWidths[$columnIndex] = $width; + + return $this; + } + + /** + * @return $this + */ + public function setHeaders(array $headers) + { + $headers = array_values($headers); + if (!empty($headers) && !\is_array($headers[0])) { + $headers = [$headers]; + } + + $this->headers = $headers; + + return $this; + } + + public function setRows(array $rows) + { + $this->rows = []; + + return $this->addRows($rows); + } + + /** + * @return $this + */ + public function addRows(array $rows) + { + foreach ($rows as $row) { + $this->addRow($row); + } + + return $this; + } + + /** + * @return $this + */ + public function addRow($row) + { + if ($row instanceof TableSeparator) { + $this->rows[] = $row; + + return $this; + } + + if (!\is_array($row)) { + throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.'); + } + + $this->rows[] = array_values($row); + + return $this; + } + + /** + * Adds a row to the table, and re-renders the table. + * + * @return $this + */ + public function appendRow($row): self + { + if (!$this->output instanceof ConsoleSectionOutput) { + throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__)); + } + + if ($this->rendered) { + $this->output->clear($this->calculateRowCount()); + } + + $this->addRow($row); + $this->render(); + + return $this; + } + + /** + * @return $this + */ + public function setRow($column, array $row) + { + $this->rows[$column] = $row; + + return $this; + } + + /** + * @return $this + */ + public function setHeaderTitle(?string $title): self + { + $this->headerTitle = $title; + + return $this; + } + + /** + * @return $this + */ + public function setFooterTitle(?string $title): self + { + $this->footerTitle = $title; + + return $this; + } + + /** + * @return $this + */ + public function setHorizontal(bool $horizontal = true): self + { + $this->horizontal = $horizontal; + + return $this; + } + + /** + * Renders table to output. + * + * Example: + * + * +---------------+-----------------------+------------------+ + * | ISBN | Title | Author | + * +---------------+-----------------------+------------------+ + * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + * +---------------+-----------------------+------------------+ + */ + public function render() + { + $divider = new TableSeparator(); + if ($this->horizontal) { + $rows = []; + foreach ($this->headers[0] ?? [] as $i => $header) { + $rows[$i] = [$header]; + foreach ($this->rows as $row) { + if ($row instanceof TableSeparator) { + continue; + } + if (isset($row[$i])) { + $rows[$i][] = $row[$i]; + } elseif ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) { + // Noop, there is a "title" + } else { + $rows[$i][] = null; + } + } + } + } else { + $rows = array_merge($this->headers, [$divider], $this->rows); + } + + $this->calculateNumberOfColumns($rows); + + $rows = $this->buildTableRows($rows); + $this->calculateColumnsWidth($rows); + + $isHeader = !$this->horizontal; + $isFirstRow = $this->horizontal; + $hasTitle = (bool) $this->headerTitle; + foreach ($rows as $row) { + if ($divider === $row) { + $isHeader = false; + $isFirstRow = true; + + continue; + } + if ($row instanceof TableSeparator) { + $this->renderRowSeparator(); + + continue; + } + if (!$row) { + continue; + } + + if ($isHeader || $isFirstRow) { + $this->renderRowSeparator( + $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, + $hasTitle ? $this->headerTitle : null, + $hasTitle ? $this->style->getHeaderTitleFormat() : null + ); + $isFirstRow = false; + $hasTitle = false; + } + if ($this->horizontal) { + $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat()); + } else { + $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat()); + } + } + $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat()); + + $this->cleanup(); + $this->rendered = true; + } + + /** + * Renders horizontal header separator. + * + * Example: + * + * +-----+-----------+-------+ + */ + private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $title = null, string $titleFormat = null) + { + if (0 === $count = $this->numberOfColumns) { + return; + } + + $borders = $this->style->getBorderChars(); + if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) { + return; + } + + $crossings = $this->style->getCrossingChars(); + if (self::SEPARATOR_MID === $type) { + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]]; + } elseif (self::SEPARATOR_TOP === $type) { + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]]; + } elseif (self::SEPARATOR_TOP_BOTTOM === $type) { + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]]; + } else { + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]]; + } + + $markup = $leftChar; + for ($column = 0; $column < $count; ++$column) { + $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]); + $markup .= $column === $count - 1 ? $rightChar : $midChar; + } + + if (null !== $title) { + $titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title))); + $markupLength = Helper::width($markup); + if ($titleLength > $limit = $markupLength - 4) { + $titleLength = $limit; + $formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, ''))); + $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...'); + } + + $titleStart = intdiv($markupLength - $titleLength, 2); + if (false === mb_detect_encoding($markup, null, true)) { + $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength); + } else { + $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength); + } + } + + $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); + } + + /** + * Renders vertical column separator. + */ + private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string + { + $borders = $this->style->getBorderChars(); + + return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]); + } + + /** + * Renders table row. + * + * Example: + * + * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + */ + private function renderRow(array $row, string $cellFormat, string $firstCellFormat = null) + { + $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE); + $columns = $this->getRowColumns($row); + $last = \count($columns) - 1; + foreach ($columns as $i => $column) { + if ($firstCellFormat && 0 === $i) { + $rowContent .= $this->renderCell($row, $column, $firstCellFormat); + } else { + $rowContent .= $this->renderCell($row, $column, $cellFormat); + } + $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE); + } + $this->output->writeln($rowContent); + } + + /** + * Renders table cell with padding. + */ + private function renderCell(array $row, int $column, string $cellFormat): string + { + $cell = $row[$column] ?? ''; + $width = $this->effectiveColumnWidths[$column]; + if ($cell instanceof TableCell && $cell->getColspan() > 1) { + // add the width of the following columns(numbers of colspan). + foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { + $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn]; + } + } + + // str_pad won't work properly with multi-byte strings, we need to fix the padding + if (false !== $encoding = mb_detect_encoding($cell, null, true)) { + $width += \strlen($cell) - mb_strwidth($cell, $encoding); + } + + $style = $this->getColumnStyle($column); + + if ($cell instanceof TableSeparator) { + return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width)); + } + + $width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell)); + $content = sprintf($style->getCellRowContentFormat(), $cell); + + $padType = $style->getPadType(); + if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) { + $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell); + if ($isNotStyledByTag) { + $cellFormat = $cell->getStyle()->getCellFormat(); + if (!\is_string($cellFormat)) { + $tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';'); + $cellFormat = '<'.$tag.'>%s'; + } + + if (strstr($content, '')) { + $content = str_replace('', '', $content); + $width -= 3; + } + if (strstr($content, '')) { + $content = str_replace('', '', $content); + $width -= \strlen(''); + } + } + + $padType = $cell->getStyle()->getPadByAlign(); + } + + return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType)); + } + + /** + * Calculate number of columns for this table. + */ + private function calculateNumberOfColumns(array $rows) + { + $columns = [0]; + foreach ($rows as $row) { + if ($row instanceof TableSeparator) { + continue; + } + + $columns[] = $this->getNumberOfColumns($row); + } + + $this->numberOfColumns = max($columns); + } + + private function buildTableRows(array $rows): TableRows + { + /** @var WrappableOutputFormatterInterface $formatter */ + $formatter = $this->output->getFormatter(); + $unmergedRows = []; + for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) { + $rows = $this->fillNextRows($rows, $rowKey); + + // Remove any new line breaks and replace it with a new line + foreach ($rows[$rowKey] as $column => $cell) { + $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1; + + if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) { + $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan); + } + if (!strstr($cell ?? '', "\n")) { + continue; + } + $escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell))); + $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; + $lines = explode("\n", str_replace("\n", "\n", $cell)); + foreach ($lines as $lineKey => $line) { + if ($colspan > 1) { + $line = new TableCell($line, ['colspan' => $colspan]); + } + if (0 === $lineKey) { + $rows[$rowKey][$column] = $line; + } else { + if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) { + $unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey); + } + $unmergedRows[$rowKey][$lineKey][$column] = $line; + } + } + } + } + + return new TableRows(function () use ($rows, $unmergedRows): \Traversable { + foreach ($rows as $rowKey => $row) { + yield $row instanceof TableSeparator ? $row : $this->fillCells($row); + + if (isset($unmergedRows[$rowKey])) { + foreach ($unmergedRows[$rowKey] as $row) { + yield $row instanceof TableSeparator ? $row : $this->fillCells($row); + } + } + } + }); + } + + private function calculateRowCount(): int + { + $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows)))); + + if ($this->headers) { + ++$numberOfRows; // Add row for header separator + } + + if (\count($this->rows) > 0) { + ++$numberOfRows; // Add row for footer separator + } + + return $numberOfRows; + } + + /** + * fill rows that contains rowspan > 1. + * + * @throws InvalidArgumentException + */ + private function fillNextRows(array $rows, int $line): array + { + $unmergedRows = []; + foreach ($rows[$line] as $column => $cell) { + if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) { + throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell))); + } + if ($cell instanceof TableCell && $cell->getRowspan() > 1) { + $nbLines = $cell->getRowspan() - 1; + $lines = [$cell]; + if (strstr($cell, "\n")) { + $lines = explode("\n", str_replace("\n", "\n", $cell)); + $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; + + $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); + unset($lines[0]); + } + + // create a two dimensional array (rowspan x colspan) + $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows); + foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { + $value = $lines[$unmergedRowKey - $line] ?? ''; + $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); + if ($nbLines === $unmergedRowKey - $line) { + break; + } + } + } + } + + foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { + // we need to know if $unmergedRow will be merged or inserted into $rows + if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) { + foreach ($unmergedRow as $cellKey => $cell) { + // insert cell into row at cellKey position + array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]); + } + } else { + $row = $this->copyRow($rows, $unmergedRowKey - 1); + foreach ($unmergedRow as $column => $cell) { + if (!empty($cell)) { + $row[$column] = $unmergedRow[$column]; + } + } + array_splice($rows, $unmergedRowKey, 0, [$row]); + } + } + + return $rows; + } + + /** + * fill cells for a row that contains colspan > 1. + */ + private function fillCells(iterable $row) + { + $newRow = []; + + foreach ($row as $column => $cell) { + $newRow[] = $cell; + if ($cell instanceof TableCell && $cell->getColspan() > 1) { + foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { + // insert empty value at column position + $newRow[] = ''; + } + } + } + + return $newRow ?: $row; + } + + private function copyRow(array $rows, int $line): array + { + $row = $rows[$line]; + foreach ($row as $cellKey => $cellValue) { + $row[$cellKey] = ''; + if ($cellValue instanceof TableCell) { + $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]); + } + } + + return $row; + } + + /** + * Gets number of columns by row. + */ + private function getNumberOfColumns(array $row): int + { + $columns = \count($row); + foreach ($row as $column) { + $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0; + } + + return $columns; + } + + /** + * Gets list of columns for the given row. + */ + private function getRowColumns(array $row): array + { + $columns = range(0, $this->numberOfColumns - 1); + foreach ($row as $cellKey => $cell) { + if ($cell instanceof TableCell && $cell->getColspan() > 1) { + // exclude grouped columns. + $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1)); + } + } + + return $columns; + } + + /** + * Calculates columns widths. + */ + private function calculateColumnsWidth(iterable $rows) + { + for ($column = 0; $column < $this->numberOfColumns; ++$column) { + $lengths = []; + foreach ($rows as $row) { + if ($row instanceof TableSeparator) { + continue; + } + + foreach ($row as $i => $cell) { + if ($cell instanceof TableCell) { + $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); + $textLength = Helper::width($textContent); + if ($textLength > 0) { + $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); + foreach ($contentColumns as $position => $content) { + $row[$i + $position] = $content; + } + } + } + } + + $lengths[] = $this->getCellWidth($row, $column); + } + + $this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2; + } + } + + private function getColumnSeparatorWidth(): int + { + return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3])); + } + + private function getCellWidth(array $row, int $column): int + { + $cellWidth = 0; + + if (isset($row[$column])) { + $cell = $row[$column]; + $cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell)); + } + + $columnWidth = $this->columnWidths[$column] ?? 0; + $cellWidth = max($cellWidth, $columnWidth); + + return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth; + } + + /** + * Called after rendering to cleanup cache data. + */ + private function cleanup() + { + $this->effectiveColumnWidths = []; + $this->numberOfColumns = null; + } + + /** + * @return array + */ + private static function initStyles(): array + { + $borderless = new TableStyle(); + $borderless + ->setHorizontalBorderChars('=') + ->setVerticalBorderChars(' ') + ->setDefaultCrossingChar(' ') + ; + + $compact = new TableStyle(); + $compact + ->setHorizontalBorderChars('') + ->setVerticalBorderChars(' ') + ->setDefaultCrossingChar('') + ->setCellRowContentFormat('%s') + ; + + $styleGuide = new TableStyle(); + $styleGuide + ->setHorizontalBorderChars('-') + ->setVerticalBorderChars(' ') + ->setDefaultCrossingChar(' ') + ->setCellHeaderFormat('%s') + ; + + $box = (new TableStyle()) + ->setHorizontalBorderChars('─') + ->setVerticalBorderChars('│') + ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├') + ; + + $boxDouble = (new TableStyle()) + ->setHorizontalBorderChars('═', '─') + ->setVerticalBorderChars('║', '│') + ->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣') + ; + + return [ + 'default' => new TableStyle(), + 'borderless' => $borderless, + 'compact' => $compact, + 'symfony-style-guide' => $styleGuide, + 'box' => $box, + 'box-double' => $boxDouble, + ]; + } + + private function resolveStyle($name): TableStyle + { + if ($name instanceof TableStyle) { + return $name; + } + + if (isset(self::$styles[$name])) { + return self::$styles[$name]; + } + + throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); + } +} diff --git a/vendor/symfony/console/Helper/TableCell.php b/vendor/symfony/console/Helper/TableCell.php new file mode 100644 index 0000000..1a7bc6e --- /dev/null +++ b/vendor/symfony/console/Helper/TableCell.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Abdellatif Ait boudad + */ +class TableCell +{ + private $value; + private $options = [ + 'rowspan' => 1, + 'colspan' => 1, + 'style' => null, + ]; + + public function __construct(string $value = '', array $options = []) + { + $this->value = $value; + + // check option names + if ($diff = array_diff(array_keys($options), array_keys($this->options))) { + throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff))); + } + + if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) { + throw new InvalidArgumentException('The style option must be an instance of "TableCellStyle".'); + } + + $this->options = array_merge($this->options, $options); + } + + /** + * Returns the cell value. + * + * @return string + */ + public function __toString() + { + return $this->value; + } + + /** + * Gets number of colspan. + * + * @return int + */ + public function getColspan() + { + return (int) $this->options['colspan']; + } + + /** + * Gets number of rowspan. + * + * @return int + */ + public function getRowspan() + { + return (int) $this->options['rowspan']; + } + + public function getStyle(): ?TableCellStyle + { + return $this->options['style']; + } +} diff --git a/vendor/symfony/console/Helper/TableCellStyle.php b/vendor/symfony/console/Helper/TableCellStyle.php new file mode 100644 index 0000000..19cd0ff --- /dev/null +++ b/vendor/symfony/console/Helper/TableCellStyle.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Yewhen Khoptynskyi + */ +class TableCellStyle +{ + public const DEFAULT_ALIGN = 'left'; + + private const TAG_OPTIONS = [ + 'fg', + 'bg', + 'options', + ]; + + private const ALIGN_MAP = [ + 'left' => \STR_PAD_RIGHT, + 'center' => \STR_PAD_BOTH, + 'right' => \STR_PAD_LEFT, + ]; + + private $options = [ + 'fg' => 'default', + 'bg' => 'default', + 'options' => null, + 'align' => self::DEFAULT_ALIGN, + 'cellFormat' => null, + ]; + + public function __construct(array $options = []) + { + if ($diff = array_diff(array_keys($options), array_keys($this->options))) { + throw new InvalidArgumentException(sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff))); + } + + if (isset($options['align']) && !\array_key_exists($options['align'], self::ALIGN_MAP)) { + throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP)))); + } + + $this->options = array_merge($this->options, $options); + } + + public function getOptions(): array + { + return $this->options; + } + + /** + * Gets options we need for tag for example fg, bg. + * + * @return string[] + */ + public function getTagOptions() + { + return array_filter( + $this->getOptions(), + function ($key) { + return \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]); + }, + \ARRAY_FILTER_USE_KEY + ); + } + + /** + * @return int + */ + public function getPadByAlign() + { + return self::ALIGN_MAP[$this->getOptions()['align']]; + } + + public function getCellFormat(): ?string + { + return $this->getOptions()['cellFormat']; + } +} diff --git a/vendor/symfony/console/Helper/TableRows.php b/vendor/symfony/console/Helper/TableRows.php new file mode 100644 index 0000000..cbc07d2 --- /dev/null +++ b/vendor/symfony/console/Helper/TableRows.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * @internal + */ +class TableRows implements \IteratorAggregate +{ + private $generator; + + public function __construct(\Closure $generator) + { + $this->generator = $generator; + } + + public function getIterator(): \Traversable + { + return ($this->generator)(); + } +} diff --git a/vendor/symfony/console/Helper/TableSeparator.php b/vendor/symfony/console/Helper/TableSeparator.php new file mode 100644 index 0000000..e541c53 --- /dev/null +++ b/vendor/symfony/console/Helper/TableSeparator.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * Marks a row as being a separator. + * + * @author Fabien Potencier + */ +class TableSeparator extends TableCell +{ + public function __construct(array $options = []) + { + parent::__construct('', $options); + } +} diff --git a/vendor/symfony/console/Helper/TableStyle.php b/vendor/symfony/console/Helper/TableStyle.php new file mode 100644 index 0000000..dfc41e6 --- /dev/null +++ b/vendor/symfony/console/Helper/TableStyle.php @@ -0,0 +1,376 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Defines the styles for a Table. + * + * @author Fabien Potencier + * @author Саша Стаменковић + * @author Dany Maillard + */ +class TableStyle +{ + private $paddingChar = ' '; + private $horizontalOutsideBorderChar = '-'; + private $horizontalInsideBorderChar = '-'; + private $verticalOutsideBorderChar = '|'; + private $verticalInsideBorderChar = '|'; + private $crossingChar = '+'; + private $crossingTopRightChar = '+'; + private $crossingTopMidChar = '+'; + private $crossingTopLeftChar = '+'; + private $crossingMidRightChar = '+'; + private $crossingBottomRightChar = '+'; + private $crossingBottomMidChar = '+'; + private $crossingBottomLeftChar = '+'; + private $crossingMidLeftChar = '+'; + private $crossingTopLeftBottomChar = '+'; + private $crossingTopMidBottomChar = '+'; + private $crossingTopRightBottomChar = '+'; + private $headerTitleFormat = ' %s '; + private $footerTitleFormat = ' %s '; + private $cellHeaderFormat = '%s'; + private $cellRowFormat = '%s'; + private $cellRowContentFormat = ' %s '; + private $borderFormat = '%s'; + private $padType = \STR_PAD_RIGHT; + + /** + * Sets padding character, used for cell padding. + * + * @return $this + */ + public function setPaddingChar(string $paddingChar) + { + if (!$paddingChar) { + throw new LogicException('The padding char must not be empty.'); + } + + $this->paddingChar = $paddingChar; + + return $this; + } + + /** + * Gets padding character, used for cell padding. + * + * @return string + */ + public function getPaddingChar() + { + return $this->paddingChar; + } + + /** + * Sets horizontal border characters. + * + * + * ╔═══════════════╤══════════════════════════╤══════════════════╗ + * 1 ISBN 2 Title │ Author ║ + * ╠═══════════════╪══════════════════════════╪══════════════════╣ + * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ + * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ + * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ + * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ + * ╚═══════════════╧══════════════════════════╧══════════════════╝ + * + * + * @return $this + */ + public function setHorizontalBorderChars(string $outside, string $inside = null): self + { + $this->horizontalOutsideBorderChar = $outside; + $this->horizontalInsideBorderChar = $inside ?? $outside; + + return $this; + } + + /** + * Sets vertical border characters. + * + * + * ╔═══════════════╤══════════════════════════╤══════════════════╗ + * ║ ISBN │ Title │ Author ║ + * ╠═══════1═══════╪══════════════════════════╪══════════════════╣ + * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ + * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ + * ╟───────2───────┼──────────────────────────┼──────────────────╢ + * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ + * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ + * ╚═══════════════╧══════════════════════════╧══════════════════╝ + * + * + * @return $this + */ + public function setVerticalBorderChars(string $outside, string $inside = null): self + { + $this->verticalOutsideBorderChar = $outside; + $this->verticalInsideBorderChar = $inside ?? $outside; + + return $this; + } + + /** + * Gets border characters. + * + * @internal + */ + public function getBorderChars(): array + { + return [ + $this->horizontalOutsideBorderChar, + $this->verticalOutsideBorderChar, + $this->horizontalInsideBorderChar, + $this->verticalInsideBorderChar, + ]; + } + + /** + * Sets crossing characters. + * + * Example: + * + * 1═══════════════2══════════════════════════2══════════════════3 + * ║ ISBN │ Title │ Author ║ + * 8'══════════════0'═════════════════════════0'═════════════════4' + * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ + * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ + * 8───────────────0──────────────────────────0──────────────────4 + * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ + * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ + * 7═══════════════6══════════════════════════6══════════════════5 + * + * + * @param string $cross Crossing char (see #0 of example) + * @param string $topLeft Top left char (see #1 of example) + * @param string $topMid Top mid char (see #2 of example) + * @param string $topRight Top right char (see #3 of example) + * @param string $midRight Mid right char (see #4 of example) + * @param string $bottomRight Bottom right char (see #5 of example) + * @param string $bottomMid Bottom mid char (see #6 of example) + * @param string $bottomLeft Bottom left char (see #7 of example) + * @param string $midLeft Mid left char (see #8 of example) + * @param string|null $topLeftBottom Top left bottom char (see #8' of example), equals to $midLeft if null + * @param string|null $topMidBottom Top mid bottom char (see #0' of example), equals to $cross if null + * @param string|null $topRightBottom Top right bottom char (see #4' of example), equals to $midRight if null + * + * @return $this + */ + public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, string $topLeftBottom = null, string $topMidBottom = null, string $topRightBottom = null): self + { + $this->crossingChar = $cross; + $this->crossingTopLeftChar = $topLeft; + $this->crossingTopMidChar = $topMid; + $this->crossingTopRightChar = $topRight; + $this->crossingMidRightChar = $midRight; + $this->crossingBottomRightChar = $bottomRight; + $this->crossingBottomMidChar = $bottomMid; + $this->crossingBottomLeftChar = $bottomLeft; + $this->crossingMidLeftChar = $midLeft; + $this->crossingTopLeftBottomChar = $topLeftBottom ?? $midLeft; + $this->crossingTopMidBottomChar = $topMidBottom ?? $cross; + $this->crossingTopRightBottomChar = $topRightBottom ?? $midRight; + + return $this; + } + + /** + * Sets default crossing character used for each cross. + * + * @see {@link setCrossingChars()} for setting each crossing individually. + */ + public function setDefaultCrossingChar(string $char): self + { + return $this->setCrossingChars($char, $char, $char, $char, $char, $char, $char, $char, $char); + } + + /** + * Gets crossing character. + * + * @return string + */ + public function getCrossingChar() + { + return $this->crossingChar; + } + + /** + * Gets crossing characters. + * + * @internal + */ + public function getCrossingChars(): array + { + return [ + $this->crossingChar, + $this->crossingTopLeftChar, + $this->crossingTopMidChar, + $this->crossingTopRightChar, + $this->crossingMidRightChar, + $this->crossingBottomRightChar, + $this->crossingBottomMidChar, + $this->crossingBottomLeftChar, + $this->crossingMidLeftChar, + $this->crossingTopLeftBottomChar, + $this->crossingTopMidBottomChar, + $this->crossingTopRightBottomChar, + ]; + } + + /** + * Sets header cell format. + * + * @return $this + */ + public function setCellHeaderFormat(string $cellHeaderFormat) + { + $this->cellHeaderFormat = $cellHeaderFormat; + + return $this; + } + + /** + * Gets header cell format. + * + * @return string + */ + public function getCellHeaderFormat() + { + return $this->cellHeaderFormat; + } + + /** + * Sets row cell format. + * + * @return $this + */ + public function setCellRowFormat(string $cellRowFormat) + { + $this->cellRowFormat = $cellRowFormat; + + return $this; + } + + /** + * Gets row cell format. + * + * @return string + */ + public function getCellRowFormat() + { + return $this->cellRowFormat; + } + + /** + * Sets row cell content format. + * + * @return $this + */ + public function setCellRowContentFormat(string $cellRowContentFormat) + { + $this->cellRowContentFormat = $cellRowContentFormat; + + return $this; + } + + /** + * Gets row cell content format. + * + * @return string + */ + public function getCellRowContentFormat() + { + return $this->cellRowContentFormat; + } + + /** + * Sets table border format. + * + * @return $this + */ + public function setBorderFormat(string $borderFormat) + { + $this->borderFormat = $borderFormat; + + return $this; + } + + /** + * Gets table border format. + * + * @return string + */ + public function getBorderFormat() + { + return $this->borderFormat; + } + + /** + * Sets cell padding type. + * + * @return $this + */ + public function setPadType(int $padType) + { + if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) { + throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).'); + } + + $this->padType = $padType; + + return $this; + } + + /** + * Gets cell padding type. + * + * @return int + */ + public function getPadType() + { + return $this->padType; + } + + public function getHeaderTitleFormat(): string + { + return $this->headerTitleFormat; + } + + /** + * @return $this + */ + public function setHeaderTitleFormat(string $format): self + { + $this->headerTitleFormat = $format; + + return $this; + } + + public function getFooterTitleFormat(): string + { + return $this->footerTitleFormat; + } + + /** + * @return $this + */ + public function setFooterTitleFormat(string $format): self + { + $this->footerTitleFormat = $format; + + return $this; + } +} diff --git a/vendor/symfony/console/Input/ArgvInput.php b/vendor/symfony/console/Input/ArgvInput.php new file mode 100644 index 0000000..675b9ef --- /dev/null +++ b/vendor/symfony/console/Input/ArgvInput.php @@ -0,0 +1,378 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\RuntimeException; + +/** + * ArgvInput represents an input coming from the CLI arguments. + * + * Usage: + * + * $input = new ArgvInput(); + * + * By default, the `$_SERVER['argv']` array is used for the input values. + * + * This can be overridden by explicitly passing the input values in the constructor: + * + * $input = new ArgvInput($_SERVER['argv']); + * + * If you pass it yourself, don't forget that the first element of the array + * is the name of the running application. + * + * When passing an argument to the constructor, be sure that it respects + * the same rules as the argv one. It's almost always better to use the + * `StringInput` when you want to provide your own input. + * + * @author Fabien Potencier + * + * @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html + * @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02 + */ +class ArgvInput extends Input +{ + private $tokens; + private $parsed; + + public function __construct(array $argv = null, InputDefinition $definition = null) + { + $argv = $argv ?? $_SERVER['argv'] ?? []; + + // strip the application name + array_shift($argv); + + $this->tokens = $argv; + + parent::__construct($definition); + } + + protected function setTokens(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * {@inheritdoc} + */ + protected function parse() + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + $parseOptions = $this->parseToken($token, $parseOptions); + } + } + + protected function parseToken(string $token, bool $parseOptions): bool + { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + return false; + } elseif ($parseOptions && str_starts_with($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + + return $parseOptions; + } + + /** + * Parses a short option. + */ + private function parseShortOption(string $token) + { + $name = substr($token, 1); + + if (\strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { + // an option with a value (with no space) + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * Parses a short option set. + * + * @throws RuntimeException When option given doesn't exist + */ + private function parseShortOptionSet(string $name) + { + $len = \strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + $encoding = mb_detect_encoding($name, null, true); + throw new RuntimeException(sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding))); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * Parses a long option. + */ + private function parseLongOption(string $token) + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + if ('' === $value = substr($name, $pos + 1)) { + array_unshift($this->parsed, $value); + } + $this->addLongOption(substr($name, 0, $pos), $value); + } else { + $this->addLongOption($name, null); + } + } + + /** + * Parses an argument. + * + * @throws RuntimeException When too many arguments are given + */ + private function parseArgument(string $token) + { + $c = \count($this->arguments); + + // if input is expecting another argument, add it + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + // if last argument isArray(), append token to last argument + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + $this->arguments[$arg->getName()][] = $token; + + // unexpected argument + } else { + $all = $this->definition->getArguments(); + $symfonyCommandName = null; + if (($inputArgument = $all[$key = array_key_first($all)] ?? null) && 'command' === $inputArgument->getName()) { + $symfonyCommandName = $this->arguments['command'] ?? null; + unset($all[$key]); + } + + if (\count($all)) { + if ($symfonyCommandName) { + $message = sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all))); + } else { + $message = sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all))); + } + } elseif ($symfonyCommandName) { + $message = sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token); + } else { + $message = sprintf('No arguments expected, got "%s".', $token); + } + + throw new RuntimeException($message); + } + } + + /** + * Adds a short option value. + * + * @throws RuntimeException When option given doesn't exist + */ + private function addShortOption(string $shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * Adds a long option value. + * + * @throws RuntimeException When option given doesn't exist + */ + private function addLongOption(string $name, $value) + { + if (!$this->definition->hasOption($name)) { + if (!$this->definition->hasNegation($name)) { + throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $optionName = $this->definition->negationToName($name); + if (null !== $value) { + throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); + } + $this->options[$optionName] = false; + + return; + } + + $option = $this->definition->getOption($name); + + if (null !== $value && !$option->acceptValue()) { + throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); + } + + if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) { + // if option accepts an optional or mandatory argument + // let's see if there is one provided + $next = array_shift($this->parsed); + if ((isset($next[0]) && '-' !== $next[0]) || \in_array($next, ['', null], true)) { + $value = $next; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray() && !$option->isValueOptional()) { + $value = true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function getFirstArgument() + { + $isOption = false; + foreach ($this->tokens as $i => $token) { + if ($token && '-' === $token[0]) { + if (str_contains($token, '=') || !isset($this->tokens[$i + 1])) { + continue; + } + + // If it's a long option, consider that everything after "--" is the option name. + // Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator) + $name = '-' === $token[1] ? substr($token, 2) : substr($token, -1); + if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) { + // noop + } elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) { + $isOption = true; + } + + continue; + } + + if ($isOption) { + $isOption = false; + continue; + } + + return $token; + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function hasParameterOption($values, bool $onlyParams = false) + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + if ($onlyParams && '--' === $token) { + return false; + } + foreach ($values as $value) { + // Options with values: + // For long options, test for '--option=' at beginning + // For short options, test for '-o' at beginning + $leading = str_starts_with($value, '--') ? $value.'=' : $value; + if ($token === $value || '' !== $leading && str_starts_with($token, $leading)) { + return true; + } + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getParameterOption($values, $default = false, bool $onlyParams = false) + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < \count($tokens)) { + $token = array_shift($tokens); + if ($onlyParams && '--' === $token) { + return $default; + } + + foreach ($values as $value) { + if ($token === $value) { + return array_shift($tokens); + } + // Options with values: + // For long options, test for '--option=' at beginning + // For short options, test for '-o' at beginning + $leading = str_starts_with($value, '--') ? $value.'=' : $value; + if ('' !== $leading && str_starts_with($token, $leading)) { + return substr($token, \strlen($leading)); + } + } + } + + return $default; + } + + /** + * Returns a stringified representation of the args passed to the command. + * + * @return string + */ + public function __toString() + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1].$this->escapeToken($match[2]); + } + + if ($token && '-' !== $token[0]) { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/vendor/symfony/console/Input/ArrayInput.php b/vendor/symfony/console/Input/ArrayInput.php new file mode 100644 index 0000000..c651614 --- /dev/null +++ b/vendor/symfony/console/Input/ArrayInput.php @@ -0,0 +1,210 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\InvalidOptionException; + +/** + * ArrayInput represents an input provided as an array. + * + * Usage: + * + * $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']); + * + * @author Fabien Potencier + */ +class ArrayInput extends Input +{ + private $parameters; + + public function __construct(array $parameters, InputDefinition $definition = null) + { + $this->parameters = $parameters; + + parent::__construct($definition); + } + + /** + * {@inheritdoc} + */ + public function getFirstArgument() + { + foreach ($this->parameters as $param => $value) { + if ($param && \is_string($param) && '-' === $param[0]) { + continue; + } + + return $value; + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function hasParameterOption($values, bool $onlyParams = false) + { + $values = (array) $values; + + foreach ($this->parameters as $k => $v) { + if (!\is_int($k)) { + $v = $k; + } + + if ($onlyParams && '--' === $v) { + return false; + } + + if (\in_array($v, $values)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getParameterOption($values, $default = false, bool $onlyParams = false) + { + $values = (array) $values; + + foreach ($this->parameters as $k => $v) { + if ($onlyParams && ('--' === $k || (\is_int($k) && '--' === $v))) { + return $default; + } + + if (\is_int($k)) { + if (\in_array($v, $values)) { + return true; + } + } elseif (\in_array($k, $values)) { + return $v; + } + } + + return $default; + } + + /** + * Returns a stringified representation of the args passed to the command. + * + * @return string + */ + public function __toString() + { + $params = []; + foreach ($this->parameters as $param => $val) { + if ($param && \is_string($param) && '-' === $param[0]) { + $glue = ('-' === $param[1]) ? '=' : ' '; + if (\is_array($val)) { + foreach ($val as $v) { + $params[] = $param.('' != $v ? $glue.$this->escapeToken($v) : ''); + } + } else { + $params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : ''); + } + } else { + $params[] = \is_array($val) ? implode(' ', array_map([$this, 'escapeToken'], $val)) : $this->escapeToken($val); + } + } + + return implode(' ', $params); + } + + /** + * {@inheritdoc} + */ + protected function parse() + { + foreach ($this->parameters as $key => $value) { + if ('--' === $key) { + return; + } + if (str_starts_with($key, '--')) { + $this->addLongOption(substr($key, 2), $value); + } elseif (str_starts_with($key, '-')) { + $this->addShortOption(substr($key, 1), $value); + } else { + $this->addArgument($key, $value); + } + } + } + + /** + * Adds a short option value. + * + * @throws InvalidOptionException When option given doesn't exist + */ + private function addShortOption(string $shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * Adds a long option value. + * + * @throws InvalidOptionException When option given doesn't exist + * @throws InvalidOptionException When a required value is missing + */ + private function addLongOption(string $name, $value) + { + if (!$this->definition->hasOption($name)) { + if (!$this->definition->hasNegation($name)) { + throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name)); + } + + $optionName = $this->definition->negationToName($name); + $this->options[$optionName] = false; + + return; + } + + $option = $this->definition->getOption($name); + + if (null === $value) { + if ($option->isValueRequired()) { + throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isValueOptional()) { + $value = true; + } + } + + $this->options[$name] = $value; + } + + /** + * Adds an argument value. + * + * @param string|int $name The argument name + * @param mixed $value The value for the argument + * + * @throws InvalidArgumentException When argument given doesn't exist + */ + private function addArgument($name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } +} diff --git a/vendor/symfony/console/Input/Input.php b/vendor/symfony/console/Input/Input.php new file mode 100644 index 0000000..d37460e --- /dev/null +++ b/vendor/symfony/console/Input/Input.php @@ -0,0 +1,213 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; + +/** + * Input is the base class for all concrete Input classes. + * + * Three concrete classes are provided by default: + * + * * `ArgvInput`: The input comes from the CLI arguments (argv) + * * `StringInput`: The input is provided as a string + * * `ArrayInput`: The input is provided as an array + * + * @author Fabien Potencier + */ +abstract class Input implements InputInterface, StreamableInputInterface +{ + protected $definition; + protected $stream; + protected $options = []; + protected $arguments = []; + protected $interactive = true; + + public function __construct(InputDefinition $definition = null) + { + if (null === $definition) { + $this->definition = new InputDefinition(); + } else { + $this->bind($definition); + $this->validate(); + } + } + + /** + * {@inheritdoc} + */ + public function bind(InputDefinition $definition) + { + $this->arguments = []; + $this->options = []; + $this->definition = $definition; + + $this->parse(); + } + + /** + * Processes command line arguments. + */ + abstract protected function parse(); + + /** + * {@inheritdoc} + */ + public function validate() + { + $definition = $this->definition; + $givenArguments = $this->arguments; + + $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) { + return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired(); + }); + + if (\count($missingArguments) > 0) { + throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments))); + } + } + + /** + * {@inheritdoc} + */ + public function isInteractive() + { + return $this->interactive; + } + + /** + * {@inheritdoc} + */ + public function setInteractive(bool $interactive) + { + $this->interactive = $interactive; + } + + /** + * {@inheritdoc} + */ + public function getArguments() + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * {@inheritdoc} + */ + public function getArgument(string $name) + { + if (!$this->definition->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault(); + } + + /** + * {@inheritdoc} + */ + public function setArgument(string $name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function hasArgument(string $name) + { + return $this->definition->hasArgument($name); + } + + /** + * {@inheritdoc} + */ + public function getOptions() + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * {@inheritdoc} + */ + public function getOption(string $name) + { + if ($this->definition->hasNegation($name)) { + if (null === $value = $this->getOption($this->definition->negationToName($name))) { + return $value; + } + + return !$value; + } + + if (!$this->definition->hasOption($name)) { + throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + } + + /** + * {@inheritdoc} + */ + public function setOption(string $name, $value) + { + if ($this->definition->hasNegation($name)) { + $this->options[$this->definition->negationToName($name)] = !$value; + + return; + } elseif (!$this->definition->hasOption($name)) { + throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function hasOption(string $name) + { + return $this->definition->hasOption($name) || $this->definition->hasNegation($name); + } + + /** + * Escapes a token through escapeshellarg if it contains unsafe chars. + * + * @return string + */ + public function escapeToken(string $token) + { + return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + } + + /** + * {@inheritdoc} + */ + public function setStream($stream) + { + $this->stream = $stream; + } + + /** + * {@inheritdoc} + */ + public function getStream() + { + return $this->stream; + } +} diff --git a/vendor/symfony/console/Input/InputArgument.php b/vendor/symfony/console/Input/InputArgument.php new file mode 100644 index 0000000..e891b94 --- /dev/null +++ b/vendor/symfony/console/Input/InputArgument.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Represents a command line argument. + * + * @author Fabien Potencier + */ +class InputArgument +{ + public const REQUIRED = 1; + public const OPTIONAL = 2; + public const IS_ARRAY = 4; + + private $name; + private $mode; + private $default; + private $description; + + /** + * @param string $name The argument name + * @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL + * @param string $description A description text + * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only) + * + * @throws InvalidArgumentException When argument mode is not valid + */ + public function __construct(string $name, int $mode = null, string $description = '', $default = null) + { + if (null === $mode) { + $mode = self::OPTIONAL; + } elseif ($mode > 7 || $mode < 1) { + throw new InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->mode = $mode; + $this->description = $description; + + $this->setDefault($default); + } + + /** + * Returns the argument name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns true if the argument is required. + * + * @return bool true if parameter mode is self::REQUIRED, false otherwise + */ + public function isRequired() + { + return self::REQUIRED === (self::REQUIRED & $this->mode); + } + + /** + * Returns true if the argument can take multiple values. + * + * @return bool true if mode is self::IS_ARRAY, false otherwise + */ + public function isArray() + { + return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); + } + + /** + * Sets the default value. + * + * @param string|bool|int|float|array|null $default + * + * @throws LogicException When incorrect default value is given + */ + public function setDefault($default = null) + { + if (self::REQUIRED === $this->mode && null !== $default) { + throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!\is_array($default)) { + throw new LogicException('A default value for an array argument must be an array.'); + } + } + + $this->default = $default; + } + + /** + * Returns the default value. + * + * @return string|bool|int|float|array|null + */ + public function getDefault() + { + return $this->default; + } + + /** + * Returns the description text. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/vendor/symfony/console/Input/InputAwareInterface.php b/vendor/symfony/console/Input/InputAwareInterface.php new file mode 100644 index 0000000..5a288de --- /dev/null +++ b/vendor/symfony/console/Input/InputAwareInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +/** + * InputAwareInterface should be implemented by classes that depends on the + * Console Input. + * + * @author Wouter J + */ +interface InputAwareInterface +{ + /** + * Sets the Console Input. + */ + public function setInput(InputInterface $input); +} diff --git a/vendor/symfony/console/Input/InputDefinition.php b/vendor/symfony/console/Input/InputDefinition.php new file mode 100644 index 0000000..11f704f --- /dev/null +++ b/vendor/symfony/console/Input/InputDefinition.php @@ -0,0 +1,424 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * A InputDefinition represents a set of valid command line arguments and options. + * + * Usage: + * + * $definition = new InputDefinition([ + * new InputArgument('name', InputArgument::REQUIRED), + * new InputOption('foo', 'f', InputOption::VALUE_REQUIRED), + * ]); + * + * @author Fabien Potencier + */ +class InputDefinition +{ + private $arguments; + private $requiredCount; + private $lastArrayArgument; + private $lastOptionalArgument; + private $options; + private $negations; + private $shortcuts; + + /** + * @param array $definition An array of InputArgument and InputOption instance + */ + public function __construct(array $definition = []) + { + $this->setDefinition($definition); + } + + /** + * Sets the definition of the input. + */ + public function setDefinition(array $definition) + { + $arguments = []; + $options = []; + foreach ($definition as $item) { + if ($item instanceof InputOption) { + $options[] = $item; + } else { + $arguments[] = $item; + } + } + + $this->setArguments($arguments); + $this->setOptions($options); + } + + /** + * Sets the InputArgument objects. + * + * @param InputArgument[] $arguments An array of InputArgument objects + */ + public function setArguments(array $arguments = []) + { + $this->arguments = []; + $this->requiredCount = 0; + $this->lastOptionalArgument = null; + $this->lastArrayArgument = null; + $this->addArguments($arguments); + } + + /** + * Adds an array of InputArgument objects. + * + * @param InputArgument[] $arguments An array of InputArgument objects + */ + public function addArguments(?array $arguments = []) + { + if (null !== $arguments) { + foreach ($arguments as $argument) { + $this->addArgument($argument); + } + } + } + + /** + * @throws LogicException When incorrect argument is given + */ + public function addArgument(InputArgument $argument) + { + if (isset($this->arguments[$argument->getName()])) { + throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); + } + + if (null !== $this->lastArrayArgument) { + throw new LogicException(sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName())); + } + + if ($argument->isRequired() && null !== $this->lastOptionalArgument) { + throw new LogicException(sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName())); + } + + if ($argument->isArray()) { + $this->lastArrayArgument = $argument; + } + + if ($argument->isRequired()) { + ++$this->requiredCount; + } else { + $this->lastOptionalArgument = $argument; + } + + $this->arguments[$argument->getName()] = $argument; + } + + /** + * Returns an InputArgument by name or by position. + * + * @param string|int $name The InputArgument name or position + * + * @return InputArgument + * + * @throws InvalidArgumentException When argument given doesn't exist + */ + public function getArgument($name) + { + if (!$this->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; + + return $arguments[$name]; + } + + /** + * Returns true if an InputArgument object exists by name or position. + * + * @param string|int $name The InputArgument name or position + * + * @return bool + */ + public function hasArgument($name) + { + $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; + + return isset($arguments[$name]); + } + + /** + * Gets the array of InputArgument objects. + * + * @return InputArgument[] + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Returns the number of InputArguments. + * + * @return int + */ + public function getArgumentCount() + { + return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments); + } + + /** + * Returns the number of required InputArguments. + * + * @return int + */ + public function getArgumentRequiredCount() + { + return $this->requiredCount; + } + + /** + * @return array + */ + public function getArgumentDefaults() + { + $values = []; + foreach ($this->arguments as $argument) { + $values[$argument->getName()] = $argument->getDefault(); + } + + return $values; + } + + /** + * Sets the InputOption objects. + * + * @param InputOption[] $options An array of InputOption objects + */ + public function setOptions(array $options = []) + { + $this->options = []; + $this->shortcuts = []; + $this->negations = []; + $this->addOptions($options); + } + + /** + * Adds an array of InputOption objects. + * + * @param InputOption[] $options An array of InputOption objects + */ + public function addOptions(array $options = []) + { + foreach ($options as $option) { + $this->addOption($option); + } + } + + /** + * @throws LogicException When option given already exist + */ + public function addOption(InputOption $option) + { + if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { + throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + if (isset($this->negations[$option->getName()])) { + throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + if (isset($this->shortcuts[$shortcut]) && !$option->equals($this->options[$this->shortcuts[$shortcut]])) { + throw new LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); + } + } + } + + $this->options[$option->getName()] = $option; + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + $this->shortcuts[$shortcut] = $option->getName(); + } + } + + if ($option->isNegatable()) { + $negatedName = 'no-'.$option->getName(); + if (isset($this->options[$negatedName])) { + throw new LogicException(sprintf('An option named "%s" already exists.', $negatedName)); + } + $this->negations[$negatedName] = $option->getName(); + } + } + + /** + * Returns an InputOption by name. + * + * @return InputOption + * + * @throws InvalidArgumentException When option given doesn't exist + */ + public function getOption(string $name) + { + if (!$this->hasOption($name)) { + throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->options[$name]; + } + + /** + * Returns true if an InputOption object exists by name. + * + * This method can't be used to check if the user included the option when + * executing the command (use getOption() instead). + * + * @return bool + */ + public function hasOption(string $name) + { + return isset($this->options[$name]); + } + + /** + * Gets the array of InputOption objects. + * + * @return InputOption[] + */ + public function getOptions() + { + return $this->options; + } + + /** + * Returns true if an InputOption object exists by shortcut. + * + * @return bool + */ + public function hasShortcut(string $name) + { + return isset($this->shortcuts[$name]); + } + + /** + * Returns true if an InputOption object exists by negated name. + */ + public function hasNegation(string $name): bool + { + return isset($this->negations[$name]); + } + + /** + * Gets an InputOption by shortcut. + * + * @return InputOption + */ + public function getOptionForShortcut(string $shortcut) + { + return $this->getOption($this->shortcutToName($shortcut)); + } + + /** + * @return array + */ + public function getOptionDefaults() + { + $values = []; + foreach ($this->options as $option) { + $values[$option->getName()] = $option->getDefault(); + } + + return $values; + } + + /** + * Returns the InputOption name given a shortcut. + * + * @throws InvalidArgumentException When option given does not exist + * + * @internal + */ + public function shortcutToName(string $shortcut): string + { + if (!isset($this->shortcuts[$shortcut])) { + throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + return $this->shortcuts[$shortcut]; + } + + /** + * Returns the InputOption name given a negation. + * + * @throws InvalidArgumentException When option given does not exist + * + * @internal + */ + public function negationToName(string $negation): string + { + if (!isset($this->negations[$negation])) { + throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $negation)); + } + + return $this->negations[$negation]; + } + + /** + * Gets the synopsis. + * + * @return string + */ + public function getSynopsis(bool $short = false) + { + $elements = []; + + if ($short && $this->getOptions()) { + $elements[] = '[options]'; + } elseif (!$short) { + foreach ($this->getOptions() as $option) { + $value = ''; + if ($option->acceptValue()) { + $value = sprintf( + ' %s%s%s', + $option->isValueOptional() ? '[' : '', + strtoupper($option->getName()), + $option->isValueOptional() ? ']' : '' + ); + } + + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $negation = $option->isNegatable() ? sprintf('|--no-%s', $option->getName()) : ''; + $elements[] = sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation); + } + } + + if (\count($elements) && $this->getArguments()) { + $elements[] = '[--]'; + } + + $tail = ''; + foreach ($this->getArguments() as $argument) { + $element = '<'.$argument->getName().'>'; + if ($argument->isArray()) { + $element .= '...'; + } + + if (!$argument->isRequired()) { + $element = '['.$element; + $tail .= ']'; + } + + $elements[] = $element; + } + + return implode(' ', $elements).$tail; + } +} diff --git a/vendor/symfony/console/Input/InputInterface.php b/vendor/symfony/console/Input/InputInterface.php new file mode 100644 index 0000000..628b603 --- /dev/null +++ b/vendor/symfony/console/Input/InputInterface.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; + +/** + * InputInterface is the interface implemented by all input classes. + * + * @author Fabien Potencier + */ +interface InputInterface +{ + /** + * Returns the first argument from the raw parameters (not parsed). + * + * @return string|null + */ + public function getFirstArgument(); + + /** + * Returns true if the raw parameters (not parsed) contain a value. + * + * This method is to be used to introspect the input parameters + * before they have been validated. It must be used carefully. + * Does not necessarily return the correct result for short options + * when multiple flags are combined in the same option. + * + * @param string|array $values The values to look for in the raw parameters (can be an array) + * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal + * + * @return bool + */ + public function hasParameterOption($values, bool $onlyParams = false); + + /** + * Returns the value of a raw option (not parsed). + * + * This method is to be used to introspect the input parameters + * before they have been validated. It must be used carefully. + * Does not necessarily return the correct result for short options + * when multiple flags are combined in the same option. + * + * @param string|array $values The value(s) to look for in the raw parameters (can be an array) + * @param string|bool|int|float|array|null $default The default value to return if no result is found + * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal + * + * @return mixed + */ + public function getParameterOption($values, $default = false, bool $onlyParams = false); + + /** + * Binds the current Input instance with the given arguments and options. + * + * @throws RuntimeException + */ + public function bind(InputDefinition $definition); + + /** + * Validates the input. + * + * @throws RuntimeException When not enough arguments are given + */ + public function validate(); + + /** + * Returns all the given arguments merged with the default values. + * + * @return array + */ + public function getArguments(); + + /** + * Returns the argument value for a given argument name. + * + * @return mixed + * + * @throws InvalidArgumentException When argument given doesn't exist + */ + public function getArgument(string $name); + + /** + * Sets an argument value by name. + * + * @param mixed $value The argument value + * + * @throws InvalidArgumentException When argument given doesn't exist + */ + public function setArgument(string $name, $value); + + /** + * Returns true if an InputArgument object exists by name or position. + * + * @return bool + */ + public function hasArgument(string $name); + + /** + * Returns all the given options merged with the default values. + * + * @return array + */ + public function getOptions(); + + /** + * Returns the option value for a given option name. + * + * @return mixed + * + * @throws InvalidArgumentException When option given doesn't exist + */ + public function getOption(string $name); + + /** + * Sets an option value by name. + * + * @param mixed $value The option value + * + * @throws InvalidArgumentException When option given doesn't exist + */ + public function setOption(string $name, $value); + + /** + * Returns true if an InputOption object exists by name. + * + * @return bool + */ + public function hasOption(string $name); + + /** + * Is this input means interactive? + * + * @return bool + */ + public function isInteractive(); + + /** + * Sets the input interactivity. + */ + public function setInteractive(bool $interactive); +} diff --git a/vendor/symfony/console/Input/InputOption.php b/vendor/symfony/console/Input/InputOption.php new file mode 100644 index 0000000..2bec34f --- /dev/null +++ b/vendor/symfony/console/Input/InputOption.php @@ -0,0 +1,231 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Represents a command line option. + * + * @author Fabien Potencier + */ +class InputOption +{ + /** + * Do not accept input for the option (e.g. --yell). This is the default behavior of options. + */ + public const VALUE_NONE = 1; + + /** + * A value must be passed when the option is used (e.g. --iterations=5 or -i5). + */ + public const VALUE_REQUIRED = 2; + + /** + * The option may or may not have a value (e.g. --yell or --yell=loud). + */ + public const VALUE_OPTIONAL = 4; + + /** + * The option accepts multiple values (e.g. --dir=/foo --dir=/bar). + */ + public const VALUE_IS_ARRAY = 8; + + /** + * The option may have either positive or negative value (e.g. --ansi or --no-ansi). + */ + public const VALUE_NEGATABLE = 16; + + private $name; + private $shortcut; + private $mode; + private $default; + private $description; + + /** + * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the VALUE_* constants + * @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE) + * + * @throws InvalidArgumentException If option mode is invalid or incompatible + */ + public function __construct(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null) + { + if (str_starts_with($name, '--')) { + $name = substr($name, 2); + } + + if (empty($name)) { + throw new InvalidArgumentException('An option name cannot be empty.'); + } + + if (empty($shortcut)) { + $shortcut = null; + } + + if (null !== $shortcut) { + if (\is_array($shortcut)) { + $shortcut = implode('|', $shortcut); + } + $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = array_filter($shortcuts); + $shortcut = implode('|', $shortcuts); + + if (empty($shortcut)) { + throw new InvalidArgumentException('An option shortcut cannot be empty.'); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) { + throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + + if ($this->isArray() && !$this->acceptValue()) { + throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + if ($this->isNegatable() && $this->acceptValue()) { + throw new InvalidArgumentException('Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.'); + } + + $this->setDefault($default); + } + + /** + * Returns the option shortcut. + * + * @return string|null + */ + public function getShortcut() + { + return $this->shortcut; + } + + /** + * Returns the option name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns true if the option accepts a value. + * + * @return bool true if value mode is not self::VALUE_NONE, false otherwise + */ + public function acceptValue() + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * Returns true if the option requires a value. + * + * @return bool true if value mode is self::VALUE_REQUIRED, false otherwise + */ + public function isValueRequired() + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * Returns true if the option takes an optional value. + * + * @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise + */ + public function isValueOptional() + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * Returns true if the option can take multiple values. + * + * @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise + */ + public function isArray() + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + public function isNegatable(): bool + { + return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode); + } + + /** + * @param string|bool|int|float|array|null $default + */ + public function setDefault($default = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!\is_array($default)) { + throw new LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() || $this->isNegatable() ? $default : false; + } + + /** + * Returns the default value. + * + * @return string|bool|int|float|array|null + */ + public function getDefault() + { + return $this->default; + } + + /** + * Returns the description text. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Checks whether the given option equals this one. + * + * @return bool + */ + public function equals(self $option) + { + return $option->getName() === $this->getName() + && $option->getShortcut() === $this->getShortcut() + && $option->getDefault() === $this->getDefault() + && $option->isNegatable() === $this->isNegatable() + && $option->isArray() === $this->isArray() + && $option->isValueRequired() === $this->isValueRequired() + && $option->isValueOptional() === $this->isValueOptional() + ; + } +} diff --git a/vendor/symfony/console/Input/StreamableInputInterface.php b/vendor/symfony/console/Input/StreamableInputInterface.php new file mode 100644 index 0000000..d7e462f --- /dev/null +++ b/vendor/symfony/console/Input/StreamableInputInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +/** + * StreamableInputInterface is the interface implemented by all input classes + * that have an input stream. + * + * @author Robin Chalas + */ +interface StreamableInputInterface extends InputInterface +{ + /** + * Sets the input stream to read from when interacting with the user. + * + * This is mainly useful for testing purpose. + * + * @param resource $stream The input stream + */ + public function setStream($stream); + + /** + * Returns the input stream. + * + * @return resource|null + */ + public function getStream(); +} diff --git a/vendor/symfony/console/Input/StringInput.php b/vendor/symfony/console/Input/StringInput.php new file mode 100644 index 0000000..eb5c07f --- /dev/null +++ b/vendor/symfony/console/Input/StringInput.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * StringInput represents an input provided as a string. + * + * Usage: + * + * $input = new StringInput('foo --bar="foobar"'); + * + * @author Fabien Potencier + */ +class StringInput extends ArgvInput +{ + public const REGEX_STRING = '([^\s]+?)(?:\s|(?setTokens($this->tokenize($input)); + } + + /** + * Tokenizes a string. + * + * @throws InvalidArgumentException When unable to parse input (should never happen) + */ + private function tokenize(string $input): array + { + $tokens = []; + $length = \strlen($input); + $cursor = 0; + while ($cursor < $length) { + if (preg_match('/\s+/A', $input, $match, 0, $cursor)) { + } elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) { + $tokens[] = $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, -1))); + } elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) { + $tokens[] = stripcslashes(substr($match[0], 1, -1)); + } elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, 0, $cursor)) { + $tokens[] = stripcslashes($match[1]); + } else { + // should never happen + throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10))); + } + + $cursor += \strlen($match[0]); + } + + return $tokens; + } +} diff --git a/vendor/symfony/console/LICENSE b/vendor/symfony/console/LICENSE new file mode 100644 index 0000000..9ff2d0d --- /dev/null +++ b/vendor/symfony/console/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/console/Logger/ConsoleLogger.php b/vendor/symfony/console/Logger/ConsoleLogger.php new file mode 100644 index 0000000..c9ee035 --- /dev/null +++ b/vendor/symfony/console/Logger/ConsoleLogger.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Logger; + +use Psr\Log\AbstractLogger; +use Psr\Log\InvalidArgumentException; +use Psr\Log\LogLevel; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * PSR-3 compliant console logger. + * + * @author Kévin Dunglas + * + * @see https://www.php-fig.org/psr/psr-3/ + */ +class ConsoleLogger extends AbstractLogger +{ + public const INFO = 'info'; + public const ERROR = 'error'; + + private $output; + private $verbosityLevelMap = [ + LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL, + LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, + LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, + LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, + LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, + LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE, + LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE, + LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG, + ]; + private $formatLevelMap = [ + LogLevel::EMERGENCY => self::ERROR, + LogLevel::ALERT => self::ERROR, + LogLevel::CRITICAL => self::ERROR, + LogLevel::ERROR => self::ERROR, + LogLevel::WARNING => self::INFO, + LogLevel::NOTICE => self::INFO, + LogLevel::INFO => self::INFO, + LogLevel::DEBUG => self::INFO, + ]; + private $errored = false; + + public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = []) + { + $this->output = $output; + $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap; + $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap; + } + + /** + * {@inheritdoc} + * + * @return void + */ + public function log($level, $message, array $context = []) + { + if (!isset($this->verbosityLevelMap[$level])) { + throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); + } + + $output = $this->output; + + // Write to the error output if necessary and available + if (self::ERROR === $this->formatLevelMap[$level]) { + if ($this->output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + $this->errored = true; + } + + // the if condition check isn't necessary -- it's the same one that $output will do internally anyway. + // We only do it for efficiency here as the message formatting is relatively expensive. + if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) { + $output->writeln(sprintf('<%1$s>[%2$s] %3$s', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]); + } + } + + /** + * Returns true when any messages have been logged at error levels. + * + * @return bool + */ + public function hasErrored() + { + return $this->errored; + } + + /** + * Interpolates context values into the message placeholders. + * + * @author PHP Framework Interoperability Group + */ + private function interpolate(string $message, array $context): string + { + if (!str_contains($message, '{')) { + return $message; + } + + $replacements = []; + foreach ($context as $key => $val) { + if (null === $val || is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { + $replacements["{{$key}}"] = $val; + } elseif ($val instanceof \DateTimeInterface) { + $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); + } elseif (\is_object($val)) { + $replacements["{{$key}}"] = '[object '.\get_class($val).']'; + } else { + $replacements["{{$key}}"] = '['.\gettype($val).']'; + } + } + + return strtr($message, $replacements); + } +} diff --git a/vendor/symfony/console/Output/BufferedOutput.php b/vendor/symfony/console/Output/BufferedOutput.php new file mode 100644 index 0000000..d37c6e3 --- /dev/null +++ b/vendor/symfony/console/Output/BufferedOutput.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +/** + * @author Jean-François Simon + */ +class BufferedOutput extends Output +{ + private $buffer = ''; + + /** + * Empties buffer and returns its content. + * + * @return string + */ + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + + return $content; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $message, bool $newline) + { + $this->buffer .= $message; + + if ($newline) { + $this->buffer .= \PHP_EOL; + } + } +} diff --git a/vendor/symfony/console/Output/ConsoleOutput.php b/vendor/symfony/console/Output/ConsoleOutput.php new file mode 100644 index 0000000..2cda213 --- /dev/null +++ b/vendor/symfony/console/Output/ConsoleOutput.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR. + * + * This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR. + * + * $output = new ConsoleOutput(); + * + * This is equivalent to: + * + * $output = new StreamOutput(fopen('php://stdout', 'w')); + * $stdErr = new StreamOutput(fopen('php://stderr', 'w')); + * + * @author Fabien Potencier + */ +class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface +{ + private $stderr; + private $consoleSectionOutputs = []; + + /** + * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) + * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) + */ + public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null) + { + parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter); + + if (null === $formatter) { + // for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter. + $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated); + + return; + } + + $actualDecorated = $this->isDecorated(); + $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter()); + + if (null === $decorated) { + $this->setDecorated($actualDecorated && $this->stderr->isDecorated()); + } + } + + /** + * Creates a new output section. + */ + public function section(): ConsoleSectionOutput + { + return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); + } + + /** + * {@inheritdoc} + */ + public function setDecorated(bool $decorated) + { + parent::setDecorated($decorated); + $this->stderr->setDecorated($decorated); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + parent::setFormatter($formatter); + $this->stderr->setFormatter($formatter); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity(int $level) + { + parent::setVerbosity($level); + $this->stderr->setVerbosity($level); + } + + /** + * {@inheritdoc} + */ + public function getErrorOutput() + { + return $this->stderr; + } + + /** + * {@inheritdoc} + */ + public function setErrorOutput(OutputInterface $error) + { + $this->stderr = $error; + } + + /** + * Returns true if current environment supports writing console output to + * STDOUT. + * + * @return bool + */ + protected function hasStdoutSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * Returns true if current environment supports writing console output to + * STDERR. + * + * @return bool + */ + protected function hasStderrSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * Checks if current executing environment is IBM iSeries (OS400), which + * doesn't properly convert character-encodings between ASCII to EBCDIC. + */ + private function isRunningOS400(): bool + { + $checks = [ + \function_exists('php_uname') ? php_uname('s') : '', + getenv('OSTYPE'), + \PHP_OS, + ]; + + return false !== stripos(implode(';', $checks), 'OS400'); + } + + /** + * @return resource + */ + private function openOutputStream() + { + if (!$this->hasStdoutSupport()) { + return fopen('php://output', 'w'); + } + + return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); + } + + /** + * @return resource + */ + private function openErrorStream() + { + return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); + } +} diff --git a/vendor/symfony/console/Output/ConsoleOutputInterface.php b/vendor/symfony/console/Output/ConsoleOutputInterface.php new file mode 100644 index 0000000..6b6635f --- /dev/null +++ b/vendor/symfony/console/Output/ConsoleOutputInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +/** + * ConsoleOutputInterface is the interface implemented by ConsoleOutput class. + * This adds information about stderr and section output stream. + * + * @author Dariusz Górecki + */ +interface ConsoleOutputInterface extends OutputInterface +{ + /** + * Gets the OutputInterface for errors. + * + * @return OutputInterface + */ + public function getErrorOutput(); + + public function setErrorOutput(OutputInterface $error); + + public function section(): ConsoleSectionOutput; +} diff --git a/vendor/symfony/console/Output/ConsoleSectionOutput.php b/vendor/symfony/console/Output/ConsoleSectionOutput.php new file mode 100644 index 0000000..8f16497 --- /dev/null +++ b/vendor/symfony/console/Output/ConsoleSectionOutput.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Terminal; + +/** + * @author Pierre du Plessis + * @author Gabriel Ostrolucký + */ +class ConsoleSectionOutput extends StreamOutput +{ + private $content = []; + private $lines = 0; + private $sections; + private $terminal; + + /** + * @param resource $stream + * @param ConsoleSectionOutput[] $sections + */ + public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter) + { + parent::__construct($stream, $verbosity, $decorated, $formatter); + array_unshift($sections, $this); + $this->sections = &$sections; + $this->terminal = new Terminal(); + } + + /** + * Clears previous output for this section. + * + * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared + */ + public function clear(int $lines = null) + { + if (empty($this->content) || !$this->isDecorated()) { + return; + } + + if ($lines) { + array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content + } else { + $lines = $this->lines; + $this->content = []; + } + + $this->lines -= $lines; + + parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false); + } + + /** + * Overwrites the previous output with a new message. + * + * @param array|string $message + */ + public function overwrite($message) + { + $this->clear(); + $this->writeln($message); + } + + public function getContent(): string + { + return implode('', $this->content); + } + + /** + * @internal + */ + public function addContent(string $input) + { + foreach (explode(\PHP_EOL, $input) as $lineContent) { + $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1; + $this->content[] = $lineContent; + $this->content[] = \PHP_EOL; + } + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $message, bool $newline) + { + if (!$this->isDecorated()) { + parent::doWrite($message, $newline); + + return; + } + + $erasedContent = $this->popStreamContentUntilCurrentSection(); + + $this->addContent($message); + + parent::doWrite($message, true); + parent::doWrite($erasedContent, false); + } + + /** + * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits + * current section. Then it erases content it crawled through. Optionally, it erases part of current section too. + */ + private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string + { + $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection; + $erasedContent = []; + + foreach ($this->sections as $section) { + if ($section === $this) { + break; + } + + $numberOfLinesToClear += $section->lines; + $erasedContent[] = $section->getContent(); + } + + if ($numberOfLinesToClear > 0) { + // move cursor up n lines + parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false); + // erase to end of screen + parent::doWrite("\x1b[0J", false); + } + + return implode('', array_reverse($erasedContent)); + } + + private function getDisplayLength(string $text): int + { + return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text))); + } +} diff --git a/vendor/symfony/console/Output/NullOutput.php b/vendor/symfony/console/Output/NullOutput.php new file mode 100644 index 0000000..3bbe63e --- /dev/null +++ b/vendor/symfony/console/Output/NullOutput.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\NullOutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * NullOutput suppresses all output. + * + * $output = new NullOutput(); + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +class NullOutput implements OutputInterface +{ + private $formatter; + + /** + * {@inheritdoc} + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function getFormatter() + { + if ($this->formatter) { + return $this->formatter; + } + // to comply with the interface we must return a OutputFormatterInterface + return $this->formatter = new NullOutputFormatter(); + } + + /** + * {@inheritdoc} + */ + public function setDecorated(bool $decorated) + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function isDecorated() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function setVerbosity(int $level) + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function getVerbosity() + { + return self::VERBOSITY_QUIET; + } + + /** + * {@inheritdoc} + */ + public function isQuiet() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function isVerbose() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function isVeryVerbose() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function isDebug() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function writeln($messages, int $options = self::OUTPUT_NORMAL) + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) + { + // do nothing + } +} diff --git a/vendor/symfony/console/Output/Output.php b/vendor/symfony/console/Output/Output.php new file mode 100644 index 0000000..d7c5fb2 --- /dev/null +++ b/vendor/symfony/console/Output/Output.php @@ -0,0 +1,174 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * Base class for output classes. + * + * There are five levels of verbosity: + * + * * normal: no option passed (normal output) + * * verbose: -v (more output) + * * very verbose: -vv (highly extended output) + * * debug: -vvv (all debug output) + * * quiet: -q (no output) + * + * @author Fabien Potencier + */ +abstract class Output implements OutputInterface +{ + private $verbosity; + private $formatter; + + /** + * @param int|null $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param bool $decorated Whether to decorate messages + * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) + */ + public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null) + { + $this->verbosity = $verbosity ?? self::VERBOSITY_NORMAL; + $this->formatter = $formatter ?? new OutputFormatter(); + $this->formatter->setDecorated($decorated); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + $this->formatter = $formatter; + } + + /** + * {@inheritdoc} + */ + public function getFormatter() + { + return $this->formatter; + } + + /** + * {@inheritdoc} + */ + public function setDecorated(bool $decorated) + { + $this->formatter->setDecorated($decorated); + } + + /** + * {@inheritdoc} + */ + public function isDecorated() + { + return $this->formatter->isDecorated(); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity(int $level) + { + $this->verbosity = $level; + } + + /** + * {@inheritdoc} + */ + public function getVerbosity() + { + return $this->verbosity; + } + + /** + * {@inheritdoc} + */ + public function isQuiet() + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + /** + * {@inheritdoc} + */ + public function isVerbose() + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + /** + * {@inheritdoc} + */ + public function isVeryVerbose() + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + /** + * {@inheritdoc} + */ + public function isDebug() + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + /** + * {@inheritdoc} + */ + public function writeln($messages, int $options = self::OUTPUT_NORMAL) + { + $this->write($messages, true, $options); + } + + /** + * {@inheritdoc} + */ + public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + $types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN; + $type = $types & $options ?: self::OUTPUT_NORMAL; + + $verbosities = self::VERBOSITY_QUIET | self::VERBOSITY_NORMAL | self::VERBOSITY_VERBOSE | self::VERBOSITY_VERY_VERBOSE | self::VERBOSITY_DEBUG; + $verbosity = $verbosities & $options ?: self::VERBOSITY_NORMAL; + + if ($verbosity > $this->getVerbosity()) { + return; + } + + foreach ($messages as $message) { + switch ($type) { + case OutputInterface::OUTPUT_NORMAL: + $message = $this->formatter->format($message); + break; + case OutputInterface::OUTPUT_RAW: + break; + case OutputInterface::OUTPUT_PLAIN: + $message = strip_tags($this->formatter->format($message)); + break; + } + + $this->doWrite($message ?? '', $newline); + } + } + + /** + * Writes a message to the output. + */ + abstract protected function doWrite(string $message, bool $newline); +} diff --git a/vendor/symfony/console/Output/OutputInterface.php b/vendor/symfony/console/Output/OutputInterface.php new file mode 100644 index 0000000..55caab8 --- /dev/null +++ b/vendor/symfony/console/Output/OutputInterface.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * OutputInterface is the interface implemented by all Output classes. + * + * @author Fabien Potencier + */ +interface OutputInterface +{ + public const VERBOSITY_QUIET = 16; + public const VERBOSITY_NORMAL = 32; + public const VERBOSITY_VERBOSE = 64; + public const VERBOSITY_VERY_VERBOSE = 128; + public const VERBOSITY_DEBUG = 256; + + public const OUTPUT_NORMAL = 1; + public const OUTPUT_RAW = 2; + public const OUTPUT_PLAIN = 4; + + /** + * Writes a message to the output. + * + * @param string|iterable $messages The message as an iterable of strings or a single string + * @param bool $newline Whether to add a newline + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + */ + public function write($messages, bool $newline = false, int $options = 0); + + /** + * Writes a message to the output and adds a newline at the end. + * + * @param string|iterable $messages The message as an iterable of strings or a single string + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + */ + public function writeln($messages, int $options = 0); + + /** + * Sets the verbosity of the output. + */ + public function setVerbosity(int $level); + + /** + * Gets the current verbosity of the output. + * + * @return int + */ + public function getVerbosity(); + + /** + * Returns whether verbosity is quiet (-q). + * + * @return bool + */ + public function isQuiet(); + + /** + * Returns whether verbosity is verbose (-v). + * + * @return bool + */ + public function isVerbose(); + + /** + * Returns whether verbosity is very verbose (-vv). + * + * @return bool + */ + public function isVeryVerbose(); + + /** + * Returns whether verbosity is debug (-vvv). + * + * @return bool + */ + public function isDebug(); + + /** + * Sets the decorated flag. + */ + public function setDecorated(bool $decorated); + + /** + * Gets the decorated flag. + * + * @return bool + */ + public function isDecorated(); + + public function setFormatter(OutputFormatterInterface $formatter); + + /** + * Returns current output formatter instance. + * + * @return OutputFormatterInterface + */ + public function getFormatter(); +} diff --git a/vendor/symfony/console/Output/StreamOutput.php b/vendor/symfony/console/Output/StreamOutput.php new file mode 100644 index 0000000..7f55518 --- /dev/null +++ b/vendor/symfony/console/Output/StreamOutput.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * StreamOutput writes the output to a given stream. + * + * Usage: + * + * $output = new StreamOutput(fopen('php://stdout', 'w')); + * + * As `StreamOutput` can use any stream, you can also use a file: + * + * $output = new StreamOutput(fopen('/path/to/output.log', 'a', false)); + * + * @author Fabien Potencier + */ +class StreamOutput extends Output +{ + private $stream; + + /** + * @param resource $stream A stream resource + * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) + * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) + * + * @throws InvalidArgumentException When first argument is not a real stream + */ + public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null) + { + if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) { + throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.'); + } + + $this->stream = $stream; + + if (null === $decorated) { + $decorated = $this->hasColorSupport(); + } + + parent::__construct($verbosity, $decorated, $formatter); + } + + /** + * Gets the stream attached to this StreamOutput instance. + * + * @return resource + */ + public function getStream() + { + return $this->stream; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $message, bool $newline) + { + if ($newline) { + $message .= \PHP_EOL; + } + + @fwrite($this->stream, $message); + + fflush($this->stream); + } + + /** + * Returns true if the stream supports colorization. + * + * Colorization is disabled if not supported by the stream: + * + * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo + * terminals via named pipes, so we can only check the environment. + * + * Reference: Composer\XdebugHandler\Process::supportsColor + * https://github.com/composer/xdebug-handler + * + * @return bool true if the stream supports colorization, false otherwise + */ + protected function hasColorSupport() + { + // Follow https://no-color.org/ + if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { + return false; + } + + if ('Hyper' === getenv('TERM_PROGRAM')) { + return true; + } + + if (\DIRECTORY_SEPARATOR === '\\') { + return (\function_exists('sapi_windows_vt100_support') + && @sapi_windows_vt100_support($this->stream)) + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + return stream_isatty($this->stream); + } +} diff --git a/vendor/symfony/console/Output/TrimmedBufferOutput.php b/vendor/symfony/console/Output/TrimmedBufferOutput.php new file mode 100644 index 0000000..3f4d375 --- /dev/null +++ b/vendor/symfony/console/Output/TrimmedBufferOutput.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * A BufferedOutput that keeps only the last N chars. + * + * @author Jérémy Derussé + */ +class TrimmedBufferOutput extends Output +{ + private $maxLength; + private $buffer = ''; + + public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null) + { + if ($maxLength <= 0) { + throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength)); + } + + parent::__construct($verbosity, $decorated, $formatter); + $this->maxLength = $maxLength; + } + + /** + * Empties buffer and returns its content. + * + * @return string + */ + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + + return $content; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $message, bool $newline) + { + $this->buffer .= $message; + + if ($newline) { + $this->buffer .= \PHP_EOL; + } + + $this->buffer = substr($this->buffer, 0 - $this->maxLength); + } +} diff --git a/vendor/symfony/console/Question/ChoiceQuestion.php b/vendor/symfony/console/Question/ChoiceQuestion.php new file mode 100644 index 0000000..92b6e86 --- /dev/null +++ b/vendor/symfony/console/Question/ChoiceQuestion.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Question; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * Represents a choice question. + * + * @author Fabien Potencier + */ +class ChoiceQuestion extends Question +{ + private $choices; + private $multiselect = false; + private $prompt = ' > '; + private $errorMessage = 'Value "%s" is invalid'; + + /** + * @param string $question The question to ask to the user + * @param array $choices The list of available choices + * @param mixed $default The default answer to return + */ + public function __construct(string $question, array $choices, $default = null) + { + if (!$choices) { + throw new \LogicException('Choice question must have at least 1 choice available.'); + } + + parent::__construct($question, $default); + + $this->choices = $choices; + $this->setValidator($this->getDefaultValidator()); + $this->setAutocompleterValues($choices); + } + + /** + * Returns available choices. + * + * @return array + */ + public function getChoices() + { + return $this->choices; + } + + /** + * Sets multiselect option. + * + * When multiselect is set to true, multiple choices can be answered. + * + * @return $this + */ + public function setMultiselect(bool $multiselect) + { + $this->multiselect = $multiselect; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + /** + * Returns whether the choices are multiselect. + * + * @return bool + */ + public function isMultiselect() + { + return $this->multiselect; + } + + /** + * Gets the prompt for choices. + * + * @return string + */ + public function getPrompt() + { + return $this->prompt; + } + + /** + * Sets the prompt for choices. + * + * @return $this + */ + public function setPrompt(string $prompt) + { + $this->prompt = $prompt; + + return $this; + } + + /** + * Sets the error message for invalid values. + * + * The error message has a string placeholder (%s) for the invalid value. + * + * @return $this + */ + public function setErrorMessage(string $errorMessage) + { + $this->errorMessage = $errorMessage; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + private function getDefaultValidator(): callable + { + $choices = $this->choices; + $errorMessage = $this->errorMessage; + $multiselect = $this->multiselect; + $isAssoc = $this->isAssoc($choices); + + return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { + if ($multiselect) { + // Check for a separated comma values + if (!preg_match('/^[^,]+(?:,[^,]+)*$/', $selected, $matches)) { + throw new InvalidArgumentException(sprintf($errorMessage, $selected)); + } + + $selectedChoices = explode(',', $selected); + } else { + $selectedChoices = [$selected]; + } + + if ($this->isTrimmable()) { + foreach ($selectedChoices as $k => $v) { + $selectedChoices[$k] = trim($v); + } + } + + $multiselectChoices = []; + foreach ($selectedChoices as $value) { + $results = []; + foreach ($choices as $key => $choice) { + if ($choice === $value) { + $results[] = $key; + } + } + + if (\count($results) > 1) { + throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results))); + } + + $result = array_search($value, $choices); + + if (!$isAssoc) { + if (false !== $result) { + $result = $choices[$result]; + } elseif (isset($choices[$value])) { + $result = $choices[$value]; + } + } elseif (false === $result && isset($choices[$value])) { + $result = $value; + } + + if (false === $result) { + throw new InvalidArgumentException(sprintf($errorMessage, $value)); + } + + // For associative choices, consistently return the key as string: + $multiselectChoices[] = $isAssoc ? (string) $result : $result; + } + + if ($multiselect) { + return $multiselectChoices; + } + + return current($multiselectChoices); + }; + } +} diff --git a/vendor/symfony/console/Question/ConfirmationQuestion.php b/vendor/symfony/console/Question/ConfirmationQuestion.php new file mode 100644 index 0000000..4228521 --- /dev/null +++ b/vendor/symfony/console/Question/ConfirmationQuestion.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Question; + +/** + * Represents a yes/no question. + * + * @author Fabien Potencier + */ +class ConfirmationQuestion extends Question +{ + private $trueAnswerRegex; + + /** + * @param string $question The question to ask to the user + * @param bool $default The default answer to return, true or false + * @param string $trueAnswerRegex A regex to match the "yes" answer + */ + public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y/i') + { + parent::__construct($question, $default); + + $this->trueAnswerRegex = $trueAnswerRegex; + $this->setNormalizer($this->getDefaultNormalizer()); + } + + /** + * Returns the default answer normalizer. + */ + private function getDefaultNormalizer(): callable + { + $default = $this->getDefault(); + $regex = $this->trueAnswerRegex; + + return function ($answer) use ($default, $regex) { + if (\is_bool($answer)) { + return $answer; + } + + $answerIsTrue = (bool) preg_match($regex, $answer); + if (false === $default) { + return $answer && $answerIsTrue; + } + + return '' === $answer || $answerIsTrue; + }; + } +} diff --git a/vendor/symfony/console/Question/Question.php b/vendor/symfony/console/Question/Question.php new file mode 100644 index 0000000..3a73f04 --- /dev/null +++ b/vendor/symfony/console/Question/Question.php @@ -0,0 +1,299 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Question; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Represents a Question. + * + * @author Fabien Potencier + */ +class Question +{ + private $question; + private $attempts; + private $hidden = false; + private $hiddenFallback = true; + private $autocompleterCallback; + private $validator; + private $default; + private $normalizer; + private $trimmable = true; + private $multiline = false; + + /** + * @param string $question The question to ask to the user + * @param string|bool|int|float|null $default The default answer to return if the user enters nothing + */ + public function __construct(string $question, $default = null) + { + $this->question = $question; + $this->default = $default; + } + + /** + * Returns the question. + * + * @return string + */ + public function getQuestion() + { + return $this->question; + } + + /** + * Returns the default answer. + * + * @return string|bool|int|float|null + */ + public function getDefault() + { + return $this->default; + } + + /** + * Returns whether the user response accepts newline characters. + */ + public function isMultiline(): bool + { + return $this->multiline; + } + + /** + * Sets whether the user response should accept newline characters. + * + * @return $this + */ + public function setMultiline(bool $multiline): self + { + $this->multiline = $multiline; + + return $this; + } + + /** + * Returns whether the user response must be hidden. + * + * @return bool + */ + public function isHidden() + { + return $this->hidden; + } + + /** + * Sets whether the user response must be hidden or not. + * + * @return $this + * + * @throws LogicException In case the autocompleter is also used + */ + public function setHidden(bool $hidden) + { + if ($this->autocompleterCallback) { + throw new LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->hidden = $hidden; + + return $this; + } + + /** + * In case the response cannot be hidden, whether to fallback on non-hidden question or not. + * + * @return bool + */ + public function isHiddenFallback() + { + return $this->hiddenFallback; + } + + /** + * Sets whether to fallback on non-hidden question if the response cannot be hidden. + * + * @return $this + */ + public function setHiddenFallback(bool $fallback) + { + $this->hiddenFallback = $fallback; + + return $this; + } + + /** + * Gets values for the autocompleter. + * + * @return iterable|null + */ + public function getAutocompleterValues() + { + $callback = $this->getAutocompleterCallback(); + + return $callback ? $callback('') : null; + } + + /** + * Sets values for the autocompleter. + * + * @return $this + * + * @throws LogicException + */ + public function setAutocompleterValues(?iterable $values) + { + if (\is_array($values)) { + $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values); + + $callback = static function () use ($values) { + return $values; + }; + } elseif ($values instanceof \Traversable) { + $valueCache = null; + $callback = static function () use ($values, &$valueCache) { + return $valueCache ?? $valueCache = iterator_to_array($values, false); + }; + } else { + $callback = null; + } + + return $this->setAutocompleterCallback($callback); + } + + /** + * Gets the callback function used for the autocompleter. + */ + public function getAutocompleterCallback(): ?callable + { + return $this->autocompleterCallback; + } + + /** + * Sets the callback function used for the autocompleter. + * + * The callback is passed the user input as argument and should return an iterable of corresponding suggestions. + * + * @return $this + */ + public function setAutocompleterCallback(callable $callback = null): self + { + if ($this->hidden && null !== $callback) { + throw new LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->autocompleterCallback = $callback; + + return $this; + } + + /** + * Sets a validator for the question. + * + * @return $this + */ + public function setValidator(callable $validator = null) + { + $this->validator = $validator; + + return $this; + } + + /** + * Gets the validator for the question. + * + * @return callable|null + */ + public function getValidator() + { + return $this->validator; + } + + /** + * Sets the maximum number of attempts. + * + * Null means an unlimited number of attempts. + * + * @return $this + * + * @throws InvalidArgumentException in case the number of attempts is invalid + */ + public function setMaxAttempts(?int $attempts) + { + if (null !== $attempts && $attempts < 1) { + throw new InvalidArgumentException('Maximum number of attempts must be a positive value.'); + } + + $this->attempts = $attempts; + + return $this; + } + + /** + * Gets the maximum number of attempts. + * + * Null means an unlimited number of attempts. + * + * @return int|null + */ + public function getMaxAttempts() + { + return $this->attempts; + } + + /** + * Sets a normalizer for the response. + * + * The normalizer can be a callable (a string), a closure or a class implementing __invoke. + * + * @return $this + */ + public function setNormalizer(callable $normalizer) + { + $this->normalizer = $normalizer; + + return $this; + } + + /** + * Gets the normalizer for the response. + * + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * + * @return callable|null + */ + public function getNormalizer() + { + return $this->normalizer; + } + + protected function isAssoc(array $array) + { + return (bool) \count(array_filter(array_keys($array), 'is_string')); + } + + public function isTrimmable(): bool + { + return $this->trimmable; + } + + /** + * @return $this + */ + public function setTrimmable(bool $trimmable): self + { + $this->trimmable = $trimmable; + + return $this; + } +} diff --git a/vendor/symfony/console/README.md b/vendor/symfony/console/README.md new file mode 100644 index 0000000..c4c1299 --- /dev/null +++ b/vendor/symfony/console/README.md @@ -0,0 +1,36 @@ +Console Component +================= + +The Console component eases the creation of beautiful and testable command line +interfaces. + +Sponsor +------- + +The Console component for Symfony 5.4/6.0 is [backed][1] by [Les-Tilleuls.coop][2]. + +Les-Tilleuls.coop is a team of 50+ Symfony experts who can help you design, develop and +fix your projects. We provide a wide range of professional services including development, +consulting, coaching, training and audits. We also are highly skilled in JS, Go and DevOps. +We are a worker cooperative! + +Help Symfony by [sponsoring][3] its development! + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/console.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) + +Credits +------- + +`Resources/bin/hiddeninput.exe` is a third party binary provided within this +component. Find sources and license at https://github.com/Seldaek/hidden-input. + +[1]: https://symfony.com/backers +[2]: https://les-tilleuls.coop +[3]: https://symfony.com/sponsor diff --git a/vendor/symfony/console/Resources/bin/hiddeninput.exe b/vendor/symfony/console/Resources/bin/hiddeninput.exe new file mode 100644 index 0000000..c8cf65e Binary files /dev/null and b/vendor/symfony/console/Resources/bin/hiddeninput.exe differ diff --git a/vendor/symfony/console/Resources/completion.bash b/vendor/symfony/console/Resources/completion.bash new file mode 100644 index 0000000..c5e89c3 --- /dev/null +++ b/vendor/symfony/console/Resources/completion.bash @@ -0,0 +1,81 @@ +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +_sf_{{ COMMAND_NAME }}() { + # Use newline as only separator to allow space in completion values + IFS=$'\n' + local sf_cmd="${COMP_WORDS[0]}" + + # for an alias, get the real script behind it + if [[ $(type -t $sf_cmd) == "alias" ]]; then + sf_cmd=$(alias $sf_cmd | sed -E "s/alias $sf_cmd='(.*)'/\1/") + fi + + if [ ! -f "$sf_cmd" ]; then + return 1 + fi + + local cur prev words cword + _get_comp_words_by_ref -n := cur prev words cword + + local completecmd=("$sf_cmd" "_complete" "-sbash" "-c$cword" "-S{{ VERSION }}") + for w in ${words[@]}; do + w=$(printf -- '%b' "$w") + # remove quotes from typed values + quote="${w:0:1}" + if [ "$quote" == \' ]; then + w="${w%\'}" + w="${w#\'}" + elif [ "$quote" == \" ]; then + w="${w%\"}" + w="${w#\"}" + fi + # empty values are ignored + if [ ! -z "$w" ]; then + completecmd+=("-i$w") + fi + done + + local sfcomplete + if sfcomplete=$(${completecmd[@]} 2>&1); then + local quote suggestions + quote=${cur:0:1} + + # Use single quotes by default if suggestions contains backslash (FQCN) + if [ "$quote" == '' ] && [[ "$sfcomplete" =~ \\ ]]; then + quote=\' + fi + + if [ "$quote" == \' ]; then + # single quotes: no additional escaping (does not accept ' in values) + suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\n' "$quote" "$s" "$quote"; done) + elif [ "$quote" == \" ]; then + # double quotes: double escaping for \ $ ` " + suggestions=$(for s in $sfcomplete; do + s=${s//\\/\\\\} + s=${s//\$/\\\$} + s=${s//\`/\\\`} + s=${s//\"/\\\"} + printf $'%q%q%q\n' "$quote" "$s" "$quote"; + done) + else + # no quotes: double escaping + suggestions=$(for s in $sfcomplete; do printf $'%q\n' $(printf '%q' "$s"); done) + fi + COMPREPLY=($(IFS=$'\n' compgen -W "$suggestions" -- $(printf -- "%q" "$cur"))) + __ltrim_colon_completions "$cur" + else + if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then + >&2 echo + >&2 echo $sfcomplete + fi + + return 1 + fi +} + +complete -F _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }} diff --git a/vendor/symfony/console/SignalRegistry/SignalRegistry.php b/vendor/symfony/console/SignalRegistry/SignalRegistry.php new file mode 100644 index 0000000..ed93dd0 --- /dev/null +++ b/vendor/symfony/console/SignalRegistry/SignalRegistry.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\SignalRegistry; + +final class SignalRegistry +{ + private $signalHandlers = []; + + public function __construct() + { + if (\function_exists('pcntl_async_signals')) { + pcntl_async_signals(true); + } + } + + public function register(int $signal, callable $signalHandler): void + { + if (!isset($this->signalHandlers[$signal])) { + $previousCallback = pcntl_signal_get_handler($signal); + + if (\is_callable($previousCallback)) { + $this->signalHandlers[$signal][] = $previousCallback; + } + } + + $this->signalHandlers[$signal][] = $signalHandler; + + pcntl_signal($signal, [$this, 'handle']); + } + + public static function isSupported(): bool + { + if (!\function_exists('pcntl_signal')) { + return false; + } + + if (\in_array('pcntl_signal', explode(',', ini_get('disable_functions')))) { + return false; + } + + return true; + } + + /** + * @internal + */ + public function handle(int $signal): void + { + $count = \count($this->signalHandlers[$signal]); + + foreach ($this->signalHandlers[$signal] as $i => $signalHandler) { + $hasNext = $i !== $count - 1; + $signalHandler($signal, $hasNext); + } + } +} diff --git a/vendor/symfony/console/SingleCommandApplication.php b/vendor/symfony/console/SingleCommandApplication.php new file mode 100644 index 0000000..e93c182 --- /dev/null +++ b/vendor/symfony/console/SingleCommandApplication.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Grégoire Pineau + */ +class SingleCommandApplication extends Command +{ + private $version = 'UNKNOWN'; + private $autoExit = true; + private $running = false; + + /** + * @return $this + */ + public function setVersion(string $version): self + { + $this->version = $version; + + return $this; + } + + /** + * @final + * + * @return $this + */ + public function setAutoExit(bool $autoExit): self + { + $this->autoExit = $autoExit; + + return $this; + } + + public function run(InputInterface $input = null, OutputInterface $output = null): int + { + if ($this->running) { + return parent::run($input, $output); + } + + // We use the command name as the application name + $application = new Application($this->getName() ?: 'UNKNOWN', $this->version); + $application->setAutoExit($this->autoExit); + // Fix the usage of the command displayed with "--help" + $this->setName($_SERVER['argv'][0]); + $application->add($this); + $application->setDefaultCommand($this->getName(), true); + + $this->running = true; + try { + $ret = $application->run($input, $output); + } finally { + $this->running = false; + } + + return $ret ?? 1; + } +} diff --git a/vendor/symfony/console/Style/OutputStyle.php b/vendor/symfony/console/Style/OutputStyle.php new file mode 100644 index 0000000..67a98ff --- /dev/null +++ b/vendor/symfony/console/Style/OutputStyle.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Style; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Decorates output to add console style guide helpers. + * + * @author Kevin Bond + */ +abstract class OutputStyle implements OutputInterface, StyleInterface +{ + private $output; + + public function __construct(OutputInterface $output) + { + $this->output = $output; + } + + /** + * {@inheritdoc} + */ + public function newLine(int $count = 1) + { + $this->output->write(str_repeat(\PHP_EOL, $count)); + } + + /** + * @return ProgressBar + */ + public function createProgressBar(int $max = 0) + { + return new ProgressBar($this->output, $max); + } + + /** + * {@inheritdoc} + */ + public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) + { + $this->output->write($messages, $newline, $type); + } + + /** + * {@inheritdoc} + */ + public function writeln($messages, int $type = self::OUTPUT_NORMAL) + { + $this->output->writeln($messages, $type); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity(int $level) + { + $this->output->setVerbosity($level); + } + + /** + * {@inheritdoc} + */ + public function getVerbosity() + { + return $this->output->getVerbosity(); + } + + /** + * {@inheritdoc} + */ + public function setDecorated(bool $decorated) + { + $this->output->setDecorated($decorated); + } + + /** + * {@inheritdoc} + */ + public function isDecorated() + { + return $this->output->isDecorated(); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + $this->output->setFormatter($formatter); + } + + /** + * {@inheritdoc} + */ + public function getFormatter() + { + return $this->output->getFormatter(); + } + + /** + * {@inheritdoc} + */ + public function isQuiet() + { + return $this->output->isQuiet(); + } + + /** + * {@inheritdoc} + */ + public function isVerbose() + { + return $this->output->isVerbose(); + } + + /** + * {@inheritdoc} + */ + public function isVeryVerbose() + { + return $this->output->isVeryVerbose(); + } + + /** + * {@inheritdoc} + */ + public function isDebug() + { + return $this->output->isDebug(); + } + + protected function getErrorOutput() + { + if (!$this->output instanceof ConsoleOutputInterface) { + return $this->output; + } + + return $this->output->getErrorOutput(); + } +} diff --git a/vendor/symfony/console/Style/StyleInterface.php b/vendor/symfony/console/Style/StyleInterface.php new file mode 100644 index 0000000..38d23b7 --- /dev/null +++ b/vendor/symfony/console/Style/StyleInterface.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Style; + +/** + * Output style helpers. + * + * @author Kevin Bond + */ +interface StyleInterface +{ + /** + * Formats a command title. + */ + public function title(string $message); + + /** + * Formats a section title. + */ + public function section(string $message); + + /** + * Formats a list. + */ + public function listing(array $elements); + + /** + * Formats informational text. + * + * @param string|array $message + */ + public function text($message); + + /** + * Formats a success result bar. + * + * @param string|array $message + */ + public function success($message); + + /** + * Formats an error result bar. + * + * @param string|array $message + */ + public function error($message); + + /** + * Formats an warning result bar. + * + * @param string|array $message + */ + public function warning($message); + + /** + * Formats a note admonition. + * + * @param string|array $message + */ + public function note($message); + + /** + * Formats a caution admonition. + * + * @param string|array $message + */ + public function caution($message); + + /** + * Formats a table. + */ + public function table(array $headers, array $rows); + + /** + * Asks a question. + * + * @return mixed + */ + public function ask(string $question, string $default = null, callable $validator = null); + + /** + * Asks a question with the user input hidden. + * + * @return mixed + */ + public function askHidden(string $question, callable $validator = null); + + /** + * Asks for confirmation. + * + * @return bool + */ + public function confirm(string $question, bool $default = true); + + /** + * Asks a choice question. + * + * @param string|int|null $default + * + * @return mixed + */ + public function choice(string $question, array $choices, $default = null); + + /** + * Add newline(s). + */ + public function newLine(int $count = 1); + + /** + * Starts the progress output. + */ + public function progressStart(int $max = 0); + + /** + * Advances the progress output X steps. + */ + public function progressAdvance(int $step = 1); + + /** + * Finishes the progress output. + */ + public function progressFinish(); +} diff --git a/vendor/symfony/console/Style/SymfonyStyle.php b/vendor/symfony/console/Style/SymfonyStyle.php new file mode 100644 index 0000000..bcf30d8 --- /dev/null +++ b/vendor/symfony/console/Style/SymfonyStyle.php @@ -0,0 +1,518 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Style; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\SymfonyQuestionHelper; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\TrimmedBufferOutput; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Terminal; + +/** + * Output decorator helpers for the Symfony Style Guide. + * + * @author Kevin Bond + */ +class SymfonyStyle extends OutputStyle +{ + public const MAX_LINE_LENGTH = 120; + + private $input; + private $output; + private $questionHelper; + private $progressBar; + private $lineLength; + private $bufferedOutput; + + public function __construct(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->bufferedOutput = new TrimmedBufferOutput(\DIRECTORY_SEPARATOR === '\\' ? 4 : 2, $output->getVerbosity(), false, clone $output->getFormatter()); + // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. + $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; + $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); + + parent::__construct($this->output = $output); + } + + /** + * Formats a message as a block of text. + * + * @param string|array $messages The message to write in the block + */ + public function block($messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true) + { + $messages = \is_array($messages) ? array_values($messages) : [$messages]; + + $this->autoPrependBlock(); + $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape)); + $this->newLine(); + } + + /** + * {@inheritdoc} + */ + public function title(string $message) + { + $this->autoPrependBlock(); + $this->writeln([ + sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), + sprintf('%s', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))), + ]); + $this->newLine(); + } + + /** + * {@inheritdoc} + */ + public function section(string $message) + { + $this->autoPrependBlock(); + $this->writeln([ + sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), + sprintf('%s', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))), + ]); + $this->newLine(); + } + + /** + * {@inheritdoc} + */ + public function listing(array $elements) + { + $this->autoPrependText(); + $elements = array_map(function ($element) { + return sprintf(' * %s', $element); + }, $elements); + + $this->writeln($elements); + $this->newLine(); + } + + /** + * {@inheritdoc} + */ + public function text($message) + { + $this->autoPrependText(); + + $messages = \is_array($message) ? array_values($message) : [$message]; + foreach ($messages as $message) { + $this->writeln(sprintf(' %s', $message)); + } + } + + /** + * Formats a command comment. + * + * @param string|array $message + */ + public function comment($message) + { + $this->block($message, null, null, ' // ', false, false); + } + + /** + * {@inheritdoc} + */ + public function success($message) + { + $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); + } + + /** + * {@inheritdoc} + */ + public function error($message) + { + $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); + } + + /** + * {@inheritdoc} + */ + public function warning($message) + { + $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true); + } + + /** + * {@inheritdoc} + */ + public function note($message) + { + $this->block($message, 'NOTE', 'fg=yellow', ' ! '); + } + + /** + * Formats an info message. + * + * @param string|array $message + */ + public function info($message) + { + $this->block($message, 'INFO', 'fg=green', ' ', true); + } + + /** + * {@inheritdoc} + */ + public function caution($message) + { + $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); + } + + /** + * {@inheritdoc} + */ + public function table(array $headers, array $rows) + { + $this->createTable() + ->setHeaders($headers) + ->setRows($rows) + ->render() + ; + + $this->newLine(); + } + + /** + * Formats a horizontal table. + */ + public function horizontalTable(array $headers, array $rows) + { + $this->createTable() + ->setHorizontal(true) + ->setHeaders($headers) + ->setRows($rows) + ->render() + ; + + $this->newLine(); + } + + /** + * Formats a list of key/value horizontally. + * + * Each row can be one of: + * * 'A title' + * * ['key' => 'value'] + * * new TableSeparator() + * + * @param string|array|TableSeparator ...$list + */ + public function definitionList(...$list) + { + $headers = []; + $row = []; + foreach ($list as $value) { + if ($value instanceof TableSeparator) { + $headers[] = $value; + $row[] = $value; + continue; + } + if (\is_string($value)) { + $headers[] = new TableCell($value, ['colspan' => 2]); + $row[] = null; + continue; + } + if (!\is_array($value)) { + throw new InvalidArgumentException('Value should be an array, string, or an instance of TableSeparator.'); + } + $headers[] = key($value); + $row[] = current($value); + } + + $this->horizontalTable($headers, [$row]); + } + + /** + * {@inheritdoc} + */ + public function ask(string $question, string $default = null, callable $validator = null) + { + $question = new Question($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($question); + } + + /** + * {@inheritdoc} + */ + public function askHidden(string $question, callable $validator = null) + { + $question = new Question($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($question); + } + + /** + * {@inheritdoc} + */ + public function confirm(string $question, bool $default = true) + { + return $this->askQuestion(new ConfirmationQuestion($question, $default)); + } + + /** + * {@inheritdoc} + */ + public function choice(string $question, array $choices, $default = null) + { + if (null !== $default) { + $values = array_flip($choices); + $default = $values[$default] ?? $default; + } + + return $this->askQuestion(new ChoiceQuestion($question, $choices, $default)); + } + + /** + * {@inheritdoc} + */ + public function progressStart(int $max = 0) + { + $this->progressBar = $this->createProgressBar($max); + $this->progressBar->start(); + } + + /** + * {@inheritdoc} + */ + public function progressAdvance(int $step = 1) + { + $this->getProgressBar()->advance($step); + } + + /** + * {@inheritdoc} + */ + public function progressFinish() + { + $this->getProgressBar()->finish(); + $this->newLine(2); + $this->progressBar = null; + } + + /** + * {@inheritdoc} + */ + public function createProgressBar(int $max = 0) + { + $progressBar = parent::createProgressBar($max); + + if ('\\' !== \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) { + $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591 + $progressBar->setProgressCharacter(''); + $progressBar->setBarCharacter('▓'); // dark shade character \u2593 + } + + return $progressBar; + } + + /** + * @see ProgressBar::iterate() + */ + public function progressIterate(iterable $iterable, int $max = null): iterable + { + yield from $this->createProgressBar()->iterate($iterable, $max); + + $this->newLine(2); + } + + /** + * @return mixed + */ + public function askQuestion(Question $question) + { + if ($this->input->isInteractive()) { + $this->autoPrependBlock(); + } + + if (!$this->questionHelper) { + $this->questionHelper = new SymfonyQuestionHelper(); + } + + $answer = $this->questionHelper->ask($this->input, $this, $question); + + if ($this->input->isInteractive()) { + $this->newLine(); + $this->bufferedOutput->write("\n"); + } + + return $answer; + } + + /** + * {@inheritdoc} + */ + public function writeln($messages, int $type = self::OUTPUT_NORMAL) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + foreach ($messages as $message) { + parent::writeln($message, $type); + $this->writeBuffer($message, true, $type); + } + } + + /** + * {@inheritdoc} + */ + public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + foreach ($messages as $message) { + parent::write($message, $newline, $type); + $this->writeBuffer($message, $newline, $type); + } + } + + /** + * {@inheritdoc} + */ + public function newLine(int $count = 1) + { + parent::newLine($count); + $this->bufferedOutput->write(str_repeat("\n", $count)); + } + + /** + * Returns a new instance which makes use of stderr if available. + * + * @return self + */ + public function getErrorStyle() + { + return new self($this->input, $this->getErrorOutput()); + } + + public function createTable(): Table + { + $output = $this->output instanceof ConsoleOutputInterface ? $this->output->section() : $this->output; + $style = clone Table::getStyleDefinition('symfony-style-guide'); + $style->setCellHeaderFormat('%s'); + + return (new Table($output))->setStyle($style); + } + + private function getProgressBar(): ProgressBar + { + if (!$this->progressBar) { + throw new RuntimeException('The ProgressBar is not started.'); + } + + return $this->progressBar; + } + + private function autoPrependBlock(): void + { + $chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); + + if (!isset($chars[0])) { + $this->newLine(); //empty history, so we should start with a new line. + + return; + } + //Prepend new line for each non LF chars (This means no blank line was output before) + $this->newLine(2 - substr_count($chars, "\n")); + } + + private function autoPrependText(): void + { + $fetched = $this->bufferedOutput->fetch(); + //Prepend new line if last char isn't EOL: + if (!str_ends_with($fetched, "\n")) { + $this->newLine(); + } + } + + private function writeBuffer(string $message, bool $newLine, int $type): void + { + // We need to know if the last chars are PHP_EOL + $this->bufferedOutput->write($message, $newLine, $type); + } + + private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array + { + $indentLength = 0; + $prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix)); + $lines = []; + + if (null !== $type) { + $type = sprintf('[%s] ', $type); + $indentLength = \strlen($type); + $lineIndentation = str_repeat(' ', $indentLength); + } + + // wrap and add newlines for each element + foreach ($messages as $key => $message) { + if ($escape) { + $message = OutputFormatter::escape($message); + } + + $decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message)); + $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength); + $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true)); + foreach ($messageLines as $messageLine) { + $lines[] = $messageLine; + } + + if (\count($messages) > 1 && $key < \count($messages) - 1) { + $lines[] = ''; + } + } + + $firstLineIndex = 0; + if ($padding && $this->isDecorated()) { + $firstLineIndex = 1; + array_unshift($lines, ''); + $lines[] = ''; + } + + foreach ($lines as $i => &$line) { + if (null !== $type) { + $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line; + } + + $line = $prefix.$line; + $line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0)); + + if ($style) { + $line = sprintf('<%s>%s', $style, $line); + } + } + + return $lines; + } +} diff --git a/vendor/symfony/console/Terminal.php b/vendor/symfony/console/Terminal.php new file mode 100644 index 0000000..08c5353 --- /dev/null +++ b/vendor/symfony/console/Terminal.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +class Terminal +{ + private static $width; + private static $height; + private static $stty; + + /** + * Gets the terminal width. + * + * @return int + */ + public function getWidth() + { + $width = getenv('COLUMNS'); + if (false !== $width) { + return (int) trim($width); + } + + if (null === self::$width) { + self::initDimensions(); + } + + return self::$width ?: 80; + } + + /** + * Gets the terminal height. + * + * @return int + */ + public function getHeight() + { + $height = getenv('LINES'); + if (false !== $height) { + return (int) trim($height); + } + + if (null === self::$height) { + self::initDimensions(); + } + + return self::$height ?: 50; + } + + /** + * @internal + */ + public static function hasSttyAvailable(): bool + { + if (null !== self::$stty) { + return self::$stty; + } + + // skip check if exec function is disabled + if (!\function_exists('exec')) { + return false; + } + + exec('stty 2>&1', $output, $exitcode); + + return self::$stty = 0 === $exitcode; + } + + private static function initDimensions() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + if (preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim(getenv('ANSICON')), $matches)) { + // extract [w, H] from "wxh (WxH)" + // or [w, h] from "wxh" + self::$width = (int) $matches[1]; + self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2]; + } elseif (!self::hasVt100Support() && self::hasSttyAvailable()) { + // only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash) + // testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT + self::initDimensionsUsingStty(); + } elseif (null !== $dimensions = self::getConsoleMode()) { + // extract [w, h] from "wxh" + self::$width = (int) $dimensions[0]; + self::$height = (int) $dimensions[1]; + } + } else { + self::initDimensionsUsingStty(); + } + } + + /** + * Returns whether STDOUT has vt100 support (some Windows 10+ configurations). + */ + private static function hasVt100Support(): bool + { + return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(fopen('php://stdout', 'w')); + } + + /** + * Initializes dimensions using the output of an stty columns line. + */ + private static function initDimensionsUsingStty() + { + if ($sttyString = self::getSttyColumns()) { + if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + // extract [w, h] from "rows h; columns w;" + self::$width = (int) $matches[2]; + self::$height = (int) $matches[1]; + } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + // extract [w, h] from "; h rows; w columns" + self::$width = (int) $matches[2]; + self::$height = (int) $matches[1]; + } + } + } + + /** + * Runs and parses mode CON if it's available, suppressing any error output. + * + * @return int[]|null An array composed of the width and the height or null if it could not be parsed + */ + private static function getConsoleMode(): ?array + { + $info = self::readFromProcess('mode CON'); + + if (null === $info || !preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return null; + } + + return [(int) $matches[2], (int) $matches[1]]; + } + + /** + * Runs and parses stty -a if it's available, suppressing any error output. + */ + private static function getSttyColumns(): ?string + { + return self::readFromProcess('stty -a | grep columns'); + } + + private static function readFromProcess(string $command): ?string + { + if (!\function_exists('proc_open')) { + return null; + } + + $descriptorspec = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $process = proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (!\is_resource($process)) { + return null; + } + + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $info; + } +} diff --git a/vendor/symfony/console/Tester/ApplicationTester.php b/vendor/symfony/console/Tester/ApplicationTester.php new file mode 100644 index 0000000..19a95c8 --- /dev/null +++ b/vendor/symfony/console/Tester/ApplicationTester.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\ArrayInput; + +/** + * Eases the testing of console applications. + * + * When testing an application, don't forget to disable the auto exit flag: + * + * $application = new Application(); + * $application->setAutoExit(false); + * + * @author Fabien Potencier + */ +class ApplicationTester +{ + use TesterTrait; + + private $application; + + public function __construct(Application $application) + { + $this->application = $application; + } + + /** + * Executes the application. + * + * Available options: + * + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * * capture_stderr_separately: Make output of stdOut and stdErr separately available + * + * @return int The command exit code + */ + public function run(array $input, array $options = []) + { + $this->input = new ArrayInput($input); + if (isset($options['interactive'])) { + $this->input->setInteractive($options['interactive']); + } + + if ($this->inputs) { + $this->input->setStream(self::createStream($this->inputs)); + } + + $this->initOutput($options); + + return $this->statusCode = $this->application->run($this->input, $this->output); + } +} diff --git a/vendor/symfony/console/Tester/CommandCompletionTester.php b/vendor/symfony/console/Tester/CommandCompletionTester.php new file mode 100644 index 0000000..ade7327 --- /dev/null +++ b/vendor/symfony/console/Tester/CommandCompletionTester.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; + +/** + * Eases the testing of command completion. + * + * @author Jérôme Tamarelle + */ +class CommandCompletionTester +{ + private $command; + + public function __construct(Command $command) + { + $this->command = $command; + } + + /** + * Create completion suggestions from input tokens. + */ + public function complete(array $input): array + { + $currentIndex = \count($input); + if ('' === end($input)) { + array_pop($input); + } + array_unshift($input, $this->command->getName()); + + $completionInput = CompletionInput::fromTokens($input, $currentIndex); + $completionInput->bind($this->command->getDefinition()); + $suggestions = new CompletionSuggestions(); + + $this->command->complete($completionInput, $suggestions); + + $options = []; + foreach ($suggestions->getOptionSuggestions() as $option) { + $options[] = '--'.$option->getName(); + } + + return array_map('strval', array_merge($options, $suggestions->getValueSuggestions())); + } +} diff --git a/vendor/symfony/console/Tester/CommandTester.php b/vendor/symfony/console/Tester/CommandTester.php new file mode 100644 index 0000000..6c15c25 --- /dev/null +++ b/vendor/symfony/console/Tester/CommandTester.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; + +/** + * Eases the testing of console commands. + * + * @author Fabien Potencier + * @author Robin Chalas + */ +class CommandTester +{ + use TesterTrait; + + private $command; + + public function __construct(Command $command) + { + $this->command = $command; + } + + /** + * Executes the command. + * + * Available execution options: + * + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * * capture_stderr_separately: Make output of stdOut and stdErr separately available + * + * @param array $input An array of command arguments and options + * @param array $options An array of execution options + * + * @return int The command exit code + */ + public function execute(array $input, array $options = []) + { + // set the command name automatically if the application requires + // this argument and no command name was passed + if (!isset($input['command']) + && (null !== $application = $this->command->getApplication()) + && $application->getDefinition()->hasArgument('command') + ) { + $input = array_merge(['command' => $this->command->getName()], $input); + } + + $this->input = new ArrayInput($input); + // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. + $this->input->setStream(self::createStream($this->inputs)); + + if (isset($options['interactive'])) { + $this->input->setInteractive($options['interactive']); + } + + if (!isset($options['decorated'])) { + $options['decorated'] = false; + } + + $this->initOutput($options); + + return $this->statusCode = $this->command->run($this->input, $this->output); + } +} diff --git a/vendor/symfony/console/Tester/Constraint/CommandIsSuccessful.php b/vendor/symfony/console/Tester/Constraint/CommandIsSuccessful.php new file mode 100644 index 0000000..a473242 --- /dev/null +++ b/vendor/symfony/console/Tester/Constraint/CommandIsSuccessful.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Console\Command\Command; + +final class CommandIsSuccessful extends Constraint +{ + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'is successful'; + } + + /** + * {@inheritdoc} + */ + protected function matches($other): bool + { + return Command::SUCCESS === $other; + } + + /** + * {@inheritdoc} + */ + protected function failureDescription($other): string + { + return 'the command '.$this->toString(); + } + + /** + * {@inheritdoc} + */ + protected function additionalFailureDescription($other): string + { + $mapping = [ + Command::FAILURE => 'Command failed.', + Command::INVALID => 'Command was invalid.', + ]; + + return $mapping[$other] ?? sprintf('Command returned exit status %d.', $other); + } +} diff --git a/vendor/symfony/console/Tester/TesterTrait.php b/vendor/symfony/console/Tester/TesterTrait.php new file mode 100644 index 0000000..40bc581 --- /dev/null +++ b/vendor/symfony/console/Tester/TesterTrait.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use PHPUnit\Framework\Assert; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; +use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful; + +/** + * @author Amrouche Hamza + */ +trait TesterTrait +{ + /** @var StreamOutput */ + private $output; + private $inputs = []; + private $captureStreamsIndependently = false; + /** @var InputInterface */ + private $input; + /** @var int */ + private $statusCode; + + /** + * Gets the display returned by the last execution of the command or application. + * + * @throws \RuntimeException If it's called before the execute method + * + * @return string + */ + public function getDisplay(bool $normalize = false) + { + if (null === $this->output) { + throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?'); + } + + rewind($this->output->getStream()); + + $display = stream_get_contents($this->output->getStream()); + + if ($normalize) { + $display = str_replace(\PHP_EOL, "\n", $display); + } + + return $display; + } + + /** + * Gets the output written to STDERR by the application. + * + * @param bool $normalize Whether to normalize end of lines to \n or not + * + * @return string + */ + public function getErrorOutput(bool $normalize = false) + { + if (!$this->captureStreamsIndependently) { + throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.'); + } + + rewind($this->output->getErrorOutput()->getStream()); + + $display = stream_get_contents($this->output->getErrorOutput()->getStream()); + + if ($normalize) { + $display = str_replace(\PHP_EOL, "\n", $display); + } + + return $display; + } + + /** + * Gets the input instance used by the last execution of the command or application. + * + * @return InputInterface + */ + public function getInput() + { + return $this->input; + } + + /** + * Gets the output instance used by the last execution of the command or application. + * + * @return OutputInterface + */ + public function getOutput() + { + return $this->output; + } + + /** + * Gets the status code returned by the last execution of the command or application. + * + * @throws \RuntimeException If it's called before the execute method + * + * @return int + */ + public function getStatusCode() + { + if (null === $this->statusCode) { + throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?'); + } + + return $this->statusCode; + } + + public function assertCommandIsSuccessful(string $message = ''): void + { + Assert::assertThat($this->statusCode, new CommandIsSuccessful(), $message); + } + + /** + * Sets the user inputs. + * + * @param array $inputs An array of strings representing each input + * passed to the command input stream + * + * @return $this + */ + public function setInputs(array $inputs) + { + $this->inputs = $inputs; + + return $this; + } + + /** + * Initializes the output property. + * + * Available options: + * + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * * capture_stderr_separately: Make output of stdOut and stdErr separately available + */ + private function initOutput(array $options) + { + $this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately']; + if (!$this->captureStreamsIndependently) { + $this->output = new StreamOutput(fopen('php://memory', 'w', false)); + if (isset($options['decorated'])) { + $this->output->setDecorated($options['decorated']); + } + if (isset($options['verbosity'])) { + $this->output->setVerbosity($options['verbosity']); + } + } else { + $this->output = new ConsoleOutput( + $options['verbosity'] ?? ConsoleOutput::VERBOSITY_NORMAL, + $options['decorated'] ?? null + ); + + $errorOutput = new StreamOutput(fopen('php://memory', 'w', false)); + $errorOutput->setFormatter($this->output->getFormatter()); + $errorOutput->setVerbosity($this->output->getVerbosity()); + $errorOutput->setDecorated($this->output->isDecorated()); + + $reflectedOutput = new \ReflectionObject($this->output); + $strErrProperty = $reflectedOutput->getProperty('stderr'); + $strErrProperty->setAccessible(true); + $strErrProperty->setValue($this->output, $errorOutput); + + $reflectedParent = $reflectedOutput->getParentClass(); + $streamProperty = $reflectedParent->getProperty('stream'); + $streamProperty->setAccessible(true); + $streamProperty->setValue($this->output, fopen('php://memory', 'w', false)); + } + } + + /** + * @return resource + */ + private static function createStream(array $inputs) + { + $stream = fopen('php://memory', 'r+', false); + + foreach ($inputs as $input) { + fwrite($stream, $input.\PHP_EOL); + } + + rewind($stream); + + return $stream; + } +} diff --git a/vendor/symfony/console/composer.json b/vendor/symfony/console/composer.json new file mode 100644 index 0000000..9a56506 --- /dev/null +++ b/vendor/symfony/console/composer.json @@ -0,0 +1,60 @@ +{ + "name": "symfony/console", + "type": "library", + "description": "Eases the creation of beautiful and testable command line interfaces", + "keywords": ["console", "cli", "command line", "terminal"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "require-dev": { + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0", + "psr/log": "^1|^2" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "suggest": { + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "", + "psr/log": "For using the console logger" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Console\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/symfony/deprecation-contracts/.gitignore b/vendor/symfony/deprecation-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/deprecation-contracts/CHANGELOG.md b/vendor/symfony/deprecation-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/vendor/symfony/deprecation-contracts/LICENSE b/vendor/symfony/deprecation-contracts/LICENSE new file mode 100644 index 0000000..ad85e17 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/deprecation-contracts/README.md b/vendor/symfony/deprecation-contracts/README.md new file mode 100644 index 0000000..4957933 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/README.md @@ -0,0 +1,26 @@ +Symfony Deprecation Contracts +============================= + +A generic function and convention to trigger deprecation notices. + +This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. + +By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, +the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. + +The function requires at least 3 arguments: + - the name of the Composer package that is triggering the deprecation + - the version of the package that introduced the deprecation + - the message of the deprecation + - more arguments can be provided: they will be inserted in the message using `printf()` formatting + +Example: +```php +trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); +``` + +This will generate the following message: +`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` + +While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty +`function trigger_deprecation() {}` in your application. diff --git a/vendor/symfony/deprecation-contracts/composer.json b/vendor/symfony/deprecation-contracts/composer.json new file mode 100644 index 0000000..1c1b4ba --- /dev/null +++ b/vendor/symfony/deprecation-contracts/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/deprecation-contracts", + "type": "library", + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.0.2" + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/vendor/symfony/deprecation-contracts/function.php b/vendor/symfony/deprecation-contracts/function.php new file mode 100644 index 0000000..2d56512 --- /dev/null +++ b/vendor/symfony/deprecation-contracts/function.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!function_exists('trigger_deprecation')) { + /** + * Triggers a silenced deprecation notice. + * + * @param string $package The name of the Composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The message of the deprecation + * @param mixed ...$args Values to insert in the message using printf() formatting + * + * @author Nicolas Grekas + */ + function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void + { + @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); + } +} diff --git a/vendor/symfony/filesystem/CHANGELOG.md b/vendor/symfony/filesystem/CHANGELOG.md new file mode 100644 index 0000000..fcb7170 --- /dev/null +++ b/vendor/symfony/filesystem/CHANGELOG.md @@ -0,0 +1,82 @@ +CHANGELOG +========= + +5.4 +--- + + * Add `Path` class + * Add `$lock` argument to `Filesystem::appendToFile()` + +5.0.0 +----- + + * `Filesystem::dumpFile()` and `appendToFile()` don't accept arrays anymore + +4.4.0 +----- + + * support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated and will be removed in 5.0 + * `tempnam()` now accepts a third argument `$suffix`. + +4.3.0 +----- + + * support for passing arrays to `Filesystem::dumpFile()` is deprecated and will be removed in 5.0 + * support for passing arrays to `Filesystem::appendToFile()` is deprecated and will be removed in 5.0 + +4.0.0 +----- + + * removed `LockHandler` + * Support for passing relative paths to `Filesystem::makePathRelative()` has been removed. + +3.4.0 +----- + + * support for passing relative paths to `Filesystem::makePathRelative()` is deprecated and will be removed in 4.0 + +3.3.0 +----- + + * added `appendToFile()` to append contents to existing files + +3.2.0 +----- + + * added `readlink()` as a platform independent method to read links + +3.0.0 +----- + + * removed `$mode` argument from `Filesystem::dumpFile()` + +2.8.0 +----- + + * added tempnam() a stream aware version of PHP's native tempnam() + +2.6.0 +----- + + * added LockHandler + +2.3.12 +------ + + * deprecated dumpFile() file mode argument. + +2.3.0 +----- + + * added the dumpFile() method to atomically write files + +2.2.0 +----- + + * added a delete option for the mirror() method + +2.1.0 +----- + + * 24eb396 : BC Break : mkdir() function now throws exception in case of failure instead of returning Boolean value + * created the component diff --git a/vendor/symfony/filesystem/Exception/ExceptionInterface.php b/vendor/symfony/filesystem/Exception/ExceptionInterface.php new file mode 100644 index 0000000..fc438d9 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Romain Neutron + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/vendor/symfony/filesystem/Exception/FileNotFoundException.php b/vendor/symfony/filesystem/Exception/FileNotFoundException.php new file mode 100644 index 0000000..48b6408 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/FileNotFoundException.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception class thrown when a file couldn't be found. + * + * @author Fabien Potencier + * @author Christian Gärtner + */ +class FileNotFoundException extends IOException +{ + public function __construct(string $message = null, int $code = 0, \Throwable $previous = null, string $path = null) + { + if (null === $message) { + if (null === $path) { + $message = 'File could not be found.'; + } else { + $message = sprintf('File "%s" could not be found.', $path); + } + } + + parent::__construct($message, $code, $previous, $path); + } +} diff --git a/vendor/symfony/filesystem/Exception/IOException.php b/vendor/symfony/filesystem/Exception/IOException.php new file mode 100644 index 0000000..bcca860 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/IOException.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception class thrown when a filesystem operation failure happens. + * + * @author Romain Neutron + * @author Christian Gärtner + * @author Fabien Potencier + */ +class IOException extends \RuntimeException implements IOExceptionInterface +{ + private ?string $path; + + public function __construct(string $message, int $code = 0, \Throwable $previous = null, string $path = null) + { + $this->path = $path; + + parent::__construct($message, $code, $previous); + } + + /** + * {@inheritdoc} + */ + public function getPath(): ?string + { + return $this->path; + } +} diff --git a/vendor/symfony/filesystem/Exception/IOExceptionInterface.php b/vendor/symfony/filesystem/Exception/IOExceptionInterface.php new file mode 100644 index 0000000..90c71db --- /dev/null +++ b/vendor/symfony/filesystem/Exception/IOExceptionInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * IOException interface for file and input/output stream related exceptions thrown by the component. + * + * @author Christian Gärtner + */ +interface IOExceptionInterface extends ExceptionInterface +{ + /** + * Returns the associated path for the exception. + */ + public function getPath(): ?string; +} diff --git a/vendor/symfony/filesystem/Exception/InvalidArgumentException.php b/vendor/symfony/filesystem/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..abadc20 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * @author Christian Flothmann + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/filesystem/Exception/RuntimeException.php b/vendor/symfony/filesystem/Exception/RuntimeException.php new file mode 100644 index 0000000..a7512dc --- /dev/null +++ b/vendor/symfony/filesystem/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * @author Théo Fidry + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/filesystem/Filesystem.php b/vendor/symfony/filesystem/Filesystem.php new file mode 100644 index 0000000..8df9641 --- /dev/null +++ b/vendor/symfony/filesystem/Filesystem.php @@ -0,0 +1,724 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem; + +use Symfony\Component\Filesystem\Exception\FileNotFoundException; +use Symfony\Component\Filesystem\Exception\InvalidArgumentException; +use Symfony\Component\Filesystem\Exception\IOException; + +/** + * Provides basic utility to manipulate the file system. + * + * @author Fabien Potencier + */ +class Filesystem +{ + private static $lastError; + + /** + * Copies a file. + * + * If the target file is older than the origin file, it's always overwritten. + * If the target file is newer, it is overwritten only when the + * $overwriteNewerFiles option is set to true. + * + * @throws FileNotFoundException When originFile doesn't exist + * @throws IOException When copy fails + */ + public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false) + { + $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://'); + if ($originIsLocal && !is_file($originFile)) { + throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); + } + + $this->mkdir(\dirname($targetFile)); + + $doCopy = true; + if (!$overwriteNewerFiles && null === parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) { + $doCopy = filemtime($originFile) > filemtime($targetFile); + } + + if ($doCopy) { + // https://bugs.php.net/64634 + if (!$source = self::box('fopen', $originFile, 'r')) { + throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile); + } + + // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default + if (!$target = self::box('fopen', $targetFile, 'w', false, stream_context_create(['ftp' => ['overwrite' => true]]))) { + throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile); + } + + $bytesCopied = stream_copy_to_stream($source, $target); + fclose($source); + fclose($target); + unset($source, $target); + + if (!is_file($targetFile)) { + throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); + } + + if ($originIsLocal) { + // Like `cp`, preserve executable permission bits + self::box('chmod', $targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111)); + + if ($bytesCopied !== $bytesOrigin = filesize($originFile)) { + throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile); + } + } + } + } + + /** + * Creates a directory recursively. + * + * @throws IOException On any directory creation failure + */ + public function mkdir(string|iterable $dirs, int $mode = 0777) + { + foreach ($this->toIterable($dirs) as $dir) { + if (is_dir($dir)) { + continue; + } + + if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) { + throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir); + } + } + } + + /** + * Checks the existence of files or directories. + */ + public function exists(string|iterable $files): bool + { + $maxPathLength = \PHP_MAXPATHLEN - 2; + + foreach ($this->toIterable($files) as $file) { + if (\strlen($file) > $maxPathLength) { + throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file); + } + + if (!file_exists($file)) { + return false; + } + } + + return true; + } + + /** + * Sets access and modification time of file. + * + * @param int|null $time The touch time as a Unix timestamp, if not supplied the current system time is used + * @param int|null $atime The access time as a Unix timestamp, if not supplied the current system time is used + * + * @throws IOException When touch fails + */ + public function touch(string|iterable $files, int $time = null, int $atime = null) + { + foreach ($this->toIterable($files) as $file) { + if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) { + throw new IOException(sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file); + } + } + } + + /** + * Removes files or directories. + * + * @throws IOException When removal fails + */ + public function remove(string|iterable $files) + { + if ($files instanceof \Traversable) { + $files = iterator_to_array($files, false); + } elseif (!\is_array($files)) { + $files = [$files]; + } + + self::doRemove($files, false); + } + + private static function doRemove(array $files, bool $isRecursive): void + { + $files = array_reverse($files); + foreach ($files as $file) { + if (is_link($file)) { + // See https://bugs.php.net/52176 + if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) { + throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError); + } + } elseif (is_dir($file)) { + if (!$isRecursive) { + $tmpName = \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-.')); + + if (file_exists($tmpName)) { + try { + self::doRemove([$tmpName], true); + } catch (IOException $e) { + } + } + + if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) { + $origFile = $file; + $file = $tmpName; + } else { + $origFile = null; + } + } + + $files = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS); + self::doRemove(iterator_to_array($files, true), true); + + if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) { + $lastError = self::$lastError; + + if (null !== $origFile && self::box('rename', $file, $origFile)) { + $file = $origFile; + } + + throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError); + } + } elseif (!self::box('unlink', $file) && (str_contains(self::$lastError, 'Permission denied') || file_exists($file))) { + throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError); + } + } + } + + /** + * Change mode for an array of files or directories. + * + * @param int $mode The new mode (octal) + * @param int $umask The mode mask (octal) + * @param bool $recursive Whether change the mod recursively or not + * + * @throws IOException When the change fails + */ + public function chmod(string|iterable $files, int $mode, int $umask = 0000, bool $recursive = false) + { + foreach ($this->toIterable($files) as $file) { + if (\is_int($mode) && !self::box('chmod', $file, $mode & ~$umask)) { + throw new IOException(sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file); + } + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); + } + } + } + + /** + * Change the owner of an array of files or directories. + * + * @param string|int $user A user name or number + * @param bool $recursive Whether change the owner recursively or not + * + * @throws IOException When the change fails + */ + public function chown(string|iterable $files, string|int $user, bool $recursive = false) + { + foreach ($this->toIterable($files) as $file) { + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chown(new \FilesystemIterator($file), $user, true); + } + if (is_link($file) && \function_exists('lchown')) { + if (!self::box('lchown', $file, $user)) { + throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); + } + } else { + if (!self::box('chown', $file, $user)) { + throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); + } + } + } + } + + /** + * Change the group of an array of files or directories. + * + * @param string|int $group A group name or number + * @param bool $recursive Whether change the group recursively or not + * + * @throws IOException When the change fails + */ + public function chgrp(string|iterable $files, string|int $group, bool $recursive = false) + { + foreach ($this->toIterable($files) as $file) { + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chgrp(new \FilesystemIterator($file), $group, true); + } + if (is_link($file) && \function_exists('lchgrp')) { + if (!self::box('lchgrp', $file, $group)) { + throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); + } + } else { + if (!self::box('chgrp', $file, $group)) { + throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); + } + } + } + } + + /** + * Renames a file or a directory. + * + * @throws IOException When target file or directory already exists + * @throws IOException When origin cannot be renamed + */ + public function rename(string $origin, string $target, bool $overwrite = false) + { + // we check that target does not exist + if (!$overwrite && $this->isReadable($target)) { + throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); + } + + if (!self::box('rename', $origin, $target)) { + if (is_dir($origin)) { + // See https://bugs.php.net/54097 & https://php.net/rename#113943 + $this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]); + $this->remove($origin); + + return; + } + throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target); + } + } + + /** + * Tells whether a file exists and is readable. + * + * @throws IOException When windows path is longer than 258 characters + */ + private function isReadable(string $filename): bool + { + $maxPathLength = \PHP_MAXPATHLEN - 2; + + if (\strlen($filename) > $maxPathLength) { + throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename); + } + + return is_readable($filename); + } + + /** + * Creates a symbolic link or copy a directory. + * + * @throws IOException When symlink fails + */ + public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false) + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $originDir = strtr($originDir, '/', '\\'); + $targetDir = strtr($targetDir, '/', '\\'); + + if ($copyOnWindows) { + $this->mirror($originDir, $targetDir); + + return; + } + } + + $this->mkdir(\dirname($targetDir)); + + if (is_link($targetDir)) { + if (readlink($targetDir) === $originDir) { + return; + } + $this->remove($targetDir); + } + + if (!self::box('symlink', $originDir, $targetDir)) { + $this->linkException($originDir, $targetDir, 'symbolic'); + } + } + + /** + * Creates a hard link, or several hard links to a file. + * + * @param string|string[] $targetFiles The target file(s) + * + * @throws FileNotFoundException When original file is missing or not a file + * @throws IOException When link fails, including if link already exists + */ + public function hardlink(string $originFile, string|iterable $targetFiles) + { + if (!$this->exists($originFile)) { + throw new FileNotFoundException(null, 0, null, $originFile); + } + + if (!is_file($originFile)) { + throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile)); + } + + foreach ($this->toIterable($targetFiles) as $targetFile) { + if (is_file($targetFile)) { + if (fileinode($originFile) === fileinode($targetFile)) { + continue; + } + $this->remove($targetFile); + } + + if (!self::box('link', $originFile, $targetFile)) { + $this->linkException($originFile, $targetFile, 'hard'); + } + } + } + + /** + * @param string $linkType Name of the link type, typically 'symbolic' or 'hard' + */ + private function linkException(string $origin, string $target, string $linkType) + { + if (self::$lastError) { + if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError, 'error code(1314)')) { + throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target); + } + } + throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target); + } + + /** + * Resolves links in paths. + * + * With $canonicalize = false (default) + * - if $path does not exist or is not a link, returns null + * - if $path is a link, returns the next direct target of the link without considering the existence of the target + * + * With $canonicalize = true + * - if $path does not exist, returns null + * - if $path exists, returns its absolute fully resolved final version + */ + public function readlink(string $path, bool $canonicalize = false): ?string + { + if (!$canonicalize && !is_link($path)) { + return null; + } + + if ($canonicalize) { + if (!$this->exists($path)) { + return null; + } + + return realpath($path); + } + + return readlink($path); + } + + /** + * Given an existing path, convert it to a path relative to a given starting path. + */ + public function makePathRelative(string $endPath, string $startPath): string + { + if (!$this->isAbsolutePath($startPath)) { + throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.', $startPath)); + } + + if (!$this->isAbsolutePath($endPath)) { + throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.', $endPath)); + } + + // Normalize separators on Windows + if ('\\' === \DIRECTORY_SEPARATOR) { + $endPath = str_replace('\\', '/', $endPath); + $startPath = str_replace('\\', '/', $startPath); + } + + $splitDriveLetter = function ($path) { + return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0])) + ? [substr($path, 2), strtoupper($path[0])] + : [$path, null]; + }; + + $splitPath = function ($path) { + $result = []; + + foreach (explode('/', trim($path, '/')) as $segment) { + if ('..' === $segment) { + array_pop($result); + } elseif ('.' !== $segment && '' !== $segment) { + $result[] = $segment; + } + } + + return $result; + }; + + [$endPath, $endDriveLetter] = $splitDriveLetter($endPath); + [$startPath, $startDriveLetter] = $splitDriveLetter($startPath); + + $startPathArr = $splitPath($startPath); + $endPathArr = $splitPath($endPath); + + if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) { + // End path is on another drive, so no relative path exists + return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : ''); + } + + // Find for which directory the common path stops + $index = 0; + while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) { + ++$index; + } + + // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels) + if (1 === \count($startPathArr) && '' === $startPathArr[0]) { + $depth = 0; + } else { + $depth = \count($startPathArr) - $index; + } + + // Repeated "../" for each level need to reach the common path + $traverser = str_repeat('../', $depth); + + $endPathRemainder = implode('/', \array_slice($endPathArr, $index)); + + // Construct $endPath from traversing to the common path, then to the remaining $endPath + $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : ''); + + return '' === $relativePath ? './' : $relativePath; + } + + /** + * Mirrors a directory to another. + * + * Copies files and directories from the origin directory into the target directory. By default: + * + * - existing files in the target directory will be overwritten, except if they are newer (see the `override` option) + * - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option) + * + * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created + * @param array $options An array of boolean options + * Valid options are: + * - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false) + * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false) + * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false) + * + * @throws IOException When file type is unknown + */ + public function mirror(string $originDir, string $targetDir, \Traversable $iterator = null, array $options = []) + { + $targetDir = rtrim($targetDir, '/\\'); + $originDir = rtrim($originDir, '/\\'); + $originDirLen = \strlen($originDir); + + if (!$this->exists($originDir)) { + throw new IOException(sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir); + } + + // Iterate in destination folder to remove obsolete entries + if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) { + $deleteIterator = $iterator; + if (null === $deleteIterator) { + $flags = \FilesystemIterator::SKIP_DOTS; + $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST); + } + $targetDirLen = \strlen($targetDir); + foreach ($deleteIterator as $file) { + $origin = $originDir.substr($file->getPathname(), $targetDirLen); + if (!$this->exists($origin)) { + $this->remove($file); + } + } + } + + $copyOnWindows = $options['copy_on_windows'] ?? false; + + if (null === $iterator) { + $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST); + } + + $this->mkdir($targetDir); + $filesCreatedWhileMirroring = []; + + foreach ($iterator as $file) { + if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) { + continue; + } + + $target = $targetDir.substr($file->getPathname(), $originDirLen); + $filesCreatedWhileMirroring[$target] = true; + + if (!$copyOnWindows && is_link($file)) { + $this->symlink($file->getLinkTarget(), $target); + } elseif (is_dir($file)) { + $this->mkdir($target); + } elseif (is_file($file)) { + $this->copy($file, $target, $options['override'] ?? false); + } else { + throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); + } + } + } + + /** + * Returns whether the file path is an absolute path. + */ + public function isAbsolutePath(string $file): bool + { + return '' !== $file && (strspn($file, '/\\', 0, 1) + || (\strlen($file) > 3 && ctype_alpha($file[0]) + && ':' === $file[1] + && strspn($file, '/\\', 2, 1) + ) + || null !== parse_url($file, \PHP_URL_SCHEME) + ); + } + + /** + * Creates a temporary file with support for custom stream wrappers. + * + * @param string $prefix The prefix of the generated temporary filename + * Note: Windows uses only the first three characters of prefix + * @param string $suffix The suffix of the generated temporary filename + * + * @return string The new temporary filename (with path), or throw an exception on failure + */ + public function tempnam(string $dir, string $prefix, string $suffix = ''): string + { + [$scheme, $hierarchy] = $this->getSchemeAndHierarchy($dir); + + // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem + if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) { + // If tempnam failed or no scheme return the filename otherwise prepend the scheme + if ($tmpFile = self::box('tempnam', $hierarchy, $prefix)) { + if (null !== $scheme && 'gs' !== $scheme) { + return $scheme.'://'.$tmpFile; + } + + return $tmpFile; + } + + throw new IOException('A temporary file could not be created: '.self::$lastError); + } + + // Loop until we create a valid temp file or have reached 10 attempts + for ($i = 0; $i < 10; ++$i) { + // Create a unique filename + $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix; + + // Use fopen instead of file_exists as some streams do not support stat + // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability + if (!$handle = self::box('fopen', $tmpFile, 'x+')) { + continue; + } + + // Close the file if it was successfully opened + self::box('fclose', $handle); + + return $tmpFile; + } + + throw new IOException('A temporary file could not be created: '.self::$lastError); + } + + /** + * Atomically dumps content into a file. + * + * @param string|resource $content The data to write into the file + * + * @throws IOException if the file cannot be written to + */ + public function dumpFile(string $filename, $content) + { + if (\is_array($content)) { + throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); + } + + $dir = \dirname($filename); + + if (!is_dir($dir)) { + $this->mkdir($dir); + } + + // Will create a temp file with 0600 access rights + // when the filesystem supports chmod. + $tmpFile = $this->tempnam($dir, basename($filename)); + + try { + if (false === self::box('file_put_contents', $tmpFile, $content)) { + throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); + } + + self::box('chmod', $tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask()); + + $this->rename($tmpFile, $filename, true); + } finally { + if (file_exists($tmpFile)) { + self::box('unlink', $tmpFile); + } + } + } + + /** + * Appends content to an existing file. + * + * @param string|resource $content The content to append + * @param bool $lock Whether the file should be locked when writing to it + * + * @throws IOException If the file is not writable + */ + public function appendToFile(string $filename, $content/*, bool $lock = false*/) + { + if (\is_array($content)) { + throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); + } + + $dir = \dirname($filename); + + if (!is_dir($dir)) { + $this->mkdir($dir); + } + + $lock = \func_num_args() > 2 && func_get_arg(2); + + if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND | ($lock ? \LOCK_EX : 0))) { + throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); + } + } + + private function toIterable(string|iterable $files): iterable + { + return is_iterable($files) ? $files : [$files]; + } + + /** + * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]). + */ + private function getSchemeAndHierarchy(string $filename): array + { + $components = explode('://', $filename, 2); + + return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]]; + } + + private static function box(callable $func, mixed ...$args): mixed + { + self::$lastError = null; + set_error_handler(__CLASS__.'::handleError'); + try { + return $func(...$args); + } finally { + restore_error_handler(); + } + } + + /** + * @internal + */ + public static function handleError(int $type, string $msg) + { + self::$lastError = $msg; + } +} diff --git a/vendor/symfony/filesystem/LICENSE b/vendor/symfony/filesystem/LICENSE new file mode 100644 index 0000000..9ff2d0d --- /dev/null +++ b/vendor/symfony/filesystem/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/filesystem/Path.php b/vendor/symfony/filesystem/Path.php new file mode 100644 index 0000000..187632b --- /dev/null +++ b/vendor/symfony/filesystem/Path.php @@ -0,0 +1,819 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem; + +use Symfony\Component\Filesystem\Exception\InvalidArgumentException; +use Symfony\Component\Filesystem\Exception\RuntimeException; + +/** + * Contains utility methods for handling path strings. + * + * The methods in this class are able to deal with both UNIX and Windows paths + * with both forward and backward slashes. All methods return normalized parts + * containing only forward slashes and no excess "." and ".." segments. + * + * @author Bernhard Schussek + * @author Thomas Schulz + * @author Théo Fidry + */ +final class Path +{ + /** + * The number of buffer entries that triggers a cleanup operation. + */ + private const CLEANUP_THRESHOLD = 1250; + + /** + * The buffer size after the cleanup operation. + */ + private const CLEANUP_SIZE = 1000; + + /** + * Buffers input/output of {@link canonicalize()}. + * + * @var array + */ + private static $buffer = []; + + /** + * @var int + */ + private static $bufferSize = 0; + + /** + * Canonicalizes the given path. + * + * During normalization, all slashes are replaced by forward slashes ("/"). + * Furthermore, all "." and ".." segments are removed as far as possible. + * ".." segments at the beginning of relative paths are not removed. + * + * ```php + * echo Path::canonicalize("\symfony\puli\..\css\style.css"); + * // => /symfony/css/style.css + * + * echo Path::canonicalize("../css/./style.css"); + * // => ../css/style.css + * ``` + * + * This method is able to deal with both UNIX and Windows paths. + */ + public static function canonicalize(string $path): string + { + if ('' === $path) { + return ''; + } + + // This method is called by many other methods in this class. Buffer + // the canonicalized paths to make up for the severe performance + // decrease. + if (isset(self::$buffer[$path])) { + return self::$buffer[$path]; + } + + // Replace "~" with user's home directory. + if ('~' === $path[0]) { + $path = self::getHomeDirectory().mb_substr($path, 1); + } + + $path = self::normalize($path); + + [$root, $pathWithoutRoot] = self::split($path); + + $canonicalParts = self::findCanonicalParts($root, $pathWithoutRoot); + + // Add the root directory again + self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts); + ++self::$bufferSize; + + // Clean up regularly to prevent memory leaks + if (self::$bufferSize > self::CLEANUP_THRESHOLD) { + self::$buffer = \array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true); + self::$bufferSize = self::CLEANUP_SIZE; + } + + return $canonicalPath; + } + + /** + * Normalizes the given path. + * + * During normalization, all slashes are replaced by forward slashes ("/"). + * Contrary to {@link canonicalize()}, this method does not remove invalid + * or dot path segments. Consequently, it is much more efficient and should + * be used whenever the given path is known to be a valid, absolute system + * path. + * + * This method is able to deal with both UNIX and Windows paths. + */ + public static function normalize(string $path): string + { + return str_replace('\\', '/', $path); + } + + /** + * Returns the directory part of the path. + * + * This method is similar to PHP's dirname(), but handles various cases + * where dirname() returns a weird result: + * + * - dirname() does not accept backslashes on UNIX + * - dirname("C:/symfony") returns "C:", not "C:/" + * - dirname("C:/") returns ".", not "C:/" + * - dirname("C:") returns ".", not "C:/" + * - dirname("symfony") returns ".", not "" + * - dirname() does not canonicalize the result + * + * This method fixes these shortcomings and behaves like dirname() + * otherwise. + * + * The result is a canonical path. + * + * @return string The canonical directory part. Returns the root directory + * if the root directory is passed. Returns an empty string + * if a relative path is passed that contains no slashes. + * Returns an empty string if an empty string is passed. + */ + public static function getDirectory(string $path): string + { + if ('' === $path) { + return ''; + } + + $path = self::canonicalize($path); + + // Maintain scheme + if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) { + $scheme = mb_substr($path, 0, $schemeSeparatorPosition + 3); + $path = mb_substr($path, $schemeSeparatorPosition + 3); + } else { + $scheme = ''; + } + + if (false === ($dirSeparatorPosition = strrpos($path, '/'))) { + return ''; + } + + // Directory equals root directory "/" + if (0 === $dirSeparatorPosition) { + return $scheme.'/'; + } + + // Directory equals Windows root "C:/" + if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) { + return $scheme.mb_substr($path, 0, 3); + } + + return $scheme.mb_substr($path, 0, $dirSeparatorPosition); + } + + /** + * Returns canonical path of the user's home directory. + * + * Supported operating systems: + * + * - UNIX + * - Windows8 and upper + * + * If your operation system or environment isn't supported, an exception is thrown. + * + * The result is a canonical path. + * + * @throws RuntimeException If your operation system or environment isn't supported + */ + public static function getHomeDirectory(): string + { + // For UNIX support + if (getenv('HOME')) { + return self::canonicalize(getenv('HOME')); + } + + // For >= Windows8 support + if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) { + return self::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH')); + } + + throw new RuntimeException("Cannot find the home directory path: Your environment or operation system isn't supported."); + } + + /** + * Returns the root directory of a path. + * + * The result is a canonical path. + * + * @return string The canonical root directory. Returns an empty string if + * the given path is relative or empty. + */ + public static function getRoot(string $path): string + { + if ('' === $path) { + return ''; + } + + // Maintain scheme + if (false !== ($schemeSeparatorPosition = strpos($path, '://'))) { + $scheme = substr($path, 0, $schemeSeparatorPosition + 3); + $path = substr($path, $schemeSeparatorPosition + 3); + } else { + $scheme = ''; + } + + $firstCharacter = $path[0]; + + // UNIX root "/" or "\" (Windows style) + if ('/' === $firstCharacter || '\\' === $firstCharacter) { + return $scheme.'/'; + } + + $length = mb_strlen($path); + + // Windows root + if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) { + // Special case: "C:" + if (2 === $length) { + return $scheme.$path.'/'; + } + + // Normal case: "C:/ or "C:\" + if ('/' === $path[2] || '\\' === $path[2]) { + return $scheme.$firstCharacter.$path[1].'/'; + } + } + + return ''; + } + + /** + * Returns the file name without the extension from a file path. + * + * @param string|null $extension if specified, only that extension is cut + * off (may contain leading dot) + */ + public static function getFilenameWithoutExtension(string $path, string $extension = null) + { + if ('' === $path) { + return ''; + } + + if (null !== $extension) { + // remove extension and trailing dot + return rtrim(basename($path, $extension), '.'); + } + + return pathinfo($path, \PATHINFO_FILENAME); + } + + /** + * Returns the extension from a file path (without leading dot). + * + * @param bool $forceLowerCase forces the extension to be lower-case + */ + public static function getExtension(string $path, bool $forceLowerCase = false): string + { + if ('' === $path) { + return ''; + } + + $extension = pathinfo($path, \PATHINFO_EXTENSION); + + if ($forceLowerCase) { + $extension = self::toLower($extension); + } + + return $extension; + } + + /** + * Returns whether the path has an (or the specified) extension. + * + * @param string $path the path string + * @param string|string[]|null $extensions if null or not provided, checks if + * an extension exists, otherwise + * checks for the specified extension + * or array of extensions (with or + * without leading dot) + * @param bool $ignoreCase whether to ignore case-sensitivity + */ + public static function hasExtension(string $path, $extensions = null, bool $ignoreCase = false): bool + { + if ('' === $path) { + return false; + } + + $actualExtension = self::getExtension($path, $ignoreCase); + + // Only check if path has any extension + if ([] === $extensions || null === $extensions) { + return '' !== $actualExtension; + } + + if (\is_string($extensions)) { + $extensions = [$extensions]; + } + + foreach ($extensions as $key => $extension) { + if ($ignoreCase) { + $extension = self::toLower($extension); + } + + // remove leading '.' in extensions array + $extensions[$key] = ltrim($extension, '.'); + } + + return \in_array($actualExtension, $extensions, true); + } + + /** + * Changes the extension of a path string. + * + * @param string $path The path string with filename.ext to change. + * @param string $extension new extension (with or without leading dot) + * + * @return string the path string with new file extension + */ + public static function changeExtension(string $path, string $extension): string + { + if ('' === $path) { + return ''; + } + + $actualExtension = self::getExtension($path); + $extension = ltrim($extension, '.'); + + // No extension for paths + if ('/' === mb_substr($path, -1)) { + return $path; + } + + // No actual extension in path + if (empty($actualExtension)) { + return $path.('.' === mb_substr($path, -1) ? '' : '.').$extension; + } + + return mb_substr($path, 0, -mb_strlen($actualExtension)).$extension; + } + + public static function isAbsolute(string $path): bool + { + if ('' === $path) { + return false; + } + + // Strip scheme + if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) { + $path = mb_substr($path, $schemeSeparatorPosition + 3); + } + + $firstCharacter = $path[0]; + + // UNIX root "/" or "\" (Windows style) + if ('/' === $firstCharacter || '\\' === $firstCharacter) { + return true; + } + + // Windows root + if (mb_strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) { + // Special case: "C:" + if (2 === mb_strlen($path)) { + return true; + } + + // Normal case: "C:/ or "C:\" + if ('/' === $path[2] || '\\' === $path[2]) { + return true; + } + } + + return false; + } + + public static function isRelative(string $path): bool + { + return !self::isAbsolute($path); + } + + /** + * Turns a relative path into an absolute path in canonical form. + * + * Usually, the relative path is appended to the given base path. Dot + * segments ("." and "..") are removed/collapsed and all slashes turned + * into forward slashes. + * + * ```php + * echo Path::makeAbsolute("../style.css", "/symfony/puli/css"); + * // => /symfony/puli/style.css + * ``` + * + * If an absolute path is passed, that path is returned unless its root + * directory is different than the one of the base path. In that case, an + * exception is thrown. + * + * ```php + * Path::makeAbsolute("/style.css", "/symfony/puli/css"); + * // => /style.css + * + * Path::makeAbsolute("C:/style.css", "C:/symfony/puli/css"); + * // => C:/style.css + * + * Path::makeAbsolute("C:/style.css", "/symfony/puli/css"); + * // InvalidArgumentException + * ``` + * + * If the base path is not an absolute path, an exception is thrown. + * + * The result is a canonical path. + * + * @param string $basePath an absolute base path + * + * @throws InvalidArgumentException if the base path is not absolute or if + * the given path is an absolute path with + * a different root than the base path + */ + public static function makeAbsolute(string $path, string $basePath): string + { + if ('' === $basePath) { + throw new InvalidArgumentException(sprintf('The base path must be a non-empty string. Got: "%s".', $basePath)); + } + + if (!self::isAbsolute($basePath)) { + throw new InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath)); + } + + if (self::isAbsolute($path)) { + return self::canonicalize($path); + } + + if (false !== ($schemeSeparatorPosition = mb_strpos($basePath, '://'))) { + $scheme = mb_substr($basePath, 0, $schemeSeparatorPosition + 3); + $basePath = mb_substr($basePath, $schemeSeparatorPosition + 3); + } else { + $scheme = ''; + } + + return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path); + } + + /** + * Turns a path into a relative path. + * + * The relative path is created relative to the given base path: + * + * ```php + * echo Path::makeRelative("/symfony/style.css", "/symfony/puli"); + * // => ../style.css + * ``` + * + * If a relative path is passed and the base path is absolute, the relative + * path is returned unchanged: + * + * ```php + * Path::makeRelative("style.css", "/symfony/puli/css"); + * // => style.css + * ``` + * + * If both paths are relative, the relative path is created with the + * assumption that both paths are relative to the same directory: + * + * ```php + * Path::makeRelative("style.css", "symfony/puli/css"); + * // => ../../../style.css + * ``` + * + * If both paths are absolute, their root directory must be the same, + * otherwise an exception is thrown: + * + * ```php + * Path::makeRelative("C:/symfony/style.css", "/symfony/puli"); + * // InvalidArgumentException + * ``` + * + * If the passed path is absolute, but the base path is not, an exception + * is thrown as well: + * + * ```php + * Path::makeRelative("/symfony/style.css", "symfony/puli"); + * // InvalidArgumentException + * ``` + * + * If the base path is not an absolute path, an exception is thrown. + * + * The result is a canonical path. + * + * @throws InvalidArgumentException if the base path is not absolute or if + * the given path has a different root + * than the base path + */ + public static function makeRelative(string $path, string $basePath): string + { + $path = self::canonicalize($path); + $basePath = self::canonicalize($basePath); + + [$root, $relativePath] = self::split($path); + [$baseRoot, $relativeBasePath] = self::split($basePath); + + // If the base path is given as absolute path and the path is already + // relative, consider it to be relative to the given absolute path + // already + if ('' === $root && '' !== $baseRoot) { + // If base path is already in its root + if ('' === $relativeBasePath) { + $relativePath = ltrim($relativePath, './\\'); + } + + return $relativePath; + } + + // If the passed path is absolute, but the base path is not, we + // cannot generate a relative path + if ('' !== $root && '' === $baseRoot) { + throw new InvalidArgumentException(sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath)); + } + + // Fail if the roots of the two paths are different + if ($baseRoot && $root !== $baseRoot) { + throw new InvalidArgumentException(sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot)); + } + + if ('' === $relativeBasePath) { + return $relativePath; + } + + // Build a "../../" prefix with as many "../" parts as necessary + $parts = explode('/', $relativePath); + $baseParts = explode('/', $relativeBasePath); + $dotDotPrefix = ''; + + // Once we found a non-matching part in the prefix, we need to add + // "../" parts for all remaining parts + $match = true; + + foreach ($baseParts as $index => $basePart) { + if ($match && isset($parts[$index]) && $basePart === $parts[$index]) { + unset($parts[$index]); + + continue; + } + + $match = false; + $dotDotPrefix .= '../'; + } + + return rtrim($dotDotPrefix.implode('/', $parts), '/'); + } + + /** + * Returns whether the given path is on the local filesystem. + */ + public static function isLocal(string $path): bool + { + return '' !== $path && false === mb_strpos($path, '://'); + } + + /** + * Returns the longest common base path in canonical form of a set of paths or + * `null` if the paths are on different Windows partitions. + * + * Dot segments ("." and "..") are removed/collapsed and all slashes turned + * into forward slashes. + * + * ```php + * $basePath = Path::getLongestCommonBasePath([ + * '/symfony/css/style.css', + * '/symfony/css/..' + * ]); + * // => /symfony + * ``` + * + * The root is returned if no common base path can be found: + * + * ```php + * $basePath = Path::getLongestCommonBasePath([ + * '/symfony/css/style.css', + * '/puli/css/..' + * ]); + * // => / + * ``` + * + * If the paths are located on different Windows partitions, `null` is + * returned. + * + * ```php + * $basePath = Path::getLongestCommonBasePath([ + * 'C:/symfony/css/style.css', + * 'D:/symfony/css/..' + * ]); + * // => null + * ``` + */ + public static function getLongestCommonBasePath(string ...$paths): ?string + { + [$bpRoot, $basePath] = self::split(self::canonicalize(reset($paths))); + + for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) { + [$root, $path] = self::split(self::canonicalize(current($paths))); + + // If we deal with different roots (e.g. C:/ vs. D:/), it's time + // to quit + if ($root !== $bpRoot) { + return null; + } + + // Make the base path shorter until it fits into path + while (true) { + if ('.' === $basePath) { + // No more base paths + $basePath = ''; + + // next path + continue 2; + } + + // Prevent false positives for common prefixes + // see isBasePath() + if (0 === mb_strpos($path.'/', $basePath.'/')) { + // next path + continue 2; + } + + $basePath = \dirname($basePath); + } + } + + return $bpRoot.$basePath; + } + + /** + * Joins two or more path strings into a canonical path. + */ + public static function join(string ...$paths): string + { + $finalPath = null; + $wasScheme = false; + + foreach ($paths as $path) { + if ('' === $path) { + continue; + } + + if (null === $finalPath) { + // For first part we keep slashes, like '/top', 'C:\' or 'phar://' + $finalPath = $path; + $wasScheme = (false !== mb_strpos($path, '://')); + continue; + } + + // Only add slash if previous part didn't end with '/' or '\' + if (!\in_array(mb_substr($finalPath, -1), ['/', '\\'])) { + $finalPath .= '/'; + } + + // If first part included a scheme like 'phar://' we allow \current part to start with '/', otherwise trim + $finalPath .= $wasScheme ? $path : ltrim($path, '/'); + $wasScheme = false; + } + + if (null === $finalPath) { + return ''; + } + + return self::canonicalize($finalPath); + } + + /** + * Returns whether a path is a base path of another path. + * + * Dot segments ("." and "..") are removed/collapsed and all slashes turned + * into forward slashes. + * + * ```php + * Path::isBasePath('/symfony', '/symfony/css'); + * // => true + * + * Path::isBasePath('/symfony', '/symfony'); + * // => true + * + * Path::isBasePath('/symfony', '/symfony/..'); + * // => false + * + * Path::isBasePath('/symfony', '/puli'); + * // => false + * ``` + */ + public static function isBasePath(string $basePath, string $ofPath): bool + { + $basePath = self::canonicalize($basePath); + $ofPath = self::canonicalize($ofPath); + + // Append slashes to prevent false positives when two paths have + // a common prefix, for example /base/foo and /base/foobar. + // Don't append a slash for the root "/", because then that root + // won't be discovered as common prefix ("//" is not a prefix of + // "/foobar/"). + return 0 === mb_strpos($ofPath.'/', rtrim($basePath, '/').'/'); + } + + /** + * @return non-empty-string[] + */ + private static function findCanonicalParts(string $root, string $pathWithoutRoot): array + { + $parts = explode('/', $pathWithoutRoot); + + $canonicalParts = []; + + // Collapse "." and "..", if possible + foreach ($parts as $part) { + if ('.' === $part || '' === $part) { + continue; + } + + // Collapse ".." with the previous part, if one exists + // Don't collapse ".." if the previous part is also ".." + if ('..' === $part && \count($canonicalParts) > 0 && '..' !== $canonicalParts[\count($canonicalParts) - 1]) { + array_pop($canonicalParts); + + continue; + } + + // Only add ".." prefixes for relative paths + if ('..' !== $part || '' === $root) { + $canonicalParts[] = $part; + } + } + + return $canonicalParts; + } + + /** + * Splits a canonical path into its root directory and the remainder. + * + * If the path has no root directory, an empty root directory will be + * returned. + * + * If the root directory is a Windows style partition, the resulting root + * will always contain a trailing slash. + * + * list ($root, $path) = Path::split("C:/symfony") + * // => ["C:/", "symfony"] + * + * list ($root, $path) = Path::split("C:") + * // => ["C:/", ""] + * + * @return array{string, string} an array with the root directory and the remaining relative path + */ + private static function split(string $path): array + { + if ('' === $path) { + return ['', '']; + } + + // Remember scheme as part of the root, if any + if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) { + $root = mb_substr($path, 0, $schemeSeparatorPosition + 3); + $path = mb_substr($path, $schemeSeparatorPosition + 3); + } else { + $root = ''; + } + + $length = mb_strlen($path); + + // Remove and remember root directory + if (0 === mb_strpos($path, '/')) { + $root .= '/'; + $path = $length > 1 ? mb_substr($path, 1) : ''; + } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { + if (2 === $length) { + // Windows special case: "C:" + $root .= $path.'/'; + $path = ''; + } elseif ('/' === $path[2]) { + // Windows normal case: "C:/".. + $root .= mb_substr($path, 0, 3); + $path = $length > 3 ? mb_substr($path, 3) : ''; + } + } + + return [$root, $path]; + } + + private static function toLower(string $string): string + { + if (false !== $encoding = mb_detect_encoding($string)) { + return mb_strtolower($string, $encoding); + } + + return strtolower($string, $encoding); + } + + private function __construct() + { + } +} diff --git a/vendor/symfony/filesystem/README.md b/vendor/symfony/filesystem/README.md new file mode 100644 index 0000000..f2f6d45 --- /dev/null +++ b/vendor/symfony/filesystem/README.md @@ -0,0 +1,13 @@ +Filesystem Component +==================== + +The Filesystem component provides basic utilities for the filesystem. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/filesystem.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/filesystem/composer.json b/vendor/symfony/filesystem/composer.json new file mode 100644 index 0000000..607dde3 --- /dev/null +++ b/vendor/symfony/filesystem/composer.json @@ -0,0 +1,30 @@ +{ + "name": "symfony/filesystem", + "type": "library", + "description": "Provides basic utilities for the filesystem", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Filesystem\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/symfony/polyfill-ctype/Ctype.php b/vendor/symfony/polyfill-ctype/Ctype.php new file mode 100644 index 0000000..ba75a2c --- /dev/null +++ b/vendor/symfony/polyfill-ctype/Ctype.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Ctype; + +/** + * Ctype implementation through regex. + * + * @internal + * + * @author Gert de Pagter + */ +final class Ctype +{ + /** + * Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise. + * + * @see https://php.net/ctype-alnum + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_alnum($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text); + } + + /** + * Returns TRUE if every character in text is a letter, FALSE otherwise. + * + * @see https://php.net/ctype-alpha + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_alpha($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text); + } + + /** + * Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise. + * + * @see https://php.net/ctype-cntrl + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_cntrl($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text); + } + + /** + * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise. + * + * @see https://php.net/ctype-digit + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_digit($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text); + } + + /** + * Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise. + * + * @see https://php.net/ctype-graph + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_graph($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text); + } + + /** + * Returns TRUE if every character in text is a lowercase letter. + * + * @see https://php.net/ctype-lower + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_lower($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text); + } + + /** + * Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all. + * + * @see https://php.net/ctype-print + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_print($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text); + } + + /** + * Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise. + * + * @see https://php.net/ctype-punct + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_punct($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text); + } + + /** + * Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters. + * + * @see https://php.net/ctype-space + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_space($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text); + } + + /** + * Returns TRUE if every character in text is an uppercase letter. + * + * @see https://php.net/ctype-upper + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_upper($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text); + } + + /** + * Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise. + * + * @see https://php.net/ctype-xdigit + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_xdigit($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text); + } + + /** + * Converts integers to their char versions according to normal ctype behaviour, if needed. + * + * If an integer between -128 and 255 inclusive is provided, + * it is interpreted as the ASCII value of a single character + * (negative values have 256 added in order to allow characters in the Extended ASCII range). + * Any other integer is interpreted as a string containing the decimal digits of the integer. + * + * @param mixed $int + * @param string $function + * + * @return mixed + */ + private static function convert_int_to_char_for_ctype($int, $function) + { + if (!\is_int($int)) { + return $int; + } + + if ($int < -128 || $int > 255) { + return (string) $int; + } + + if (\PHP_VERSION_ID >= 80100) { + @trigger_error($function.'(): Argument of type int will be interpreted as string in the future', \E_USER_DEPRECATED); + } + + if ($int < 0) { + $int += 256; + } + + return \chr($int); + } +} diff --git a/vendor/symfony/polyfill-ctype/LICENSE b/vendor/symfony/polyfill-ctype/LICENSE new file mode 100644 index 0000000..3f853aa --- /dev/null +++ b/vendor/symfony/polyfill-ctype/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-ctype/README.md b/vendor/symfony/polyfill-ctype/README.md new file mode 100644 index 0000000..8add1ab --- /dev/null +++ b/vendor/symfony/polyfill-ctype/README.md @@ -0,0 +1,12 @@ +Symfony Polyfill / Ctype +======================== + +This component provides `ctype_*` functions to users who run php versions without the ctype extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-ctype/bootstrap.php b/vendor/symfony/polyfill-ctype/bootstrap.php new file mode 100644 index 0000000..d54524b --- /dev/null +++ b/vendor/symfony/polyfill-ctype/bootstrap.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Ctype as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('ctype_alnum')) { + function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); } +} +if (!function_exists('ctype_alpha')) { + function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); } +} +if (!function_exists('ctype_cntrl')) { + function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); } +} +if (!function_exists('ctype_digit')) { + function ctype_digit($text) { return p\Ctype::ctype_digit($text); } +} +if (!function_exists('ctype_graph')) { + function ctype_graph($text) { return p\Ctype::ctype_graph($text); } +} +if (!function_exists('ctype_lower')) { + function ctype_lower($text) { return p\Ctype::ctype_lower($text); } +} +if (!function_exists('ctype_print')) { + function ctype_print($text) { return p\Ctype::ctype_print($text); } +} +if (!function_exists('ctype_punct')) { + function ctype_punct($text) { return p\Ctype::ctype_punct($text); } +} +if (!function_exists('ctype_space')) { + function ctype_space($text) { return p\Ctype::ctype_space($text); } +} +if (!function_exists('ctype_upper')) { + function ctype_upper($text) { return p\Ctype::ctype_upper($text); } +} +if (!function_exists('ctype_xdigit')) { + function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); } +} diff --git a/vendor/symfony/polyfill-ctype/bootstrap80.php b/vendor/symfony/polyfill-ctype/bootstrap80.php new file mode 100644 index 0000000..ab2f861 --- /dev/null +++ b/vendor/symfony/polyfill-ctype/bootstrap80.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Ctype as p; + +if (!function_exists('ctype_alnum')) { + function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); } +} +if (!function_exists('ctype_alpha')) { + function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); } +} +if (!function_exists('ctype_cntrl')) { + function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); } +} +if (!function_exists('ctype_digit')) { + function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); } +} +if (!function_exists('ctype_graph')) { + function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); } +} +if (!function_exists('ctype_lower')) { + function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); } +} +if (!function_exists('ctype_print')) { + function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); } +} +if (!function_exists('ctype_punct')) { + function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); } +} +if (!function_exists('ctype_space')) { + function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); } +} +if (!function_exists('ctype_upper')) { + function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); } +} +if (!function_exists('ctype_xdigit')) { + function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); } +} diff --git a/vendor/symfony/polyfill-ctype/composer.json b/vendor/symfony/polyfill-ctype/composer.json new file mode 100644 index 0000000..ccb8e57 --- /dev/null +++ b/vendor/symfony/polyfill-ctype/composer.json @@ -0,0 +1,41 @@ +{ + "name": "symfony/polyfill-ctype", + "type": "library", + "description": "Symfony polyfill for ctype functions", + "keywords": ["polyfill", "compatibility", "portable", "ctype"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Ctype\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/polyfill-intl-grapheme/Grapheme.php b/vendor/symfony/polyfill-intl-grapheme/Grapheme.php new file mode 100644 index 0000000..6f7c0c7 --- /dev/null +++ b/vendor/symfony/polyfill-intl-grapheme/Grapheme.php @@ -0,0 +1,247 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Grapheme; + +\define('SYMFONY_GRAPHEME_CLUSTER_RX', ((float) \PCRE_VERSION < 10 ? (float) \PCRE_VERSION >= 8.32 : (float) \PCRE_VERSION >= 10.39) ? '\X' : Grapheme::GRAPHEME_CLUSTER_RX); + +/** + * Partial intl implementation in pure PHP. + * + * Implemented: + * - grapheme_extract - Extract a sequence of grapheme clusters from a text buffer, which must be encoded in UTF-8 + * - grapheme_stripos - Find position (in grapheme units) of first occurrence of a case-insensitive string + * - grapheme_stristr - Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack + * - grapheme_strlen - Get string length in grapheme units + * - grapheme_strpos - Find position (in grapheme units) of first occurrence of a string + * - grapheme_strripos - Find position (in grapheme units) of last occurrence of a case-insensitive string + * - grapheme_strrpos - Find position (in grapheme units) of last occurrence of a string + * - grapheme_strstr - Returns part of haystack string from the first occurrence of needle to the end of haystack + * - grapheme_substr - Return part of a string + * + * @author Nicolas Grekas + * + * @internal + */ +final class Grapheme +{ + // (CRLF|([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*|[Control]) + // This regular expression is a work around for http://bugs.exim.org/1279 + public const GRAPHEME_CLUSTER_RX = '(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])'; + + private const CASE_FOLD = [ + ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], + ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], + ]; + + public static function grapheme_extract($s, $size, $type = \GRAPHEME_EXTR_COUNT, $start = 0, &$next = 0) + { + if (0 > $start) { + $start = \strlen($s) + $start; + } + + if (!is_scalar($s)) { + $hasError = false; + set_error_handler(function () use (&$hasError) { $hasError = true; }); + $next = substr($s, $start); + restore_error_handler(); + if ($hasError) { + substr($s, $start); + $s = ''; + } else { + $s = $next; + } + } else { + $s = substr($s, $start); + } + $size = (int) $size; + $type = (int) $type; + $start = (int) $start; + + if (\GRAPHEME_EXTR_COUNT !== $type && \GRAPHEME_EXTR_MAXBYTES !== $type && \GRAPHEME_EXTR_MAXCHARS !== $type) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('grapheme_extract(): Argument #3 ($type) must be one of GRAPHEME_EXTR_COUNT, GRAPHEME_EXTR_MAXBYTES, or GRAPHEME_EXTR_MAXCHARS'); + } + + if (!isset($s[0]) || 0 > $size || 0 > $start) { + return false; + } + if (0 === $size) { + return ''; + } + + $next = $start; + + $s = preg_split('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', "\r\n".$s, $size + 1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); + + if (!isset($s[1])) { + return false; + } + + $i = 1; + $ret = ''; + + do { + if (\GRAPHEME_EXTR_COUNT === $type) { + --$size; + } elseif (\GRAPHEME_EXTR_MAXBYTES === $type) { + $size -= \strlen($s[$i]); + } else { + $size -= iconv_strlen($s[$i], 'UTF-8//IGNORE'); + } + + if ($size >= 0) { + $ret .= $s[$i]; + } + } while (isset($s[++$i]) && $size > 0); + + $next += \strlen($ret); + + return $ret; + } + + public static function grapheme_strlen($s) + { + preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $s, -1, $len); + + return 0 === $len && '' !== $s ? null : $len; + } + + public static function grapheme_substr($s, $start, $len = null) + { + if (null === $len) { + $len = 2147483647; + } + + preg_match_all('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s, $s); + + $slen = \count($s[0]); + $start = (int) $start; + + if (0 > $start) { + $start += $slen; + } + if (0 > $start) { + if (\PHP_VERSION_ID < 80000) { + return false; + } + + $start = 0; + } + if ($start >= $slen) { + return \PHP_VERSION_ID >= 80000 ? '' : false; + } + + $rem = $slen - $start; + + if (0 > $len) { + $len += $rem; + } + if (0 === $len) { + return ''; + } + if (0 > $len) { + return \PHP_VERSION_ID >= 80000 ? '' : false; + } + if ($len > $rem) { + $len = $rem; + } + + return implode('', \array_slice($s[0], $start, $len)); + } + + public static function grapheme_strpos($s, $needle, $offset = 0) + { + return self::grapheme_position($s, $needle, $offset, 0); + } + + public static function grapheme_stripos($s, $needle, $offset = 0) + { + return self::grapheme_position($s, $needle, $offset, 1); + } + + public static function grapheme_strrpos($s, $needle, $offset = 0) + { + return self::grapheme_position($s, $needle, $offset, 2); + } + + public static function grapheme_strripos($s, $needle, $offset = 0) + { + return self::grapheme_position($s, $needle, $offset, 3); + } + + public static function grapheme_stristr($s, $needle, $beforeNeedle = false) + { + return mb_stristr($s, $needle, $beforeNeedle, 'UTF-8'); + } + + public static function grapheme_strstr($s, $needle, $beforeNeedle = false) + { + return mb_strstr($s, $needle, $beforeNeedle, 'UTF-8'); + } + + private static function grapheme_position($s, $needle, $offset, $mode) + { + $needle = (string) $needle; + if (80000 > \PHP_VERSION_ID && !preg_match('/./us', $needle)) { + return false; + } + $s = (string) $s; + if (!preg_match('/./us', $s)) { + return false; + } + if ($offset > 0) { + $s = self::grapheme_substr($s, $offset); + } elseif ($offset < 0) { + if (2 > $mode) { + $offset += self::grapheme_strlen($s); + $s = self::grapheme_substr($s, $offset); + if (0 > $offset) { + $offset = 0; + } + } elseif (0 > $offset += self::grapheme_strlen($needle)) { + $s = self::grapheme_substr($s, 0, $offset); + $offset = 0; + } else { + $offset = 0; + } + } + + // As UTF-8 is self-synchronizing, and we have ensured the strings are valid UTF-8, + // we can use normal binary string functions here. For case-insensitive searches, + // case fold the strings first. + $caseInsensitive = $mode & 1; + $reverse = $mode & 2; + if ($caseInsensitive) { + // Use the same case folding mode as mbstring does for mb_stripos(). + // Stick to SIMPLE case folding to avoid changing the length of the string, which + // might result in offsets being shifted. + $mode = \defined('MB_CASE_FOLD_SIMPLE') ? \MB_CASE_FOLD_SIMPLE : \MB_CASE_LOWER; + $s = mb_convert_case($s, $mode, 'UTF-8'); + $needle = mb_convert_case($needle, $mode, 'UTF-8'); + + if (!\defined('MB_CASE_FOLD_SIMPLE')) { + $s = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $s); + $needle = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $needle); + } + } + if ($reverse) { + $needlePos = strrpos($s, $needle); + } else { + $needlePos = strpos($s, $needle); + } + + return false !== $needlePos ? self::grapheme_strlen(substr($s, 0, $needlePos)) + $offset : false; + } +} diff --git a/vendor/symfony/polyfill-intl-grapheme/LICENSE b/vendor/symfony/polyfill-intl-grapheme/LICENSE new file mode 100644 index 0000000..4cd8bdd --- /dev/null +++ b/vendor/symfony/polyfill-intl-grapheme/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-intl-grapheme/README.md b/vendor/symfony/polyfill-intl-grapheme/README.md new file mode 100644 index 0000000..77523ea --- /dev/null +++ b/vendor/symfony/polyfill-intl-grapheme/README.md @@ -0,0 +1,31 @@ +Symfony Polyfill / Intl: Grapheme +================================= + +This component provides a partial, native PHP implementation of the +[Grapheme functions](https://php.net/intl.grapheme) from the +[Intl](https://php.net/intl) extension. + +- [`grapheme_extract`](https://php.net/grapheme_extract): Extract a sequence of grapheme + clusters from a text buffer, which must be encoded in UTF-8 +- [`grapheme_stripos`](https://php.net/grapheme_stripos): Find position (in grapheme units) + of first occurrence of a case-insensitive string +- [`grapheme_stristr`](https://php.net/grapheme_stristr): Returns part of haystack string + from the first occurrence of case-insensitive needle to the end of haystack +- [`grapheme_strlen`](https://php.net/grapheme_strlen): Get string length in grapheme units +- [`grapheme_strpos`](https://php.net/grapheme_strpos): Find position (in grapheme units) + of first occurrence of a string +- [`grapheme_strripos`](https://php.net/grapheme_strripos): Find position (in grapheme units) + of last occurrence of a case-insensitive string +- [`grapheme_strrpos`](https://php.net/grapheme_strrpos): Find position (in grapheme units) + of last occurrence of a string +- [`grapheme_strstr`](https://php.net/grapheme_strstr): Returns part of haystack string from + the first occurrence of needle to the end of haystack +- [`grapheme_substr`](https://php.net/grapheme_substr): Return part of a string + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-intl-grapheme/bootstrap.php b/vendor/symfony/polyfill-intl-grapheme/bootstrap.php new file mode 100644 index 0000000..a9ea03c --- /dev/null +++ b/vendor/symfony/polyfill-intl-grapheme/bootstrap.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Grapheme as p; + +if (extension_loaded('intl')) { + return; +} + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!defined('GRAPHEME_EXTR_COUNT')) { + define('GRAPHEME_EXTR_COUNT', 0); +} +if (!defined('GRAPHEME_EXTR_MAXBYTES')) { + define('GRAPHEME_EXTR_MAXBYTES', 1); +} +if (!defined('GRAPHEME_EXTR_MAXCHARS')) { + define('GRAPHEME_EXTR_MAXCHARS', 2); +} + +if (!function_exists('grapheme_extract')) { + function grapheme_extract($haystack, $size, $type = 0, $start = 0, &$next = 0) { return p\Grapheme::grapheme_extract($haystack, $size, $type, $start, $next); } +} +if (!function_exists('grapheme_stripos')) { + function grapheme_stripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_stripos($haystack, $needle, $offset); } +} +if (!function_exists('grapheme_stristr')) { + function grapheme_stristr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_stristr($haystack, $needle, $beforeNeedle); } +} +if (!function_exists('grapheme_strlen')) { + function grapheme_strlen($input) { return p\Grapheme::grapheme_strlen($input); } +} +if (!function_exists('grapheme_strpos')) { + function grapheme_strpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strpos($haystack, $needle, $offset); } +} +if (!function_exists('grapheme_strripos')) { + function grapheme_strripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strripos($haystack, $needle, $offset); } +} +if (!function_exists('grapheme_strrpos')) { + function grapheme_strrpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strrpos($haystack, $needle, $offset); } +} +if (!function_exists('grapheme_strstr')) { + function grapheme_strstr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_strstr($haystack, $needle, $beforeNeedle); } +} +if (!function_exists('grapheme_substr')) { + function grapheme_substr($string, $offset, $length = null) { return p\Grapheme::grapheme_substr($string, $offset, $length); } +} diff --git a/vendor/symfony/polyfill-intl-grapheme/bootstrap80.php b/vendor/symfony/polyfill-intl-grapheme/bootstrap80.php new file mode 100644 index 0000000..b8c0786 --- /dev/null +++ b/vendor/symfony/polyfill-intl-grapheme/bootstrap80.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Grapheme as p; + +if (!defined('GRAPHEME_EXTR_COUNT')) { + define('GRAPHEME_EXTR_COUNT', 0); +} +if (!defined('GRAPHEME_EXTR_MAXBYTES')) { + define('GRAPHEME_EXTR_MAXBYTES', 1); +} +if (!defined('GRAPHEME_EXTR_MAXCHARS')) { + define('GRAPHEME_EXTR_MAXCHARS', 2); +} + +if (!function_exists('grapheme_extract')) { + function grapheme_extract(?string $haystack, ?int $size, ?int $type = GRAPHEME_EXTR_COUNT, ?int $offset = 0, &$next = null): string|false { return p\Grapheme::grapheme_extract((string) $haystack, (int) $size, (int) $type, (int) $offset, $next); } +} +if (!function_exists('grapheme_stripos')) { + function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_stristr')) { + function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } +} +if (!function_exists('grapheme_strlen')) { + function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); } +} +if (!function_exists('grapheme_strpos')) { + function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strripos')) { + function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strrpos')) { + function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strstr')) { + function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } +} +if (!function_exists('grapheme_substr')) { + function grapheme_substr(?string $string, ?int $offset, ?int $length = null): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, $length); } +} diff --git a/vendor/symfony/polyfill-intl-grapheme/composer.json b/vendor/symfony/polyfill-intl-grapheme/composer.json new file mode 100644 index 0000000..02c98ee --- /dev/null +++ b/vendor/symfony/polyfill-intl-grapheme/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/polyfill-intl-grapheme", + "type": "library", + "description": "Symfony polyfill for intl's grapheme_* functions", + "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "grapheme"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Intl\\Grapheme\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-intl": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/polyfill-intl-normalizer/LICENSE b/vendor/symfony/polyfill-intl-normalizer/LICENSE new file mode 100644 index 0000000..4cd8bdd --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-intl-normalizer/Normalizer.php b/vendor/symfony/polyfill-intl-normalizer/Normalizer.php new file mode 100644 index 0000000..4443c23 --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/Normalizer.php @@ -0,0 +1,310 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Normalizer; + +/** + * Normalizer is a PHP fallback implementation of the Normalizer class provided by the intl extension. + * + * It has been validated with Unicode 6.3 Normalization Conformance Test. + * See http://www.unicode.org/reports/tr15/ for detailed info about Unicode normalizations. + * + * @author Nicolas Grekas + * + * @internal + */ +class Normalizer +{ + public const FORM_D = \Normalizer::FORM_D; + public const FORM_KD = \Normalizer::FORM_KD; + public const FORM_C = \Normalizer::FORM_C; + public const FORM_KC = \Normalizer::FORM_KC; + public const NFD = \Normalizer::NFD; + public const NFKD = \Normalizer::NFKD; + public const NFC = \Normalizer::NFC; + public const NFKC = \Normalizer::NFKC; + + private static $C; + private static $D; + private static $KD; + private static $cC; + private static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + private static $ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; + + public static function isNormalized(string $s, int $form = self::FORM_C) + { + if (!\in_array($form, [self::NFD, self::NFKD, self::NFC, self::NFKC])) { + return false; + } + if (!isset($s[strspn($s, self::$ASCII)])) { + return true; + } + if (self::NFC == $form && preg_match('//u', $s) && !preg_match('/[^\x00-\x{2FF}]/u', $s)) { + return true; + } + + return self::normalize($s, $form) === $s; + } + + public static function normalize(string $s, int $form = self::FORM_C) + { + if (!preg_match('//u', $s)) { + return false; + } + + switch ($form) { + case self::NFC: $C = true; $K = false; break; + case self::NFD: $C = false; $K = false; break; + case self::NFKC: $C = true; $K = true; break; + case self::NFKD: $C = false; $K = true; break; + default: + if (\defined('Normalizer::NONE') && \Normalizer::NONE == $form) { + return $s; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('normalizer_normalize(): Argument #2 ($form) must be a a valid normalization form'); + } + + if ('' === $s) { + return ''; + } + + if ($K && null === self::$KD) { + self::$KD = self::getData('compatibilityDecomposition'); + } + + if (null === self::$D) { + self::$D = self::getData('canonicalDecomposition'); + self::$cC = self::getData('combiningClass'); + } + + if (null !== $mbEncoding = (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) ? mb_internal_encoding() : null) { + mb_internal_encoding('8bit'); + } + + $r = self::decompose($s, $K); + + if ($C) { + if (null === self::$C) { + self::$C = self::getData('canonicalComposition'); + } + + $r = self::recompose($r); + } + if (null !== $mbEncoding) { + mb_internal_encoding($mbEncoding); + } + + return $r; + } + + private static function recompose($s) + { + $ASCII = self::$ASCII; + $compMap = self::$C; + $combClass = self::$cC; + $ulenMask = self::$ulenMask; + + $result = $tail = ''; + + $i = $s[0] < "\x80" ? 1 : $ulenMask[$s[0] & "\xF0"]; + $len = \strlen($s); + + $lastUchr = substr($s, 0, $i); + $lastUcls = isset($combClass[$lastUchr]) ? 256 : 0; + + while ($i < $len) { + if ($s[$i] < "\x80") { + // ASCII chars + + if ($tail) { + $lastUchr .= $tail; + $tail = ''; + } + + if ($j = strspn($s, $ASCII, $i + 1)) { + $lastUchr .= substr($s, $i, $j); + $i += $j; + } + + $result .= $lastUchr; + $lastUchr = $s[$i]; + $lastUcls = 0; + ++$i; + continue; + } + + $ulen = $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + + if ($lastUchr < "\xE1\x84\x80" || "\xE1\x84\x92" < $lastUchr + || $uchr < "\xE1\x85\xA1" || "\xE1\x85\xB5" < $uchr + || $lastUcls) { + // Table lookup and combining chars composition + + $ucls = $combClass[$uchr] ?? 0; + + if (isset($compMap[$lastUchr.$uchr]) && (!$lastUcls || $lastUcls < $ucls)) { + $lastUchr = $compMap[$lastUchr.$uchr]; + } elseif ($lastUcls = $ucls) { + $tail .= $uchr; + } else { + if ($tail) { + $lastUchr .= $tail; + $tail = ''; + } + + $result .= $lastUchr; + $lastUchr = $uchr; + } + } else { + // Hangul chars + + $L = \ord($lastUchr[2]) - 0x80; + $V = \ord($uchr[2]) - 0xA1; + $T = 0; + + $uchr = substr($s, $i + $ulen, 3); + + if ("\xE1\x86\xA7" <= $uchr && $uchr <= "\xE1\x87\x82") { + $T = \ord($uchr[2]) - 0xA7; + 0 > $T && $T += 0x40; + $ulen += 3; + } + + $L = 0xAC00 + ($L * 21 + $V) * 28 + $T; + $lastUchr = \chr(0xE0 | $L >> 12).\chr(0x80 | $L >> 6 & 0x3F).\chr(0x80 | $L & 0x3F); + } + + $i += $ulen; + } + + return $result.$lastUchr.$tail; + } + + private static function decompose($s, $c) + { + $result = ''; + + $ASCII = self::$ASCII; + $decompMap = self::$D; + $combClass = self::$cC; + $ulenMask = self::$ulenMask; + if ($c) { + $compatMap = self::$KD; + } + + $c = []; + $i = 0; + $len = \strlen($s); + + while ($i < $len) { + if ($s[$i] < "\x80") { + // ASCII chars + + if ($c) { + ksort($c); + $result .= implode('', $c); + $c = []; + } + + $j = 1 + strspn($s, $ASCII, $i + 1); + $result .= substr($s, $i, $j); + $i += $j; + continue; + } + + $ulen = $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + + if ($uchr < "\xEA\xB0\x80" || "\xED\x9E\xA3" < $uchr) { + // Table lookup + + if ($uchr !== $j = $compatMap[$uchr] ?? ($decompMap[$uchr] ?? $uchr)) { + $uchr = $j; + + $j = \strlen($uchr); + $ulen = $uchr[0] < "\x80" ? 1 : $ulenMask[$uchr[0] & "\xF0"]; + + if ($ulen != $j) { + // Put trailing chars in $s + + $j -= $ulen; + $i -= $j; + + if (0 > $i) { + $s = str_repeat(' ', -$i).$s; + $len -= $i; + $i = 0; + } + + while ($j--) { + $s[$i + $j] = $uchr[$ulen + $j]; + } + + $uchr = substr($uchr, 0, $ulen); + } + } + if (isset($combClass[$uchr])) { + // Combining chars, for sorting + + if (!isset($c[$combClass[$uchr]])) { + $c[$combClass[$uchr]] = ''; + } + $c[$combClass[$uchr]] .= $uchr; + continue; + } + } else { + // Hangul chars + + $uchr = unpack('C*', $uchr); + $j = (($uchr[1] - 224) << 12) + (($uchr[2] - 128) << 6) + $uchr[3] - 0xAC80; + + $uchr = "\xE1\x84".\chr(0x80 + (int) ($j / 588)) + ."\xE1\x85".\chr(0xA1 + (int) (($j % 588) / 28)); + + if ($j %= 28) { + $uchr .= $j < 25 + ? ("\xE1\x86".\chr(0xA7 + $j)) + : ("\xE1\x87".\chr(0x67 + $j)); + } + } + if ($c) { + ksort($c); + $result .= implode('', $c); + $c = []; + } + + $result .= $uchr; + } + + if ($c) { + ksort($c); + $result .= implode('', $c); + } + + return $result; + } + + private static function getData($file) + { + if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { + return require $file; + } + + return false; + } +} diff --git a/vendor/symfony/polyfill-intl-normalizer/README.md b/vendor/symfony/polyfill-intl-normalizer/README.md new file mode 100644 index 0000000..15060c5 --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/README.md @@ -0,0 +1,14 @@ +Symfony Polyfill / Intl: Normalizer +=================================== + +This component provides a fallback implementation for the +[`Normalizer`](https://php.net/Normalizer) class provided +by the [Intl](https://php.net/intl) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php b/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php new file mode 100644 index 0000000..0fdfc89 --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php @@ -0,0 +1,17 @@ + 'À', + 'Á' => 'Á', + 'Â' => 'Â', + 'Ã' => 'Ã', + 'Ä' => 'Ä', + 'Å' => 'Å', + 'Ç' => 'Ç', + 'È' => 'È', + 'É' => 'É', + 'Ê' => 'Ê', + 'Ë' => 'Ë', + 'Ì' => 'Ì', + 'Í' => 'Í', + 'Î' => 'Î', + 'Ï' => 'Ï', + 'Ñ' => 'Ñ', + 'Ò' => 'Ò', + 'Ó' => 'Ó', + 'Ô' => 'Ô', + 'Õ' => 'Õ', + 'Ö' => 'Ö', + 'Ù' => 'Ù', + 'Ú' => 'Ú', + 'Û' => 'Û', + 'Ü' => 'Ü', + 'Ý' => 'Ý', + 'à' => 'à', + 'á' => 'á', + 'â' => 'â', + 'ã' => 'ã', + 'ä' => 'ä', + 'å' => 'å', + 'ç' => 'ç', + 'è' => 'è', + 'é' => 'é', + 'ê' => 'ê', + 'ë' => 'ë', + 'ì' => 'ì', + 'í' => 'í', + 'î' => 'î', + 'ï' => 'ï', + 'ñ' => 'ñ', + 'ò' => 'ò', + 'ó' => 'ó', + 'ô' => 'ô', + 'õ' => 'õ', + 'ö' => 'ö', + 'ù' => 'ù', + 'ú' => 'ú', + 'û' => 'û', + 'ü' => 'ü', + 'ý' => 'ý', + 'ÿ' => 'ÿ', + 'Ā' => 'Ā', + 'ā' => 'ā', + 'Ă' => 'Ă', + 'ă' => 'ă', + 'Ą' => 'Ą', + 'ą' => 'ą', + 'Ć' => 'Ć', + 'ć' => 'ć', + 'Ĉ' => 'Ĉ', + 'ĉ' => 'ĉ', + 'Ċ' => 'Ċ', + 'ċ' => 'ċ', + 'Č' => 'Č', + 'č' => 'č', + 'Ď' => 'Ď', + 'ď' => 'ď', + 'Ē' => 'Ē', + 'ē' => 'ē', + 'Ĕ' => 'Ĕ', + 'ĕ' => 'ĕ', + 'Ė' => 'Ė', + 'ė' => 'ė', + 'Ę' => 'Ę', + 'ę' => 'ę', + 'Ě' => 'Ě', + 'ě' => 'ě', + 'Ĝ' => 'Ĝ', + 'ĝ' => 'ĝ', + 'Ğ' => 'Ğ', + 'ğ' => 'ğ', + 'Ġ' => 'Ġ', + 'ġ' => 'ġ', + 'Ģ' => 'Ģ', + 'ģ' => 'ģ', + 'Ĥ' => 'Ĥ', + 'ĥ' => 'ĥ', + 'Ĩ' => 'Ĩ', + 'ĩ' => 'ĩ', + 'Ī' => 'Ī', + 'ī' => 'ī', + 'Ĭ' => 'Ĭ', + 'ĭ' => 'ĭ', + 'Į' => 'Į', + 'į' => 'į', + 'İ' => 'İ', + 'Ĵ' => 'Ĵ', + 'ĵ' => 'ĵ', + 'Ķ' => 'Ķ', + 'ķ' => 'ķ', + 'Ĺ' => 'Ĺ', + 'ĺ' => 'ĺ', + 'Ļ' => 'Ļ', + 'ļ' => 'ļ', + 'Ľ' => 'Ľ', + 'ľ' => 'ľ', + 'Ń' => 'Ń', + 'ń' => 'ń', + 'Ņ' => 'Ņ', + 'ņ' => 'ņ', + 'Ň' => 'Ň', + 'ň' => 'ň', + 'Ō' => 'Ō', + 'ō' => 'ō', + 'Ŏ' => 'Ŏ', + 'ŏ' => 'ŏ', + 'Ő' => 'Ő', + 'ő' => 'ő', + 'Ŕ' => 'Ŕ', + 'ŕ' => 'ŕ', + 'Ŗ' => 'Ŗ', + 'ŗ' => 'ŗ', + 'Ř' => 'Ř', + 'ř' => 'ř', + 'Ś' => 'Ś', + 'ś' => 'ś', + 'Ŝ' => 'Ŝ', + 'ŝ' => 'ŝ', + 'Ş' => 'Ş', + 'ş' => 'ş', + 'Š' => 'Š', + 'š' => 'š', + 'Ţ' => 'Ţ', + 'ţ' => 'ţ', + 'Ť' => 'Ť', + 'ť' => 'ť', + 'Ũ' => 'Ũ', + 'ũ' => 'ũ', + 'Ū' => 'Ū', + 'ū' => 'ū', + 'Ŭ' => 'Ŭ', + 'ŭ' => 'ŭ', + 'Ů' => 'Ů', + 'ů' => 'ů', + 'Ű' => 'Ű', + 'ű' => 'ű', + 'Ų' => 'Ų', + 'ų' => 'ų', + 'Ŵ' => 'Ŵ', + 'ŵ' => 'ŵ', + 'Ŷ' => 'Ŷ', + 'ŷ' => 'ŷ', + 'Ÿ' => 'Ÿ', + 'Ź' => 'Ź', + 'ź' => 'ź', + 'Ż' => 'Ż', + 'ż' => 'ż', + 'Ž' => 'Ž', + 'ž' => 'ž', + 'Ơ' => 'Ơ', + 'ơ' => 'ơ', + 'Ư' => 'Ư', + 'ư' => 'ư', + 'Ǎ' => 'Ǎ', + 'ǎ' => 'ǎ', + 'Ǐ' => 'Ǐ', + 'ǐ' => 'ǐ', + 'Ǒ' => 'Ǒ', + 'ǒ' => 'ǒ', + 'Ǔ' => 'Ǔ', + 'ǔ' => 'ǔ', + 'Ǖ' => 'Ǖ', + 'ǖ' => 'ǖ', + 'Ǘ' => 'Ǘ', + 'ǘ' => 'ǘ', + 'Ǚ' => 'Ǚ', + 'ǚ' => 'ǚ', + 'Ǜ' => 'Ǜ', + 'ǜ' => 'ǜ', + 'Ǟ' => 'Ǟ', + 'ǟ' => 'ǟ', + 'Ǡ' => 'Ǡ', + 'ǡ' => 'ǡ', + 'Ǣ' => 'Ǣ', + 'ǣ' => 'ǣ', + 'Ǧ' => 'Ǧ', + 'ǧ' => 'ǧ', + 'Ǩ' => 'Ǩ', + 'ǩ' => 'ǩ', + 'Ǫ' => 'Ǫ', + 'ǫ' => 'ǫ', + 'Ǭ' => 'Ǭ', + 'ǭ' => 'ǭ', + 'Ǯ' => 'Ǯ', + 'ǯ' => 'ǯ', + 'ǰ' => 'ǰ', + 'Ǵ' => 'Ǵ', + 'ǵ' => 'ǵ', + 'Ǹ' => 'Ǹ', + 'ǹ' => 'ǹ', + 'Ǻ' => 'Ǻ', + 'ǻ' => 'ǻ', + 'Ǽ' => 'Ǽ', + 'ǽ' => 'ǽ', + 'Ǿ' => 'Ǿ', + 'ǿ' => 'ǿ', + 'Ȁ' => 'Ȁ', + 'ȁ' => 'ȁ', + 'Ȃ' => 'Ȃ', + 'ȃ' => 'ȃ', + 'Ȅ' => 'Ȅ', + 'ȅ' => 'ȅ', + 'Ȇ' => 'Ȇ', + 'ȇ' => 'ȇ', + 'Ȉ' => 'Ȉ', + 'ȉ' => 'ȉ', + 'Ȋ' => 'Ȋ', + 'ȋ' => 'ȋ', + 'Ȍ' => 'Ȍ', + 'ȍ' => 'ȍ', + 'Ȏ' => 'Ȏ', + 'ȏ' => 'ȏ', + 'Ȑ' => 'Ȑ', + 'ȑ' => 'ȑ', + 'Ȓ' => 'Ȓ', + 'ȓ' => 'ȓ', + 'Ȕ' => 'Ȕ', + 'ȕ' => 'ȕ', + 'Ȗ' => 'Ȗ', + 'ȗ' => 'ȗ', + 'Ș' => 'Ș', + 'ș' => 'ș', + 'Ț' => 'Ț', + 'ț' => 'ț', + 'Ȟ' => 'Ȟ', + 'ȟ' => 'ȟ', + 'Ȧ' => 'Ȧ', + 'ȧ' => 'ȧ', + 'Ȩ' => 'Ȩ', + 'ȩ' => 'ȩ', + 'Ȫ' => 'Ȫ', + 'ȫ' => 'ȫ', + 'Ȭ' => 'Ȭ', + 'ȭ' => 'ȭ', + 'Ȯ' => 'Ȯ', + 'ȯ' => 'ȯ', + 'Ȱ' => 'Ȱ', + 'ȱ' => 'ȱ', + 'Ȳ' => 'Ȳ', + 'ȳ' => 'ȳ', + '΅' => '΅', + 'Ά' => 'Ά', + 'Έ' => 'Έ', + 'Ή' => 'Ή', + 'Ί' => 'Ί', + 'Ό' => 'Ό', + 'Ύ' => 'Ύ', + 'Ώ' => 'Ώ', + 'ΐ' => 'ΐ', + 'Ϊ' => 'Ϊ', + 'Ϋ' => 'Ϋ', + 'ά' => 'ά', + 'έ' => 'έ', + 'ή' => 'ή', + 'ί' => 'ί', + 'ΰ' => 'ΰ', + 'ϊ' => 'ϊ', + 'ϋ' => 'ϋ', + 'ό' => 'ό', + 'ύ' => 'ύ', + 'ώ' => 'ώ', + 'ϓ' => 'ϓ', + 'ϔ' => 'ϔ', + 'Ѐ' => 'Ѐ', + 'Ё' => 'Ё', + 'Ѓ' => 'Ѓ', + 'Ї' => 'Ї', + 'Ќ' => 'Ќ', + 'Ѝ' => 'Ѝ', + 'Ў' => 'Ў', + 'Й' => 'Й', + 'й' => 'й', + 'ѐ' => 'ѐ', + 'ё' => 'ё', + 'ѓ' => 'ѓ', + 'ї' => 'ї', + 'ќ' => 'ќ', + 'ѝ' => 'ѝ', + 'ў' => 'ў', + 'Ѷ' => 'Ѷ', + 'ѷ' => 'ѷ', + 'Ӂ' => 'Ӂ', + 'ӂ' => 'ӂ', + 'Ӑ' => 'Ӑ', + 'ӑ' => 'ӑ', + 'Ӓ' => 'Ӓ', + 'ӓ' => 'ӓ', + 'Ӗ' => 'Ӗ', + 'ӗ' => 'ӗ', + 'Ӛ' => 'Ӛ', + 'ӛ' => 'ӛ', + 'Ӝ' => 'Ӝ', + 'ӝ' => 'ӝ', + 'Ӟ' => 'Ӟ', + 'ӟ' => 'ӟ', + 'Ӣ' => 'Ӣ', + 'ӣ' => 'ӣ', + 'Ӥ' => 'Ӥ', + 'ӥ' => 'ӥ', + 'Ӧ' => 'Ӧ', + 'ӧ' => 'ӧ', + 'Ӫ' => 'Ӫ', + 'ӫ' => 'ӫ', + 'Ӭ' => 'Ӭ', + 'ӭ' => 'ӭ', + 'Ӯ' => 'Ӯ', + 'ӯ' => 'ӯ', + 'Ӱ' => 'Ӱ', + 'ӱ' => 'ӱ', + 'Ӳ' => 'Ӳ', + 'ӳ' => 'ӳ', + 'Ӵ' => 'Ӵ', + 'ӵ' => 'ӵ', + 'Ӹ' => 'Ӹ', + 'ӹ' => 'ӹ', + 'آ' => 'آ', + 'أ' => 'أ', + 'ؤ' => 'ؤ', + 'إ' => 'إ', + 'ئ' => 'ئ', + 'ۀ' => 'ۀ', + 'ۂ' => 'ۂ', + 'ۓ' => 'ۓ', + 'ऩ' => 'ऩ', + 'ऱ' => 'ऱ', + 'ऴ' => 'ऴ', + 'ো' => 'ো', + 'ৌ' => 'ৌ', + 'ୈ' => 'ୈ', + 'ୋ' => 'ୋ', + 'ୌ' => 'ୌ', + 'ஔ' => 'ஔ', + 'ொ' => 'ொ', + 'ோ' => 'ோ', + 'ௌ' => 'ௌ', + 'ై' => 'ై', + 'ೀ' => 'ೀ', + 'ೇ' => 'ೇ', + 'ೈ' => 'ೈ', + 'ೊ' => 'ೊ', + 'ೋ' => 'ೋ', + 'ൊ' => 'ൊ', + 'ോ' => 'ോ', + 'ൌ' => 'ൌ', + 'ේ' => 'ේ', + 'ො' => 'ො', + 'ෝ' => 'ෝ', + 'ෞ' => 'ෞ', + 'ဦ' => 'ဦ', + 'ᬆ' => 'ᬆ', + 'ᬈ' => 'ᬈ', + 'ᬊ' => 'ᬊ', + 'ᬌ' => 'ᬌ', + 'ᬎ' => 'ᬎ', + 'ᬒ' => 'ᬒ', + 'ᬻ' => 'ᬻ', + 'ᬽ' => 'ᬽ', + 'ᭀ' => 'ᭀ', + 'ᭁ' => 'ᭁ', + 'ᭃ' => 'ᭃ', + 'Ḁ' => 'Ḁ', + 'ḁ' => 'ḁ', + 'Ḃ' => 'Ḃ', + 'ḃ' => 'ḃ', + 'Ḅ' => 'Ḅ', + 'ḅ' => 'ḅ', + 'Ḇ' => 'Ḇ', + 'ḇ' => 'ḇ', + 'Ḉ' => 'Ḉ', + 'ḉ' => 'ḉ', + 'Ḋ' => 'Ḋ', + 'ḋ' => 'ḋ', + 'Ḍ' => 'Ḍ', + 'ḍ' => 'ḍ', + 'Ḏ' => 'Ḏ', + 'ḏ' => 'ḏ', + 'Ḑ' => 'Ḑ', + 'ḑ' => 'ḑ', + 'Ḓ' => 'Ḓ', + 'ḓ' => 'ḓ', + 'Ḕ' => 'Ḕ', + 'ḕ' => 'ḕ', + 'Ḗ' => 'Ḗ', + 'ḗ' => 'ḗ', + 'Ḙ' => 'Ḙ', + 'ḙ' => 'ḙ', + 'Ḛ' => 'Ḛ', + 'ḛ' => 'ḛ', + 'Ḝ' => 'Ḝ', + 'ḝ' => 'ḝ', + 'Ḟ' => 'Ḟ', + 'ḟ' => 'ḟ', + 'Ḡ' => 'Ḡ', + 'ḡ' => 'ḡ', + 'Ḣ' => 'Ḣ', + 'ḣ' => 'ḣ', + 'Ḥ' => 'Ḥ', + 'ḥ' => 'ḥ', + 'Ḧ' => 'Ḧ', + 'ḧ' => 'ḧ', + 'Ḩ' => 'Ḩ', + 'ḩ' => 'ḩ', + 'Ḫ' => 'Ḫ', + 'ḫ' => 'ḫ', + 'Ḭ' => 'Ḭ', + 'ḭ' => 'ḭ', + 'Ḯ' => 'Ḯ', + 'ḯ' => 'ḯ', + 'Ḱ' => 'Ḱ', + 'ḱ' => 'ḱ', + 'Ḳ' => 'Ḳ', + 'ḳ' => 'ḳ', + 'Ḵ' => 'Ḵ', + 'ḵ' => 'ḵ', + 'Ḷ' => 'Ḷ', + 'ḷ' => 'ḷ', + 'Ḹ' => 'Ḹ', + 'ḹ' => 'ḹ', + 'Ḻ' => 'Ḻ', + 'ḻ' => 'ḻ', + 'Ḽ' => 'Ḽ', + 'ḽ' => 'ḽ', + 'Ḿ' => 'Ḿ', + 'ḿ' => 'ḿ', + 'Ṁ' => 'Ṁ', + 'ṁ' => 'ṁ', + 'Ṃ' => 'Ṃ', + 'ṃ' => 'ṃ', + 'Ṅ' => 'Ṅ', + 'ṅ' => 'ṅ', + 'Ṇ' => 'Ṇ', + 'ṇ' => 'ṇ', + 'Ṉ' => 'Ṉ', + 'ṉ' => 'ṉ', + 'Ṋ' => 'Ṋ', + 'ṋ' => 'ṋ', + 'Ṍ' => 'Ṍ', + 'ṍ' => 'ṍ', + 'Ṏ' => 'Ṏ', + 'ṏ' => 'ṏ', + 'Ṑ' => 'Ṑ', + 'ṑ' => 'ṑ', + 'Ṓ' => 'Ṓ', + 'ṓ' => 'ṓ', + 'Ṕ' => 'Ṕ', + 'ṕ' => 'ṕ', + 'Ṗ' => 'Ṗ', + 'ṗ' => 'ṗ', + 'Ṙ' => 'Ṙ', + 'ṙ' => 'ṙ', + 'Ṛ' => 'Ṛ', + 'ṛ' => 'ṛ', + 'Ṝ' => 'Ṝ', + 'ṝ' => 'ṝ', + 'Ṟ' => 'Ṟ', + 'ṟ' => 'ṟ', + 'Ṡ' => 'Ṡ', + 'ṡ' => 'ṡ', + 'Ṣ' => 'Ṣ', + 'ṣ' => 'ṣ', + 'Ṥ' => 'Ṥ', + 'ṥ' => 'ṥ', + 'Ṧ' => 'Ṧ', + 'ṧ' => 'ṧ', + 'Ṩ' => 'Ṩ', + 'ṩ' => 'ṩ', + 'Ṫ' => 'Ṫ', + 'ṫ' => 'ṫ', + 'Ṭ' => 'Ṭ', + 'ṭ' => 'ṭ', + 'Ṯ' => 'Ṯ', + 'ṯ' => 'ṯ', + 'Ṱ' => 'Ṱ', + 'ṱ' => 'ṱ', + 'Ṳ' => 'Ṳ', + 'ṳ' => 'ṳ', + 'Ṵ' => 'Ṵ', + 'ṵ' => 'ṵ', + 'Ṷ' => 'Ṷ', + 'ṷ' => 'ṷ', + 'Ṹ' => 'Ṹ', + 'ṹ' => 'ṹ', + 'Ṻ' => 'Ṻ', + 'ṻ' => 'ṻ', + 'Ṽ' => 'Ṽ', + 'ṽ' => 'ṽ', + 'Ṿ' => 'Ṿ', + 'ṿ' => 'ṿ', + 'Ẁ' => 'Ẁ', + 'ẁ' => 'ẁ', + 'Ẃ' => 'Ẃ', + 'ẃ' => 'ẃ', + 'Ẅ' => 'Ẅ', + 'ẅ' => 'ẅ', + 'Ẇ' => 'Ẇ', + 'ẇ' => 'ẇ', + 'Ẉ' => 'Ẉ', + 'ẉ' => 'ẉ', + 'Ẋ' => 'Ẋ', + 'ẋ' => 'ẋ', + 'Ẍ' => 'Ẍ', + 'ẍ' => 'ẍ', + 'Ẏ' => 'Ẏ', + 'ẏ' => 'ẏ', + 'Ẑ' => 'Ẑ', + 'ẑ' => 'ẑ', + 'Ẓ' => 'Ẓ', + 'ẓ' => 'ẓ', + 'Ẕ' => 'Ẕ', + 'ẕ' => 'ẕ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẛ' => 'ẛ', + 'Ạ' => 'Ạ', + 'ạ' => 'ạ', + 'Ả' => 'Ả', + 'ả' => 'ả', + 'Ấ' => 'Ấ', + 'ấ' => 'ấ', + 'Ầ' => 'Ầ', + 'ầ' => 'ầ', + 'Ẩ' => 'Ẩ', + 'ẩ' => 'ẩ', + 'Ẫ' => 'Ẫ', + 'ẫ' => 'ẫ', + 'Ậ' => 'Ậ', + 'ậ' => 'ậ', + 'Ắ' => 'Ắ', + 'ắ' => 'ắ', + 'Ằ' => 'Ằ', + 'ằ' => 'ằ', + 'Ẳ' => 'Ẳ', + 'ẳ' => 'ẳ', + 'Ẵ' => 'Ẵ', + 'ẵ' => 'ẵ', + 'Ặ' => 'Ặ', + 'ặ' => 'ặ', + 'Ẹ' => 'Ẹ', + 'ẹ' => 'ẹ', + 'Ẻ' => 'Ẻ', + 'ẻ' => 'ẻ', + 'Ẽ' => 'Ẽ', + 'ẽ' => 'ẽ', + 'Ế' => 'Ế', + 'ế' => 'ế', + 'Ề' => 'Ề', + 'ề' => 'ề', + 'Ể' => 'Ể', + 'ể' => 'ể', + 'Ễ' => 'Ễ', + 'ễ' => 'ễ', + 'Ệ' => 'Ệ', + 'ệ' => 'ệ', + 'Ỉ' => 'Ỉ', + 'ỉ' => 'ỉ', + 'Ị' => 'Ị', + 'ị' => 'ị', + 'Ọ' => 'Ọ', + 'ọ' => 'ọ', + 'Ỏ' => 'Ỏ', + 'ỏ' => 'ỏ', + 'Ố' => 'Ố', + 'ố' => 'ố', + 'Ồ' => 'Ồ', + 'ồ' => 'ồ', + 'Ổ' => 'Ổ', + 'ổ' => 'ổ', + 'Ỗ' => 'Ỗ', + 'ỗ' => 'ỗ', + 'Ộ' => 'Ộ', + 'ộ' => 'ộ', + 'Ớ' => 'Ớ', + 'ớ' => 'ớ', + 'Ờ' => 'Ờ', + 'ờ' => 'ờ', + 'Ở' => 'Ở', + 'ở' => 'ở', + 'Ỡ' => 'Ỡ', + 'ỡ' => 'ỡ', + 'Ợ' => 'Ợ', + 'ợ' => 'ợ', + 'Ụ' => 'Ụ', + 'ụ' => 'ụ', + 'Ủ' => 'Ủ', + 'ủ' => 'ủ', + 'Ứ' => 'Ứ', + 'ứ' => 'ứ', + 'Ừ' => 'Ừ', + 'ừ' => 'ừ', + 'Ử' => 'Ử', + 'ử' => 'ử', + 'Ữ' => 'Ữ', + 'ữ' => 'ữ', + 'Ự' => 'Ự', + 'ự' => 'ự', + 'Ỳ' => 'Ỳ', + 'ỳ' => 'ỳ', + 'Ỵ' => 'Ỵ', + 'ỵ' => 'ỵ', + 'Ỷ' => 'Ỷ', + 'ỷ' => 'ỷ', + 'Ỹ' => 'Ỹ', + 'ỹ' => 'ỹ', + 'ἀ' => 'ἀ', + 'ἁ' => 'ἁ', + 'ἂ' => 'ἂ', + 'ἃ' => 'ἃ', + 'ἄ' => 'ἄ', + 'ἅ' => 'ἅ', + 'ἆ' => 'ἆ', + 'ἇ' => 'ἇ', + 'Ἀ' => 'Ἀ', + 'Ἁ' => 'Ἁ', + 'Ἂ' => 'Ἂ', + 'Ἃ' => 'Ἃ', + 'Ἄ' => 'Ἄ', + 'Ἅ' => 'Ἅ', + 'Ἆ' => 'Ἆ', + 'Ἇ' => 'Ἇ', + 'ἐ' => 'ἐ', + 'ἑ' => 'ἑ', + 'ἒ' => 'ἒ', + 'ἓ' => 'ἓ', + 'ἔ' => 'ἔ', + 'ἕ' => 'ἕ', + 'Ἐ' => 'Ἐ', + 'Ἑ' => 'Ἑ', + 'Ἒ' => 'Ἒ', + 'Ἓ' => 'Ἓ', + 'Ἔ' => 'Ἔ', + 'Ἕ' => 'Ἕ', + 'ἠ' => 'ἠ', + 'ἡ' => 'ἡ', + 'ἢ' => 'ἢ', + 'ἣ' => 'ἣ', + 'ἤ' => 'ἤ', + 'ἥ' => 'ἥ', + 'ἦ' => 'ἦ', + 'ἧ' => 'ἧ', + 'Ἠ' => 'Ἠ', + 'Ἡ' => 'Ἡ', + 'Ἢ' => 'Ἢ', + 'Ἣ' => 'Ἣ', + 'Ἤ' => 'Ἤ', + 'Ἥ' => 'Ἥ', + 'Ἦ' => 'Ἦ', + 'Ἧ' => 'Ἧ', + 'ἰ' => 'ἰ', + 'ἱ' => 'ἱ', + 'ἲ' => 'ἲ', + 'ἳ' => 'ἳ', + 'ἴ' => 'ἴ', + 'ἵ' => 'ἵ', + 'ἶ' => 'ἶ', + 'ἷ' => 'ἷ', + 'Ἰ' => 'Ἰ', + 'Ἱ' => 'Ἱ', + 'Ἲ' => 'Ἲ', + 'Ἳ' => 'Ἳ', + 'Ἴ' => 'Ἴ', + 'Ἵ' => 'Ἵ', + 'Ἶ' => 'Ἶ', + 'Ἷ' => 'Ἷ', + 'ὀ' => 'ὀ', + 'ὁ' => 'ὁ', + 'ὂ' => 'ὂ', + 'ὃ' => 'ὃ', + 'ὄ' => 'ὄ', + 'ὅ' => 'ὅ', + 'Ὀ' => 'Ὀ', + 'Ὁ' => 'Ὁ', + 'Ὂ' => 'Ὂ', + 'Ὃ' => 'Ὃ', + 'Ὄ' => 'Ὄ', + 'Ὅ' => 'Ὅ', + 'ὐ' => 'ὐ', + 'ὑ' => 'ὑ', + 'ὒ' => 'ὒ', + 'ὓ' => 'ὓ', + 'ὔ' => 'ὔ', + 'ὕ' => 'ὕ', + 'ὖ' => 'ὖ', + 'ὗ' => 'ὗ', + 'Ὑ' => 'Ὑ', + 'Ὓ' => 'Ὓ', + 'Ὕ' => 'Ὕ', + 'Ὗ' => 'Ὗ', + 'ὠ' => 'ὠ', + 'ὡ' => 'ὡ', + 'ὢ' => 'ὢ', + 'ὣ' => 'ὣ', + 'ὤ' => 'ὤ', + 'ὥ' => 'ὥ', + 'ὦ' => 'ὦ', + 'ὧ' => 'ὧ', + 'Ὠ' => 'Ὠ', + 'Ὡ' => 'Ὡ', + 'Ὢ' => 'Ὢ', + 'Ὣ' => 'Ὣ', + 'Ὤ' => 'Ὤ', + 'Ὥ' => 'Ὥ', + 'Ὦ' => 'Ὦ', + 'Ὧ' => 'Ὧ', + 'ὰ' => 'ὰ', + 'ὲ' => 'ὲ', + 'ὴ' => 'ὴ', + 'ὶ' => 'ὶ', + 'ὸ' => 'ὸ', + 'ὺ' => 'ὺ', + 'ὼ' => 'ὼ', + 'ᾀ' => 'ᾀ', + 'ᾁ' => 'ᾁ', + 'ᾂ' => 'ᾂ', + 'ᾃ' => 'ᾃ', + 'ᾄ' => 'ᾄ', + 'ᾅ' => 'ᾅ', + 'ᾆ' => 'ᾆ', + 'ᾇ' => 'ᾇ', + 'ᾈ' => 'ᾈ', + 'ᾉ' => 'ᾉ', + 'ᾊ' => 'ᾊ', + 'ᾋ' => 'ᾋ', + 'ᾌ' => 'ᾌ', + 'ᾍ' => 'ᾍ', + 'ᾎ' => 'ᾎ', + 'ᾏ' => 'ᾏ', + 'ᾐ' => 'ᾐ', + 'ᾑ' => 'ᾑ', + 'ᾒ' => 'ᾒ', + 'ᾓ' => 'ᾓ', + 'ᾔ' => 'ᾔ', + 'ᾕ' => 'ᾕ', + 'ᾖ' => 'ᾖ', + 'ᾗ' => 'ᾗ', + 'ᾘ' => 'ᾘ', + 'ᾙ' => 'ᾙ', + 'ᾚ' => 'ᾚ', + 'ᾛ' => 'ᾛ', + 'ᾜ' => 'ᾜ', + 'ᾝ' => 'ᾝ', + 'ᾞ' => 'ᾞ', + 'ᾟ' => 'ᾟ', + 'ᾠ' => 'ᾠ', + 'ᾡ' => 'ᾡ', + 'ᾢ' => 'ᾢ', + 'ᾣ' => 'ᾣ', + 'ᾤ' => 'ᾤ', + 'ᾥ' => 'ᾥ', + 'ᾦ' => 'ᾦ', + 'ᾧ' => 'ᾧ', + 'ᾨ' => 'ᾨ', + 'ᾩ' => 'ᾩ', + 'ᾪ' => 'ᾪ', + 'ᾫ' => 'ᾫ', + 'ᾬ' => 'ᾬ', + 'ᾭ' => 'ᾭ', + 'ᾮ' => 'ᾮ', + 'ᾯ' => 'ᾯ', + 'ᾰ' => 'ᾰ', + 'ᾱ' => 'ᾱ', + 'ᾲ' => 'ᾲ', + 'ᾳ' => 'ᾳ', + 'ᾴ' => 'ᾴ', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾷ', + 'Ᾰ' => 'Ᾰ', + 'Ᾱ' => 'Ᾱ', + 'Ὰ' => 'Ὰ', + 'ᾼ' => 'ᾼ', + '῁' => '῁', + 'ῂ' => 'ῂ', + 'ῃ' => 'ῃ', + 'ῄ' => 'ῄ', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῇ', + 'Ὲ' => 'Ὲ', + 'Ὴ' => 'Ὴ', + 'ῌ' => 'ῌ', + '῍' => '῍', + '῎' => '῎', + '῏' => '῏', + 'ῐ' => 'ῐ', + 'ῑ' => 'ῑ', + 'ῒ' => 'ῒ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'Ῐ' => 'Ῐ', + 'Ῑ' => 'Ῑ', + 'Ὶ' => 'Ὶ', + '῝' => '῝', + '῞' => '῞', + '῟' => '῟', + 'ῠ' => 'ῠ', + 'ῡ' => 'ῡ', + 'ῢ' => 'ῢ', + 'ῤ' => 'ῤ', + 'ῥ' => 'ῥ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'Ῠ' => 'Ῠ', + 'Ῡ' => 'Ῡ', + 'Ὺ' => 'Ὺ', + 'Ῥ' => 'Ῥ', + '῭' => '῭', + 'ῲ' => 'ῲ', + 'ῳ' => 'ῳ', + 'ῴ' => 'ῴ', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῷ', + 'Ὸ' => 'Ὸ', + 'Ὼ' => 'Ὼ', + 'ῼ' => 'ῼ', + '↚' => '↚', + '↛' => '↛', + '↮' => '↮', + '⇍' => '⇍', + '⇎' => '⇎', + '⇏' => '⇏', + '∄' => '∄', + '∉' => '∉', + '∌' => '∌', + '∤' => '∤', + '∦' => '∦', + '≁' => '≁', + '≄' => '≄', + '≇' => '≇', + '≉' => '≉', + '≠' => '≠', + '≢' => '≢', + '≭' => '≭', + '≮' => '≮', + '≯' => '≯', + '≰' => '≰', + '≱' => '≱', + '≴' => '≴', + '≵' => '≵', + '≸' => '≸', + '≹' => '≹', + '⊀' => '⊀', + '⊁' => '⊁', + '⊄' => '⊄', + '⊅' => '⊅', + '⊈' => '⊈', + '⊉' => '⊉', + '⊬' => '⊬', + '⊭' => '⊭', + '⊮' => '⊮', + '⊯' => '⊯', + '⋠' => '⋠', + '⋡' => '⋡', + '⋢' => '⋢', + '⋣' => '⋣', + '⋪' => '⋪', + '⋫' => '⋫', + '⋬' => '⋬', + '⋭' => '⋭', + 'が' => 'が', + 'ぎ' => 'ぎ', + 'ぐ' => 'ぐ', + 'げ' => 'げ', + 'ご' => 'ご', + 'ざ' => 'ざ', + 'じ' => 'じ', + 'ず' => 'ず', + 'ぜ' => 'ぜ', + 'ぞ' => 'ぞ', + 'だ' => 'だ', + 'ぢ' => 'ぢ', + 'づ' => 'づ', + 'で' => 'で', + 'ど' => 'ど', + 'ば' => 'ば', + 'ぱ' => 'ぱ', + 'び' => 'び', + 'ぴ' => 'ぴ', + 'ぶ' => 'ぶ', + 'ぷ' => 'ぷ', + 'べ' => 'べ', + 'ぺ' => 'ぺ', + 'ぼ' => 'ぼ', + 'ぽ' => 'ぽ', + 'ゔ' => 'ゔ', + 'ゞ' => 'ゞ', + 'ガ' => 'ガ', + 'ギ' => 'ギ', + 'グ' => 'グ', + 'ゲ' => 'ゲ', + 'ゴ' => 'ゴ', + 'ザ' => 'ザ', + 'ジ' => 'ジ', + 'ズ' => 'ズ', + 'ゼ' => 'ゼ', + 'ゾ' => 'ゾ', + 'ダ' => 'ダ', + 'ヂ' => 'ヂ', + 'ヅ' => 'ヅ', + 'デ' => 'デ', + 'ド' => 'ド', + 'バ' => 'バ', + 'パ' => 'パ', + 'ビ' => 'ビ', + 'ピ' => 'ピ', + 'ブ' => 'ブ', + 'プ' => 'プ', + 'ベ' => 'ベ', + 'ペ' => 'ペ', + 'ボ' => 'ボ', + 'ポ' => 'ポ', + 'ヴ' => 'ヴ', + 'ヷ' => 'ヷ', + 'ヸ' => 'ヸ', + 'ヹ' => 'ヹ', + 'ヺ' => 'ヺ', + 'ヾ' => 'ヾ', + '𑂚' => '𑂚', + '𑂜' => '𑂜', + '𑂫' => '𑂫', + '𑄮' => '𑄮', + '𑄯' => '𑄯', + '𑍋' => '𑍋', + '𑍌' => '𑍌', + '𑒻' => '𑒻', + '𑒼' => '𑒼', + '𑒾' => '𑒾', + '𑖺' => '𑖺', + '𑖻' => '𑖻', + '𑤸' => '𑤸', +); diff --git a/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php b/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php new file mode 100644 index 0000000..5a3e8e0 --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php @@ -0,0 +1,2065 @@ + 'À', + 'Á' => 'Á', + 'Â' => 'Â', + 'Ã' => 'Ã', + 'Ä' => 'Ä', + 'Å' => 'Å', + 'Ç' => 'Ç', + 'È' => 'È', + 'É' => 'É', + 'Ê' => 'Ê', + 'Ë' => 'Ë', + 'Ì' => 'Ì', + 'Í' => 'Í', + 'Î' => 'Î', + 'Ï' => 'Ï', + 'Ñ' => 'Ñ', + 'Ò' => 'Ò', + 'Ó' => 'Ó', + 'Ô' => 'Ô', + 'Õ' => 'Õ', + 'Ö' => 'Ö', + 'Ù' => 'Ù', + 'Ú' => 'Ú', + 'Û' => 'Û', + 'Ü' => 'Ü', + 'Ý' => 'Ý', + 'à' => 'à', + 'á' => 'á', + 'â' => 'â', + 'ã' => 'ã', + 'ä' => 'ä', + 'å' => 'å', + 'ç' => 'ç', + 'è' => 'è', + 'é' => 'é', + 'ê' => 'ê', + 'ë' => 'ë', + 'ì' => 'ì', + 'í' => 'í', + 'î' => 'î', + 'ï' => 'ï', + 'ñ' => 'ñ', + 'ò' => 'ò', + 'ó' => 'ó', + 'ô' => 'ô', + 'õ' => 'õ', + 'ö' => 'ö', + 'ù' => 'ù', + 'ú' => 'ú', + 'û' => 'û', + 'ü' => 'ü', + 'ý' => 'ý', + 'ÿ' => 'ÿ', + 'Ā' => 'Ā', + 'ā' => 'ā', + 'Ă' => 'Ă', + 'ă' => 'ă', + 'Ą' => 'Ą', + 'ą' => 'ą', + 'Ć' => 'Ć', + 'ć' => 'ć', + 'Ĉ' => 'Ĉ', + 'ĉ' => 'ĉ', + 'Ċ' => 'Ċ', + 'ċ' => 'ċ', + 'Č' => 'Č', + 'č' => 'č', + 'Ď' => 'Ď', + 'ď' => 'ď', + 'Ē' => 'Ē', + 'ē' => 'ē', + 'Ĕ' => 'Ĕ', + 'ĕ' => 'ĕ', + 'Ė' => 'Ė', + 'ė' => 'ė', + 'Ę' => 'Ę', + 'ę' => 'ę', + 'Ě' => 'Ě', + 'ě' => 'ě', + 'Ĝ' => 'Ĝ', + 'ĝ' => 'ĝ', + 'Ğ' => 'Ğ', + 'ğ' => 'ğ', + 'Ġ' => 'Ġ', + 'ġ' => 'ġ', + 'Ģ' => 'Ģ', + 'ģ' => 'ģ', + 'Ĥ' => 'Ĥ', + 'ĥ' => 'ĥ', + 'Ĩ' => 'Ĩ', + 'ĩ' => 'ĩ', + 'Ī' => 'Ī', + 'ī' => 'ī', + 'Ĭ' => 'Ĭ', + 'ĭ' => 'ĭ', + 'Į' => 'Į', + 'į' => 'į', + 'İ' => 'İ', + 'Ĵ' => 'Ĵ', + 'ĵ' => 'ĵ', + 'Ķ' => 'Ķ', + 'ķ' => 'ķ', + 'Ĺ' => 'Ĺ', + 'ĺ' => 'ĺ', + 'Ļ' => 'Ļ', + 'ļ' => 'ļ', + 'Ľ' => 'Ľ', + 'ľ' => 'ľ', + 'Ń' => 'Ń', + 'ń' => 'ń', + 'Ņ' => 'Ņ', + 'ņ' => 'ņ', + 'Ň' => 'Ň', + 'ň' => 'ň', + 'Ō' => 'Ō', + 'ō' => 'ō', + 'Ŏ' => 'Ŏ', + 'ŏ' => 'ŏ', + 'Ő' => 'Ő', + 'ő' => 'ő', + 'Ŕ' => 'Ŕ', + 'ŕ' => 'ŕ', + 'Ŗ' => 'Ŗ', + 'ŗ' => 'ŗ', + 'Ř' => 'Ř', + 'ř' => 'ř', + 'Ś' => 'Ś', + 'ś' => 'ś', + 'Ŝ' => 'Ŝ', + 'ŝ' => 'ŝ', + 'Ş' => 'Ş', + 'ş' => 'ş', + 'Š' => 'Š', + 'š' => 'š', + 'Ţ' => 'Ţ', + 'ţ' => 'ţ', + 'Ť' => 'Ť', + 'ť' => 'ť', + 'Ũ' => 'Ũ', + 'ũ' => 'ũ', + 'Ū' => 'Ū', + 'ū' => 'ū', + 'Ŭ' => 'Ŭ', + 'ŭ' => 'ŭ', + 'Ů' => 'Ů', + 'ů' => 'ů', + 'Ű' => 'Ű', + 'ű' => 'ű', + 'Ų' => 'Ų', + 'ų' => 'ų', + 'Ŵ' => 'Ŵ', + 'ŵ' => 'ŵ', + 'Ŷ' => 'Ŷ', + 'ŷ' => 'ŷ', + 'Ÿ' => 'Ÿ', + 'Ź' => 'Ź', + 'ź' => 'ź', + 'Ż' => 'Ż', + 'ż' => 'ż', + 'Ž' => 'Ž', + 'ž' => 'ž', + 'Ơ' => 'Ơ', + 'ơ' => 'ơ', + 'Ư' => 'Ư', + 'ư' => 'ư', + 'Ǎ' => 'Ǎ', + 'ǎ' => 'ǎ', + 'Ǐ' => 'Ǐ', + 'ǐ' => 'ǐ', + 'Ǒ' => 'Ǒ', + 'ǒ' => 'ǒ', + 'Ǔ' => 'Ǔ', + 'ǔ' => 'ǔ', + 'Ǖ' => 'Ǖ', + 'ǖ' => 'ǖ', + 'Ǘ' => 'Ǘ', + 'ǘ' => 'ǘ', + 'Ǚ' => 'Ǚ', + 'ǚ' => 'ǚ', + 'Ǜ' => 'Ǜ', + 'ǜ' => 'ǜ', + 'Ǟ' => 'Ǟ', + 'ǟ' => 'ǟ', + 'Ǡ' => 'Ǡ', + 'ǡ' => 'ǡ', + 'Ǣ' => 'Ǣ', + 'ǣ' => 'ǣ', + 'Ǧ' => 'Ǧ', + 'ǧ' => 'ǧ', + 'Ǩ' => 'Ǩ', + 'ǩ' => 'ǩ', + 'Ǫ' => 'Ǫ', + 'ǫ' => 'ǫ', + 'Ǭ' => 'Ǭ', + 'ǭ' => 'ǭ', + 'Ǯ' => 'Ǯ', + 'ǯ' => 'ǯ', + 'ǰ' => 'ǰ', + 'Ǵ' => 'Ǵ', + 'ǵ' => 'ǵ', + 'Ǹ' => 'Ǹ', + 'ǹ' => 'ǹ', + 'Ǻ' => 'Ǻ', + 'ǻ' => 'ǻ', + 'Ǽ' => 'Ǽ', + 'ǽ' => 'ǽ', + 'Ǿ' => 'Ǿ', + 'ǿ' => 'ǿ', + 'Ȁ' => 'Ȁ', + 'ȁ' => 'ȁ', + 'Ȃ' => 'Ȃ', + 'ȃ' => 'ȃ', + 'Ȅ' => 'Ȅ', + 'ȅ' => 'ȅ', + 'Ȇ' => 'Ȇ', + 'ȇ' => 'ȇ', + 'Ȉ' => 'Ȉ', + 'ȉ' => 'ȉ', + 'Ȋ' => 'Ȋ', + 'ȋ' => 'ȋ', + 'Ȍ' => 'Ȍ', + 'ȍ' => 'ȍ', + 'Ȏ' => 'Ȏ', + 'ȏ' => 'ȏ', + 'Ȑ' => 'Ȑ', + 'ȑ' => 'ȑ', + 'Ȓ' => 'Ȓ', + 'ȓ' => 'ȓ', + 'Ȕ' => 'Ȕ', + 'ȕ' => 'ȕ', + 'Ȗ' => 'Ȗ', + 'ȗ' => 'ȗ', + 'Ș' => 'Ș', + 'ș' => 'ș', + 'Ț' => 'Ț', + 'ț' => 'ț', + 'Ȟ' => 'Ȟ', + 'ȟ' => 'ȟ', + 'Ȧ' => 'Ȧ', + 'ȧ' => 'ȧ', + 'Ȩ' => 'Ȩ', + 'ȩ' => 'ȩ', + 'Ȫ' => 'Ȫ', + 'ȫ' => 'ȫ', + 'Ȭ' => 'Ȭ', + 'ȭ' => 'ȭ', + 'Ȯ' => 'Ȯ', + 'ȯ' => 'ȯ', + 'Ȱ' => 'Ȱ', + 'ȱ' => 'ȱ', + 'Ȳ' => 'Ȳ', + 'ȳ' => 'ȳ', + '̀' => '̀', + '́' => '́', + '̓' => '̓', + '̈́' => '̈́', + 'ʹ' => 'ʹ', + ';' => ';', + '΅' => '΅', + 'Ά' => 'Ά', + '·' => '·', + 'Έ' => 'Έ', + 'Ή' => 'Ή', + 'Ί' => 'Ί', + 'Ό' => 'Ό', + 'Ύ' => 'Ύ', + 'Ώ' => 'Ώ', + 'ΐ' => 'ΐ', + 'Ϊ' => 'Ϊ', + 'Ϋ' => 'Ϋ', + 'ά' => 'ά', + 'έ' => 'έ', + 'ή' => 'ή', + 'ί' => 'ί', + 'ΰ' => 'ΰ', + 'ϊ' => 'ϊ', + 'ϋ' => 'ϋ', + 'ό' => 'ό', + 'ύ' => 'ύ', + 'ώ' => 'ώ', + 'ϓ' => 'ϓ', + 'ϔ' => 'ϔ', + 'Ѐ' => 'Ѐ', + 'Ё' => 'Ё', + 'Ѓ' => 'Ѓ', + 'Ї' => 'Ї', + 'Ќ' => 'Ќ', + 'Ѝ' => 'Ѝ', + 'Ў' => 'Ў', + 'Й' => 'Й', + 'й' => 'й', + 'ѐ' => 'ѐ', + 'ё' => 'ё', + 'ѓ' => 'ѓ', + 'ї' => 'ї', + 'ќ' => 'ќ', + 'ѝ' => 'ѝ', + 'ў' => 'ў', + 'Ѷ' => 'Ѷ', + 'ѷ' => 'ѷ', + 'Ӂ' => 'Ӂ', + 'ӂ' => 'ӂ', + 'Ӑ' => 'Ӑ', + 'ӑ' => 'ӑ', + 'Ӓ' => 'Ӓ', + 'ӓ' => 'ӓ', + 'Ӗ' => 'Ӗ', + 'ӗ' => 'ӗ', + 'Ӛ' => 'Ӛ', + 'ӛ' => 'ӛ', + 'Ӝ' => 'Ӝ', + 'ӝ' => 'ӝ', + 'Ӟ' => 'Ӟ', + 'ӟ' => 'ӟ', + 'Ӣ' => 'Ӣ', + 'ӣ' => 'ӣ', + 'Ӥ' => 'Ӥ', + 'ӥ' => 'ӥ', + 'Ӧ' => 'Ӧ', + 'ӧ' => 'ӧ', + 'Ӫ' => 'Ӫ', + 'ӫ' => 'ӫ', + 'Ӭ' => 'Ӭ', + 'ӭ' => 'ӭ', + 'Ӯ' => 'Ӯ', + 'ӯ' => 'ӯ', + 'Ӱ' => 'Ӱ', + 'ӱ' => 'ӱ', + 'Ӳ' => 'Ӳ', + 'ӳ' => 'ӳ', + 'Ӵ' => 'Ӵ', + 'ӵ' => 'ӵ', + 'Ӹ' => 'Ӹ', + 'ӹ' => 'ӹ', + 'آ' => 'آ', + 'أ' => 'أ', + 'ؤ' => 'ؤ', + 'إ' => 'إ', + 'ئ' => 'ئ', + 'ۀ' => 'ۀ', + 'ۂ' => 'ۂ', + 'ۓ' => 'ۓ', + 'ऩ' => 'ऩ', + 'ऱ' => 'ऱ', + 'ऴ' => 'ऴ', + 'क़' => 'क़', + 'ख़' => 'ख़', + 'ग़' => 'ग़', + 'ज़' => 'ज़', + 'ड़' => 'ड़', + 'ढ़' => 'ढ़', + 'फ़' => 'फ़', + 'य़' => 'य़', + 'ো' => 'ো', + 'ৌ' => 'ৌ', + 'ড়' => 'ড়', + 'ঢ়' => 'ঢ়', + 'য়' => 'য়', + 'ਲ਼' => 'ਲ਼', + 'ਸ਼' => 'ਸ਼', + 'ਖ਼' => 'ਖ਼', + 'ਗ਼' => 'ਗ਼', + 'ਜ਼' => 'ਜ਼', + 'ਫ਼' => 'ਫ਼', + 'ୈ' => 'ୈ', + 'ୋ' => 'ୋ', + 'ୌ' => 'ୌ', + 'ଡ଼' => 'ଡ଼', + 'ଢ଼' => 'ଢ଼', + 'ஔ' => 'ஔ', + 'ொ' => 'ொ', + 'ோ' => 'ோ', + 'ௌ' => 'ௌ', + 'ై' => 'ై', + 'ೀ' => 'ೀ', + 'ೇ' => 'ೇ', + 'ೈ' => 'ೈ', + 'ೊ' => 'ೊ', + 'ೋ' => 'ೋ', + 'ൊ' => 'ൊ', + 'ോ' => 'ോ', + 'ൌ' => 'ൌ', + 'ේ' => 'ේ', + 'ො' => 'ො', + 'ෝ' => 'ෝ', + 'ෞ' => 'ෞ', + 'གྷ' => 'གྷ', + 'ཌྷ' => 'ཌྷ', + 'དྷ' => 'དྷ', + 'བྷ' => 'བྷ', + 'ཛྷ' => 'ཛྷ', + 'ཀྵ' => 'ཀྵ', + 'ཱི' => 'ཱི', + 'ཱུ' => 'ཱུ', + 'ྲྀ' => 'ྲྀ', + 'ླྀ' => 'ླྀ', + 'ཱྀ' => 'ཱྀ', + 'ྒྷ' => 'ྒྷ', + 'ྜྷ' => 'ྜྷ', + 'ྡྷ' => 'ྡྷ', + 'ྦྷ' => 'ྦྷ', + 'ྫྷ' => 'ྫྷ', + 'ྐྵ' => 'ྐྵ', + 'ဦ' => 'ဦ', + 'ᬆ' => 'ᬆ', + 'ᬈ' => 'ᬈ', + 'ᬊ' => 'ᬊ', + 'ᬌ' => 'ᬌ', + 'ᬎ' => 'ᬎ', + 'ᬒ' => 'ᬒ', + 'ᬻ' => 'ᬻ', + 'ᬽ' => 'ᬽ', + 'ᭀ' => 'ᭀ', + 'ᭁ' => 'ᭁ', + 'ᭃ' => 'ᭃ', + 'Ḁ' => 'Ḁ', + 'ḁ' => 'ḁ', + 'Ḃ' => 'Ḃ', + 'ḃ' => 'ḃ', + 'Ḅ' => 'Ḅ', + 'ḅ' => 'ḅ', + 'Ḇ' => 'Ḇ', + 'ḇ' => 'ḇ', + 'Ḉ' => 'Ḉ', + 'ḉ' => 'ḉ', + 'Ḋ' => 'Ḋ', + 'ḋ' => 'ḋ', + 'Ḍ' => 'Ḍ', + 'ḍ' => 'ḍ', + 'Ḏ' => 'Ḏ', + 'ḏ' => 'ḏ', + 'Ḑ' => 'Ḑ', + 'ḑ' => 'ḑ', + 'Ḓ' => 'Ḓ', + 'ḓ' => 'ḓ', + 'Ḕ' => 'Ḕ', + 'ḕ' => 'ḕ', + 'Ḗ' => 'Ḗ', + 'ḗ' => 'ḗ', + 'Ḙ' => 'Ḙ', + 'ḙ' => 'ḙ', + 'Ḛ' => 'Ḛ', + 'ḛ' => 'ḛ', + 'Ḝ' => 'Ḝ', + 'ḝ' => 'ḝ', + 'Ḟ' => 'Ḟ', + 'ḟ' => 'ḟ', + 'Ḡ' => 'Ḡ', + 'ḡ' => 'ḡ', + 'Ḣ' => 'Ḣ', + 'ḣ' => 'ḣ', + 'Ḥ' => 'Ḥ', + 'ḥ' => 'ḥ', + 'Ḧ' => 'Ḧ', + 'ḧ' => 'ḧ', + 'Ḩ' => 'Ḩ', + 'ḩ' => 'ḩ', + 'Ḫ' => 'Ḫ', + 'ḫ' => 'ḫ', + 'Ḭ' => 'Ḭ', + 'ḭ' => 'ḭ', + 'Ḯ' => 'Ḯ', + 'ḯ' => 'ḯ', + 'Ḱ' => 'Ḱ', + 'ḱ' => 'ḱ', + 'Ḳ' => 'Ḳ', + 'ḳ' => 'ḳ', + 'Ḵ' => 'Ḵ', + 'ḵ' => 'ḵ', + 'Ḷ' => 'Ḷ', + 'ḷ' => 'ḷ', + 'Ḹ' => 'Ḹ', + 'ḹ' => 'ḹ', + 'Ḻ' => 'Ḻ', + 'ḻ' => 'ḻ', + 'Ḽ' => 'Ḽ', + 'ḽ' => 'ḽ', + 'Ḿ' => 'Ḿ', + 'ḿ' => 'ḿ', + 'Ṁ' => 'Ṁ', + 'ṁ' => 'ṁ', + 'Ṃ' => 'Ṃ', + 'ṃ' => 'ṃ', + 'Ṅ' => 'Ṅ', + 'ṅ' => 'ṅ', + 'Ṇ' => 'Ṇ', + 'ṇ' => 'ṇ', + 'Ṉ' => 'Ṉ', + 'ṉ' => 'ṉ', + 'Ṋ' => 'Ṋ', + 'ṋ' => 'ṋ', + 'Ṍ' => 'Ṍ', + 'ṍ' => 'ṍ', + 'Ṏ' => 'Ṏ', + 'ṏ' => 'ṏ', + 'Ṑ' => 'Ṑ', + 'ṑ' => 'ṑ', + 'Ṓ' => 'Ṓ', + 'ṓ' => 'ṓ', + 'Ṕ' => 'Ṕ', + 'ṕ' => 'ṕ', + 'Ṗ' => 'Ṗ', + 'ṗ' => 'ṗ', + 'Ṙ' => 'Ṙ', + 'ṙ' => 'ṙ', + 'Ṛ' => 'Ṛ', + 'ṛ' => 'ṛ', + 'Ṝ' => 'Ṝ', + 'ṝ' => 'ṝ', + 'Ṟ' => 'Ṟ', + 'ṟ' => 'ṟ', + 'Ṡ' => 'Ṡ', + 'ṡ' => 'ṡ', + 'Ṣ' => 'Ṣ', + 'ṣ' => 'ṣ', + 'Ṥ' => 'Ṥ', + 'ṥ' => 'ṥ', + 'Ṧ' => 'Ṧ', + 'ṧ' => 'ṧ', + 'Ṩ' => 'Ṩ', + 'ṩ' => 'ṩ', + 'Ṫ' => 'Ṫ', + 'ṫ' => 'ṫ', + 'Ṭ' => 'Ṭ', + 'ṭ' => 'ṭ', + 'Ṯ' => 'Ṯ', + 'ṯ' => 'ṯ', + 'Ṱ' => 'Ṱ', + 'ṱ' => 'ṱ', + 'Ṳ' => 'Ṳ', + 'ṳ' => 'ṳ', + 'Ṵ' => 'Ṵ', + 'ṵ' => 'ṵ', + 'Ṷ' => 'Ṷ', + 'ṷ' => 'ṷ', + 'Ṹ' => 'Ṹ', + 'ṹ' => 'ṹ', + 'Ṻ' => 'Ṻ', + 'ṻ' => 'ṻ', + 'Ṽ' => 'Ṽ', + 'ṽ' => 'ṽ', + 'Ṿ' => 'Ṿ', + 'ṿ' => 'ṿ', + 'Ẁ' => 'Ẁ', + 'ẁ' => 'ẁ', + 'Ẃ' => 'Ẃ', + 'ẃ' => 'ẃ', + 'Ẅ' => 'Ẅ', + 'ẅ' => 'ẅ', + 'Ẇ' => 'Ẇ', + 'ẇ' => 'ẇ', + 'Ẉ' => 'Ẉ', + 'ẉ' => 'ẉ', + 'Ẋ' => 'Ẋ', + 'ẋ' => 'ẋ', + 'Ẍ' => 'Ẍ', + 'ẍ' => 'ẍ', + 'Ẏ' => 'Ẏ', + 'ẏ' => 'ẏ', + 'Ẑ' => 'Ẑ', + 'ẑ' => 'ẑ', + 'Ẓ' => 'Ẓ', + 'ẓ' => 'ẓ', + 'Ẕ' => 'Ẕ', + 'ẕ' => 'ẕ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẛ' => 'ẛ', + 'Ạ' => 'Ạ', + 'ạ' => 'ạ', + 'Ả' => 'Ả', + 'ả' => 'ả', + 'Ấ' => 'Ấ', + 'ấ' => 'ấ', + 'Ầ' => 'Ầ', + 'ầ' => 'ầ', + 'Ẩ' => 'Ẩ', + 'ẩ' => 'ẩ', + 'Ẫ' => 'Ẫ', + 'ẫ' => 'ẫ', + 'Ậ' => 'Ậ', + 'ậ' => 'ậ', + 'Ắ' => 'Ắ', + 'ắ' => 'ắ', + 'Ằ' => 'Ằ', + 'ằ' => 'ằ', + 'Ẳ' => 'Ẳ', + 'ẳ' => 'ẳ', + 'Ẵ' => 'Ẵ', + 'ẵ' => 'ẵ', + 'Ặ' => 'Ặ', + 'ặ' => 'ặ', + 'Ẹ' => 'Ẹ', + 'ẹ' => 'ẹ', + 'Ẻ' => 'Ẻ', + 'ẻ' => 'ẻ', + 'Ẽ' => 'Ẽ', + 'ẽ' => 'ẽ', + 'Ế' => 'Ế', + 'ế' => 'ế', + 'Ề' => 'Ề', + 'ề' => 'ề', + 'Ể' => 'Ể', + 'ể' => 'ể', + 'Ễ' => 'Ễ', + 'ễ' => 'ễ', + 'Ệ' => 'Ệ', + 'ệ' => 'ệ', + 'Ỉ' => 'Ỉ', + 'ỉ' => 'ỉ', + 'Ị' => 'Ị', + 'ị' => 'ị', + 'Ọ' => 'Ọ', + 'ọ' => 'ọ', + 'Ỏ' => 'Ỏ', + 'ỏ' => 'ỏ', + 'Ố' => 'Ố', + 'ố' => 'ố', + 'Ồ' => 'Ồ', + 'ồ' => 'ồ', + 'Ổ' => 'Ổ', + 'ổ' => 'ổ', + 'Ỗ' => 'Ỗ', + 'ỗ' => 'ỗ', + 'Ộ' => 'Ộ', + 'ộ' => 'ộ', + 'Ớ' => 'Ớ', + 'ớ' => 'ớ', + 'Ờ' => 'Ờ', + 'ờ' => 'ờ', + 'Ở' => 'Ở', + 'ở' => 'ở', + 'Ỡ' => 'Ỡ', + 'ỡ' => 'ỡ', + 'Ợ' => 'Ợ', + 'ợ' => 'ợ', + 'Ụ' => 'Ụ', + 'ụ' => 'ụ', + 'Ủ' => 'Ủ', + 'ủ' => 'ủ', + 'Ứ' => 'Ứ', + 'ứ' => 'ứ', + 'Ừ' => 'Ừ', + 'ừ' => 'ừ', + 'Ử' => 'Ử', + 'ử' => 'ử', + 'Ữ' => 'Ữ', + 'ữ' => 'ữ', + 'Ự' => 'Ự', + 'ự' => 'ự', + 'Ỳ' => 'Ỳ', + 'ỳ' => 'ỳ', + 'Ỵ' => 'Ỵ', + 'ỵ' => 'ỵ', + 'Ỷ' => 'Ỷ', + 'ỷ' => 'ỷ', + 'Ỹ' => 'Ỹ', + 'ỹ' => 'ỹ', + 'ἀ' => 'ἀ', + 'ἁ' => 'ἁ', + 'ἂ' => 'ἂ', + 'ἃ' => 'ἃ', + 'ἄ' => 'ἄ', + 'ἅ' => 'ἅ', + 'ἆ' => 'ἆ', + 'ἇ' => 'ἇ', + 'Ἀ' => 'Ἀ', + 'Ἁ' => 'Ἁ', + 'Ἂ' => 'Ἂ', + 'Ἃ' => 'Ἃ', + 'Ἄ' => 'Ἄ', + 'Ἅ' => 'Ἅ', + 'Ἆ' => 'Ἆ', + 'Ἇ' => 'Ἇ', + 'ἐ' => 'ἐ', + 'ἑ' => 'ἑ', + 'ἒ' => 'ἒ', + 'ἓ' => 'ἓ', + 'ἔ' => 'ἔ', + 'ἕ' => 'ἕ', + 'Ἐ' => 'Ἐ', + 'Ἑ' => 'Ἑ', + 'Ἒ' => 'Ἒ', + 'Ἓ' => 'Ἓ', + 'Ἔ' => 'Ἔ', + 'Ἕ' => 'Ἕ', + 'ἠ' => 'ἠ', + 'ἡ' => 'ἡ', + 'ἢ' => 'ἢ', + 'ἣ' => 'ἣ', + 'ἤ' => 'ἤ', + 'ἥ' => 'ἥ', + 'ἦ' => 'ἦ', + 'ἧ' => 'ἧ', + 'Ἠ' => 'Ἠ', + 'Ἡ' => 'Ἡ', + 'Ἢ' => 'Ἢ', + 'Ἣ' => 'Ἣ', + 'Ἤ' => 'Ἤ', + 'Ἥ' => 'Ἥ', + 'Ἦ' => 'Ἦ', + 'Ἧ' => 'Ἧ', + 'ἰ' => 'ἰ', + 'ἱ' => 'ἱ', + 'ἲ' => 'ἲ', + 'ἳ' => 'ἳ', + 'ἴ' => 'ἴ', + 'ἵ' => 'ἵ', + 'ἶ' => 'ἶ', + 'ἷ' => 'ἷ', + 'Ἰ' => 'Ἰ', + 'Ἱ' => 'Ἱ', + 'Ἲ' => 'Ἲ', + 'Ἳ' => 'Ἳ', + 'Ἴ' => 'Ἴ', + 'Ἵ' => 'Ἵ', + 'Ἶ' => 'Ἶ', + 'Ἷ' => 'Ἷ', + 'ὀ' => 'ὀ', + 'ὁ' => 'ὁ', + 'ὂ' => 'ὂ', + 'ὃ' => 'ὃ', + 'ὄ' => 'ὄ', + 'ὅ' => 'ὅ', + 'Ὀ' => 'Ὀ', + 'Ὁ' => 'Ὁ', + 'Ὂ' => 'Ὂ', + 'Ὃ' => 'Ὃ', + 'Ὄ' => 'Ὄ', + 'Ὅ' => 'Ὅ', + 'ὐ' => 'ὐ', + 'ὑ' => 'ὑ', + 'ὒ' => 'ὒ', + 'ὓ' => 'ὓ', + 'ὔ' => 'ὔ', + 'ὕ' => 'ὕ', + 'ὖ' => 'ὖ', + 'ὗ' => 'ὗ', + 'Ὑ' => 'Ὑ', + 'Ὓ' => 'Ὓ', + 'Ὕ' => 'Ὕ', + 'Ὗ' => 'Ὗ', + 'ὠ' => 'ὠ', + 'ὡ' => 'ὡ', + 'ὢ' => 'ὢ', + 'ὣ' => 'ὣ', + 'ὤ' => 'ὤ', + 'ὥ' => 'ὥ', + 'ὦ' => 'ὦ', + 'ὧ' => 'ὧ', + 'Ὠ' => 'Ὠ', + 'Ὡ' => 'Ὡ', + 'Ὢ' => 'Ὢ', + 'Ὣ' => 'Ὣ', + 'Ὤ' => 'Ὤ', + 'Ὥ' => 'Ὥ', + 'Ὦ' => 'Ὦ', + 'Ὧ' => 'Ὧ', + 'ὰ' => 'ὰ', + 'ά' => 'ά', + 'ὲ' => 'ὲ', + 'έ' => 'έ', + 'ὴ' => 'ὴ', + 'ή' => 'ή', + 'ὶ' => 'ὶ', + 'ί' => 'ί', + 'ὸ' => 'ὸ', + 'ό' => 'ό', + 'ὺ' => 'ὺ', + 'ύ' => 'ύ', + 'ὼ' => 'ὼ', + 'ώ' => 'ώ', + 'ᾀ' => 'ᾀ', + 'ᾁ' => 'ᾁ', + 'ᾂ' => 'ᾂ', + 'ᾃ' => 'ᾃ', + 'ᾄ' => 'ᾄ', + 'ᾅ' => 'ᾅ', + 'ᾆ' => 'ᾆ', + 'ᾇ' => 'ᾇ', + 'ᾈ' => 'ᾈ', + 'ᾉ' => 'ᾉ', + 'ᾊ' => 'ᾊ', + 'ᾋ' => 'ᾋ', + 'ᾌ' => 'ᾌ', + 'ᾍ' => 'ᾍ', + 'ᾎ' => 'ᾎ', + 'ᾏ' => 'ᾏ', + 'ᾐ' => 'ᾐ', + 'ᾑ' => 'ᾑ', + 'ᾒ' => 'ᾒ', + 'ᾓ' => 'ᾓ', + 'ᾔ' => 'ᾔ', + 'ᾕ' => 'ᾕ', + 'ᾖ' => 'ᾖ', + 'ᾗ' => 'ᾗ', + 'ᾘ' => 'ᾘ', + 'ᾙ' => 'ᾙ', + 'ᾚ' => 'ᾚ', + 'ᾛ' => 'ᾛ', + 'ᾜ' => 'ᾜ', + 'ᾝ' => 'ᾝ', + 'ᾞ' => 'ᾞ', + 'ᾟ' => 'ᾟ', + 'ᾠ' => 'ᾠ', + 'ᾡ' => 'ᾡ', + 'ᾢ' => 'ᾢ', + 'ᾣ' => 'ᾣ', + 'ᾤ' => 'ᾤ', + 'ᾥ' => 'ᾥ', + 'ᾦ' => 'ᾦ', + 'ᾧ' => 'ᾧ', + 'ᾨ' => 'ᾨ', + 'ᾩ' => 'ᾩ', + 'ᾪ' => 'ᾪ', + 'ᾫ' => 'ᾫ', + 'ᾬ' => 'ᾬ', + 'ᾭ' => 'ᾭ', + 'ᾮ' => 'ᾮ', + 'ᾯ' => 'ᾯ', + 'ᾰ' => 'ᾰ', + 'ᾱ' => 'ᾱ', + 'ᾲ' => 'ᾲ', + 'ᾳ' => 'ᾳ', + 'ᾴ' => 'ᾴ', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾷ', + 'Ᾰ' => 'Ᾰ', + 'Ᾱ' => 'Ᾱ', + 'Ὰ' => 'Ὰ', + 'Ά' => 'Ά', + 'ᾼ' => 'ᾼ', + 'ι' => 'ι', + '῁' => '῁', + 'ῂ' => 'ῂ', + 'ῃ' => 'ῃ', + 'ῄ' => 'ῄ', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῇ', + 'Ὲ' => 'Ὲ', + 'Έ' => 'Έ', + 'Ὴ' => 'Ὴ', + 'Ή' => 'Ή', + 'ῌ' => 'ῌ', + '῍' => '῍', + '῎' => '῎', + '῏' => '῏', + 'ῐ' => 'ῐ', + 'ῑ' => 'ῑ', + 'ῒ' => 'ῒ', + 'ΐ' => 'ΐ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'Ῐ' => 'Ῐ', + 'Ῑ' => 'Ῑ', + 'Ὶ' => 'Ὶ', + 'Ί' => 'Ί', + '῝' => '῝', + '῞' => '῞', + '῟' => '῟', + 'ῠ' => 'ῠ', + 'ῡ' => 'ῡ', + 'ῢ' => 'ῢ', + 'ΰ' => 'ΰ', + 'ῤ' => 'ῤ', + 'ῥ' => 'ῥ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'Ῠ' => 'Ῠ', + 'Ῡ' => 'Ῡ', + 'Ὺ' => 'Ὺ', + 'Ύ' => 'Ύ', + 'Ῥ' => 'Ῥ', + '῭' => '῭', + '΅' => '΅', + '`' => '`', + 'ῲ' => 'ῲ', + 'ῳ' => 'ῳ', + 'ῴ' => 'ῴ', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῷ', + 'Ὸ' => 'Ὸ', + 'Ό' => 'Ό', + 'Ὼ' => 'Ὼ', + 'Ώ' => 'Ώ', + 'ῼ' => 'ῼ', + '´' => '´', + ' ' => ' ', + ' ' => ' ', + 'Ω' => 'Ω', + 'K' => 'K', + 'Å' => 'Å', + '↚' => '↚', + '↛' => '↛', + '↮' => '↮', + '⇍' => '⇍', + '⇎' => '⇎', + '⇏' => '⇏', + '∄' => '∄', + '∉' => '∉', + '∌' => '∌', + '∤' => '∤', + '∦' => '∦', + '≁' => '≁', + '≄' => '≄', + '≇' => '≇', + '≉' => '≉', + '≠' => '≠', + '≢' => '≢', + '≭' => '≭', + '≮' => '≮', + '≯' => '≯', + '≰' => '≰', + '≱' => '≱', + '≴' => '≴', + '≵' => '≵', + '≸' => '≸', + '≹' => '≹', + '⊀' => '⊀', + '⊁' => '⊁', + '⊄' => '⊄', + '⊅' => '⊅', + '⊈' => '⊈', + '⊉' => '⊉', + '⊬' => '⊬', + '⊭' => '⊭', + '⊮' => '⊮', + '⊯' => '⊯', + '⋠' => '⋠', + '⋡' => '⋡', + '⋢' => '⋢', + '⋣' => '⋣', + '⋪' => '⋪', + '⋫' => '⋫', + '⋬' => '⋬', + '⋭' => '⋭', + '〈' => '〈', + '〉' => '〉', + '⫝̸' => '⫝̸', + 'が' => 'が', + 'ぎ' => 'ぎ', + 'ぐ' => 'ぐ', + 'げ' => 'げ', + 'ご' => 'ご', + 'ざ' => 'ざ', + 'じ' => 'じ', + 'ず' => 'ず', + 'ぜ' => 'ぜ', + 'ぞ' => 'ぞ', + 'だ' => 'だ', + 'ぢ' => 'ぢ', + 'づ' => 'づ', + 'で' => 'で', + 'ど' => 'ど', + 'ば' => 'ば', + 'ぱ' => 'ぱ', + 'び' => 'び', + 'ぴ' => 'ぴ', + 'ぶ' => 'ぶ', + 'ぷ' => 'ぷ', + 'べ' => 'べ', + 'ぺ' => 'ぺ', + 'ぼ' => 'ぼ', + 'ぽ' => 'ぽ', + 'ゔ' => 'ゔ', + 'ゞ' => 'ゞ', + 'ガ' => 'ガ', + 'ギ' => 'ギ', + 'グ' => 'グ', + 'ゲ' => 'ゲ', + 'ゴ' => 'ゴ', + 'ザ' => 'ザ', + 'ジ' => 'ジ', + 'ズ' => 'ズ', + 'ゼ' => 'ゼ', + 'ゾ' => 'ゾ', + 'ダ' => 'ダ', + 'ヂ' => 'ヂ', + 'ヅ' => 'ヅ', + 'デ' => 'デ', + 'ド' => 'ド', + 'バ' => 'バ', + 'パ' => 'パ', + 'ビ' => 'ビ', + 'ピ' => 'ピ', + 'ブ' => 'ブ', + 'プ' => 'プ', + 'ベ' => 'ベ', + 'ペ' => 'ペ', + 'ボ' => 'ボ', + 'ポ' => 'ポ', + 'ヴ' => 'ヴ', + 'ヷ' => 'ヷ', + 'ヸ' => 'ヸ', + 'ヹ' => 'ヹ', + 'ヺ' => 'ヺ', + 'ヾ' => 'ヾ', + '豈' => '豈', + '更' => '更', + '車' => '車', + '賈' => '賈', + '滑' => '滑', + '串' => '串', + '句' => '句', + '龜' => '龜', + '龜' => '龜', + '契' => '契', + '金' => '金', + '喇' => '喇', + '奈' => '奈', + '懶' => '懶', + '癩' => '癩', + '羅' => '羅', + '蘿' => '蘿', + '螺' => '螺', + '裸' => '裸', + '邏' => '邏', + '樂' => '樂', + '洛' => '洛', + '烙' => '烙', + '珞' => '珞', + '落' => '落', + '酪' => '酪', + '駱' => '駱', + '亂' => '亂', + '卵' => '卵', + '欄' => '欄', + '爛' => '爛', + '蘭' => '蘭', + '鸞' => '鸞', + '嵐' => '嵐', + '濫' => '濫', + '藍' => '藍', + '襤' => '襤', + '拉' => '拉', + '臘' => '臘', + '蠟' => '蠟', + '廊' => '廊', + '朗' => '朗', + '浪' => '浪', + '狼' => '狼', + '郎' => '郎', + '來' => '來', + '冷' => '冷', + '勞' => '勞', + '擄' => '擄', + '櫓' => '櫓', + '爐' => '爐', + '盧' => '盧', + '老' => '老', + '蘆' => '蘆', + '虜' => '虜', + '路' => '路', + '露' => '露', + '魯' => '魯', + '鷺' => '鷺', + '碌' => '碌', + '祿' => '祿', + '綠' => '綠', + '菉' => '菉', + '錄' => '錄', + '鹿' => '鹿', + '論' => '論', + '壟' => '壟', + '弄' => '弄', + '籠' => '籠', + '聾' => '聾', + '牢' => '牢', + '磊' => '磊', + '賂' => '賂', + '雷' => '雷', + '壘' => '壘', + '屢' => '屢', + '樓' => '樓', + '淚' => '淚', + '漏' => '漏', + '累' => '累', + '縷' => '縷', + '陋' => '陋', + '勒' => '勒', + '肋' => '肋', + '凜' => '凜', + '凌' => '凌', + '稜' => '稜', + '綾' => '綾', + '菱' => '菱', + '陵' => '陵', + '讀' => '讀', + '拏' => '拏', + '樂' => '樂', + '諾' => '諾', + '丹' => '丹', + '寧' => '寧', + '怒' => '怒', + '率' => '率', + '異' => '異', + '北' => '北', + '磻' => '磻', + '便' => '便', + '復' => '復', + '不' => '不', + '泌' => '泌', + '數' => '數', + '索' => '索', + '參' => '參', + '塞' => '塞', + '省' => '省', + '葉' => '葉', + '說' => '說', + '殺' => '殺', + '辰' => '辰', + '沈' => '沈', + '拾' => '拾', + '若' => '若', + '掠' => '掠', + '略' => '略', + '亮' => '亮', + '兩' => '兩', + '凉' => '凉', + '梁' => '梁', + '糧' => '糧', + '良' => '良', + '諒' => '諒', + '量' => '量', + '勵' => '勵', + '呂' => '呂', + '女' => '女', + '廬' => '廬', + '旅' => '旅', + '濾' => '濾', + '礪' => '礪', + '閭' => '閭', + '驪' => '驪', + '麗' => '麗', + '黎' => '黎', + '力' => '力', + '曆' => '曆', + '歷' => '歷', + '轢' => '轢', + '年' => '年', + '憐' => '憐', + '戀' => '戀', + '撚' => '撚', + '漣' => '漣', + '煉' => '煉', + '璉' => '璉', + '秊' => '秊', + '練' => '練', + '聯' => '聯', + '輦' => '輦', + '蓮' => '蓮', + '連' => '連', + '鍊' => '鍊', + '列' => '列', + '劣' => '劣', + '咽' => '咽', + '烈' => '烈', + '裂' => '裂', + '說' => '說', + '廉' => '廉', + '念' => '念', + '捻' => '捻', + '殮' => '殮', + '簾' => '簾', + '獵' => '獵', + '令' => '令', + '囹' => '囹', + '寧' => '寧', + '嶺' => '嶺', + '怜' => '怜', + '玲' => '玲', + '瑩' => '瑩', + '羚' => '羚', + '聆' => '聆', + '鈴' => '鈴', + '零' => '零', + '靈' => '靈', + '領' => '領', + '例' => '例', + '禮' => '禮', + '醴' => '醴', + '隸' => '隸', + '惡' => '惡', + '了' => '了', + '僚' => '僚', + '寮' => '寮', + '尿' => '尿', + '料' => '料', + '樂' => '樂', + '燎' => '燎', + '療' => '療', + '蓼' => '蓼', + '遼' => '遼', + '龍' => '龍', + '暈' => '暈', + '阮' => '阮', + '劉' => '劉', + '杻' => '杻', + '柳' => '柳', + '流' => '流', + '溜' => '溜', + '琉' => '琉', + '留' => '留', + '硫' => '硫', + '紐' => '紐', + '類' => '類', + '六' => '六', + '戮' => '戮', + '陸' => '陸', + '倫' => '倫', + '崙' => '崙', + '淪' => '淪', + '輪' => '輪', + '律' => '律', + '慄' => '慄', + '栗' => '栗', + '率' => '率', + '隆' => '隆', + '利' => '利', + '吏' => '吏', + '履' => '履', + '易' => '易', + '李' => '李', + '梨' => '梨', + '泥' => '泥', + '理' => '理', + '痢' => '痢', + '罹' => '罹', + '裏' => '裏', + '裡' => '裡', + '里' => '里', + '離' => '離', + '匿' => '匿', + '溺' => '溺', + '吝' => '吝', + '燐' => '燐', + '璘' => '璘', + '藺' => '藺', + '隣' => '隣', + '鱗' => '鱗', + '麟' => '麟', + '林' => '林', + '淋' => '淋', + '臨' => '臨', + '立' => '立', + '笠' => '笠', + '粒' => '粒', + '狀' => '狀', + '炙' => '炙', + '識' => '識', + '什' => '什', + '茶' => '茶', + '刺' => '刺', + '切' => '切', + '度' => '度', + '拓' => '拓', + '糖' => '糖', + '宅' => '宅', + '洞' => '洞', + '暴' => '暴', + '輻' => '輻', + '行' => '行', + '降' => '降', + '見' => '見', + '廓' => '廓', + '兀' => '兀', + '嗀' => '嗀', + '塚' => '塚', + '晴' => '晴', + '凞' => '凞', + '猪' => '猪', + '益' => '益', + '礼' => '礼', + '神' => '神', + '祥' => '祥', + '福' => '福', + '靖' => '靖', + '精' => '精', + '羽' => '羽', + '蘒' => '蘒', + '諸' => '諸', + '逸' => '逸', + '都' => '都', + '飯' => '飯', + '飼' => '飼', + '館' => '館', + '鶴' => '鶴', + '郞' => '郞', + '隷' => '隷', + '侮' => '侮', + '僧' => '僧', + '免' => '免', + '勉' => '勉', + '勤' => '勤', + '卑' => '卑', + '喝' => '喝', + '嘆' => '嘆', + '器' => '器', + '塀' => '塀', + '墨' => '墨', + '層' => '層', + '屮' => '屮', + '悔' => '悔', + '慨' => '慨', + '憎' => '憎', + '懲' => '懲', + '敏' => '敏', + '既' => '既', + '暑' => '暑', + '梅' => '梅', + '海' => '海', + '渚' => '渚', + '漢' => '漢', + '煮' => '煮', + '爫' => '爫', + '琢' => '琢', + '碑' => '碑', + '社' => '社', + '祉' => '祉', + '祈' => '祈', + '祐' => '祐', + '祖' => '祖', + '祝' => '祝', + '禍' => '禍', + '禎' => '禎', + '穀' => '穀', + '突' => '突', + '節' => '節', + '練' => '練', + '縉' => '縉', + '繁' => '繁', + '署' => '署', + '者' => '者', + '臭' => '臭', + '艹' => '艹', + '艹' => '艹', + '著' => '著', + '褐' => '褐', + '視' => '視', + '謁' => '謁', + '謹' => '謹', + '賓' => '賓', + '贈' => '贈', + '辶' => '辶', + '逸' => '逸', + '難' => '難', + '響' => '響', + '頻' => '頻', + '恵' => '恵', + '𤋮' => '𤋮', + '舘' => '舘', + '並' => '並', + '况' => '况', + '全' => '全', + '侀' => '侀', + '充' => '充', + '冀' => '冀', + '勇' => '勇', + '勺' => '勺', + '喝' => '喝', + '啕' => '啕', + '喙' => '喙', + '嗢' => '嗢', + '塚' => '塚', + '墳' => '墳', + '奄' => '奄', + '奔' => '奔', + '婢' => '婢', + '嬨' => '嬨', + '廒' => '廒', + '廙' => '廙', + '彩' => '彩', + '徭' => '徭', + '惘' => '惘', + '慎' => '慎', + '愈' => '愈', + '憎' => '憎', + '慠' => '慠', + '懲' => '懲', + '戴' => '戴', + '揄' => '揄', + '搜' => '搜', + '摒' => '摒', + '敖' => '敖', + '晴' => '晴', + '朗' => '朗', + '望' => '望', + '杖' => '杖', + '歹' => '歹', + '殺' => '殺', + '流' => '流', + '滛' => '滛', + '滋' => '滋', + '漢' => '漢', + '瀞' => '瀞', + '煮' => '煮', + '瞧' => '瞧', + '爵' => '爵', + '犯' => '犯', + '猪' => '猪', + '瑱' => '瑱', + '甆' => '甆', + '画' => '画', + '瘝' => '瘝', + '瘟' => '瘟', + '益' => '益', + '盛' => '盛', + '直' => '直', + '睊' => '睊', + '着' => '着', + '磌' => '磌', + '窱' => '窱', + '節' => '節', + '类' => '类', + '絛' => '絛', + '練' => '練', + '缾' => '缾', + '者' => '者', + '荒' => '荒', + '華' => '華', + '蝹' => '蝹', + '襁' => '襁', + '覆' => '覆', + '視' => '視', + '調' => '調', + '諸' => '諸', + '請' => '請', + '謁' => '謁', + '諾' => '諾', + '諭' => '諭', + '謹' => '謹', + '變' => '變', + '贈' => '贈', + '輸' => '輸', + '遲' => '遲', + '醙' => '醙', + '鉶' => '鉶', + '陼' => '陼', + '難' => '難', + '靖' => '靖', + '韛' => '韛', + '響' => '響', + '頋' => '頋', + '頻' => '頻', + '鬒' => '鬒', + '龜' => '龜', + '𢡊' => '𢡊', + '𢡄' => '𢡄', + '𣏕' => '𣏕', + '㮝' => '㮝', + '䀘' => '䀘', + '䀹' => '䀹', + '𥉉' => '𥉉', + '𥳐' => '𥳐', + '𧻓' => '𧻓', + '齃' => '齃', + '龎' => '龎', + 'יִ' => 'יִ', + 'ײַ' => 'ײַ', + 'שׁ' => 'שׁ', + 'שׂ' => 'שׂ', + 'שּׁ' => 'שּׁ', + 'שּׂ' => 'שּׂ', + 'אַ' => 'אַ', + 'אָ' => 'אָ', + 'אּ' => 'אּ', + 'בּ' => 'בּ', + 'גּ' => 'גּ', + 'דּ' => 'דּ', + 'הּ' => 'הּ', + 'וּ' => 'וּ', + 'זּ' => 'זּ', + 'טּ' => 'טּ', + 'יּ' => 'יּ', + 'ךּ' => 'ךּ', + 'כּ' => 'כּ', + 'לּ' => 'לּ', + 'מּ' => 'מּ', + 'נּ' => 'נּ', + 'סּ' => 'סּ', + 'ףּ' => 'ףּ', + 'פּ' => 'פּ', + 'צּ' => 'צּ', + 'קּ' => 'קּ', + 'רּ' => 'רּ', + 'שּ' => 'שּ', + 'תּ' => 'תּ', + 'וֹ' => 'וֹ', + 'בֿ' => 'בֿ', + 'כֿ' => 'כֿ', + 'פֿ' => 'פֿ', + '𑂚' => '𑂚', + '𑂜' => '𑂜', + '𑂫' => '𑂫', + '𑄮' => '𑄮', + '𑄯' => '𑄯', + '𑍋' => '𑍋', + '𑍌' => '𑍌', + '𑒻' => '𑒻', + '𑒼' => '𑒼', + '𑒾' => '𑒾', + '𑖺' => '𑖺', + '𑖻' => '𑖻', + '𑤸' => '𑤸', + '𝅗𝅥' => '𝅗𝅥', + '𝅘𝅥' => '𝅘𝅥', + '𝅘𝅥𝅮' => '𝅘𝅥𝅮', + '𝅘𝅥𝅯' => '𝅘𝅥𝅯', + '𝅘𝅥𝅰' => '𝅘𝅥𝅰', + '𝅘𝅥𝅱' => '𝅘𝅥𝅱', + '𝅘𝅥𝅲' => '𝅘𝅥𝅲', + '𝆹𝅥' => '𝆹𝅥', + '𝆺𝅥' => '𝆺𝅥', + '𝆹𝅥𝅮' => '𝆹𝅥𝅮', + '𝆺𝅥𝅮' => '𝆺𝅥𝅮', + '𝆹𝅥𝅯' => '𝆹𝅥𝅯', + '𝆺𝅥𝅯' => '𝆺𝅥𝅯', + '丽' => '丽', + '丸' => '丸', + '乁' => '乁', + '𠄢' => '𠄢', + '你' => '你', + '侮' => '侮', + '侻' => '侻', + '倂' => '倂', + '偺' => '偺', + '備' => '備', + '僧' => '僧', + '像' => '像', + '㒞' => '㒞', + '𠘺' => '𠘺', + '免' => '免', + '兔' => '兔', + '兤' => '兤', + '具' => '具', + '𠔜' => '𠔜', + '㒹' => '㒹', + '內' => '內', + '再' => '再', + '𠕋' => '𠕋', + '冗' => '冗', + '冤' => '冤', + '仌' => '仌', + '冬' => '冬', + '况' => '况', + '𩇟' => '𩇟', + '凵' => '凵', + '刃' => '刃', + '㓟' => '㓟', + '刻' => '刻', + '剆' => '剆', + '割' => '割', + '剷' => '剷', + '㔕' => '㔕', + '勇' => '勇', + '勉' => '勉', + '勤' => '勤', + '勺' => '勺', + '包' => '包', + '匆' => '匆', + '北' => '北', + '卉' => '卉', + '卑' => '卑', + '博' => '博', + '即' => '即', + '卽' => '卽', + '卿' => '卿', + '卿' => '卿', + '卿' => '卿', + '𠨬' => '𠨬', + '灰' => '灰', + '及' => '及', + '叟' => '叟', + '𠭣' => '𠭣', + '叫' => '叫', + '叱' => '叱', + '吆' => '吆', + '咞' => '咞', + '吸' => '吸', + '呈' => '呈', + '周' => '周', + '咢' => '咢', + '哶' => '哶', + '唐' => '唐', + '啓' => '啓', + '啣' => '啣', + '善' => '善', + '善' => '善', + '喙' => '喙', + '喫' => '喫', + '喳' => '喳', + '嗂' => '嗂', + '圖' => '圖', + '嘆' => '嘆', + '圗' => '圗', + '噑' => '噑', + '噴' => '噴', + '切' => '切', + '壮' => '壮', + '城' => '城', + '埴' => '埴', + '堍' => '堍', + '型' => '型', + '堲' => '堲', + '報' => '報', + '墬' => '墬', + '𡓤' => '𡓤', + '売' => '売', + '壷' => '壷', + '夆' => '夆', + '多' => '多', + '夢' => '夢', + '奢' => '奢', + '𡚨' => '𡚨', + '𡛪' => '𡛪', + '姬' => '姬', + '娛' => '娛', + '娧' => '娧', + '姘' => '姘', + '婦' => '婦', + '㛮' => '㛮', + '㛼' => '㛼', + '嬈' => '嬈', + '嬾' => '嬾', + '嬾' => '嬾', + '𡧈' => '𡧈', + '寃' => '寃', + '寘' => '寘', + '寧' => '寧', + '寳' => '寳', + '𡬘' => '𡬘', + '寿' => '寿', + '将' => '将', + '当' => '当', + '尢' => '尢', + '㞁' => '㞁', + '屠' => '屠', + '屮' => '屮', + '峀' => '峀', + '岍' => '岍', + '𡷤' => '𡷤', + '嵃' => '嵃', + '𡷦' => '𡷦', + '嵮' => '嵮', + '嵫' => '嵫', + '嵼' => '嵼', + '巡' => '巡', + '巢' => '巢', + '㠯' => '㠯', + '巽' => '巽', + '帨' => '帨', + '帽' => '帽', + '幩' => '幩', + '㡢' => '㡢', + '𢆃' => '𢆃', + '㡼' => '㡼', + '庰' => '庰', + '庳' => '庳', + '庶' => '庶', + '廊' => '廊', + '𪎒' => '𪎒', + '廾' => '廾', + '𢌱' => '𢌱', + '𢌱' => '𢌱', + '舁' => '舁', + '弢' => '弢', + '弢' => '弢', + '㣇' => '㣇', + '𣊸' => '𣊸', + '𦇚' => '𦇚', + '形' => '形', + '彫' => '彫', + '㣣' => '㣣', + '徚' => '徚', + '忍' => '忍', + '志' => '志', + '忹' => '忹', + '悁' => '悁', + '㤺' => '㤺', + '㤜' => '㤜', + '悔' => '悔', + '𢛔' => '𢛔', + '惇' => '惇', + '慈' => '慈', + '慌' => '慌', + '慎' => '慎', + '慌' => '慌', + '慺' => '慺', + '憎' => '憎', + '憲' => '憲', + '憤' => '憤', + '憯' => '憯', + '懞' => '懞', + '懲' => '懲', + '懶' => '懶', + '成' => '成', + '戛' => '戛', + '扝' => '扝', + '抱' => '抱', + '拔' => '拔', + '捐' => '捐', + '𢬌' => '𢬌', + '挽' => '挽', + '拼' => '拼', + '捨' => '捨', + '掃' => '掃', + '揤' => '揤', + '𢯱' => '𢯱', + '搢' => '搢', + '揅' => '揅', + '掩' => '掩', + '㨮' => '㨮', + '摩' => '摩', + '摾' => '摾', + '撝' => '撝', + '摷' => '摷', + '㩬' => '㩬', + '敏' => '敏', + '敬' => '敬', + '𣀊' => '𣀊', + '旣' => '旣', + '書' => '書', + '晉' => '晉', + '㬙' => '㬙', + '暑' => '暑', + '㬈' => '㬈', + '㫤' => '㫤', + '冒' => '冒', + '冕' => '冕', + '最' => '最', + '暜' => '暜', + '肭' => '肭', + '䏙' => '䏙', + '朗' => '朗', + '望' => '望', + '朡' => '朡', + '杞' => '杞', + '杓' => '杓', + '𣏃' => '𣏃', + '㭉' => '㭉', + '柺' => '柺', + '枅' => '枅', + '桒' => '桒', + '梅' => '梅', + '𣑭' => '𣑭', + '梎' => '梎', + '栟' => '栟', + '椔' => '椔', + '㮝' => '㮝', + '楂' => '楂', + '榣' => '榣', + '槪' => '槪', + '檨' => '檨', + '𣚣' => '𣚣', + '櫛' => '櫛', + '㰘' => '㰘', + '次' => '次', + '𣢧' => '𣢧', + '歔' => '歔', + '㱎' => '㱎', + '歲' => '歲', + '殟' => '殟', + '殺' => '殺', + '殻' => '殻', + '𣪍' => '𣪍', + '𡴋' => '𡴋', + '𣫺' => '𣫺', + '汎' => '汎', + '𣲼' => '𣲼', + '沿' => '沿', + '泍' => '泍', + '汧' => '汧', + '洖' => '洖', + '派' => '派', + '海' => '海', + '流' => '流', + '浩' => '浩', + '浸' => '浸', + '涅' => '涅', + '𣴞' => '𣴞', + '洴' => '洴', + '港' => '港', + '湮' => '湮', + '㴳' => '㴳', + '滋' => '滋', + '滇' => '滇', + '𣻑' => '𣻑', + '淹' => '淹', + '潮' => '潮', + '𣽞' => '𣽞', + '𣾎' => '𣾎', + '濆' => '濆', + '瀹' => '瀹', + '瀞' => '瀞', + '瀛' => '瀛', + '㶖' => '㶖', + '灊' => '灊', + '災' => '災', + '灷' => '灷', + '炭' => '炭', + '𠔥' => '𠔥', + '煅' => '煅', + '𤉣' => '𤉣', + '熜' => '熜', + '𤎫' => '𤎫', + '爨' => '爨', + '爵' => '爵', + '牐' => '牐', + '𤘈' => '𤘈', + '犀' => '犀', + '犕' => '犕', + '𤜵' => '𤜵', + '𤠔' => '𤠔', + '獺' => '獺', + '王' => '王', + '㺬' => '㺬', + '玥' => '玥', + '㺸' => '㺸', + '㺸' => '㺸', + '瑇' => '瑇', + '瑜' => '瑜', + '瑱' => '瑱', + '璅' => '璅', + '瓊' => '瓊', + '㼛' => '㼛', + '甤' => '甤', + '𤰶' => '𤰶', + '甾' => '甾', + '𤲒' => '𤲒', + '異' => '異', + '𢆟' => '𢆟', + '瘐' => '瘐', + '𤾡' => '𤾡', + '𤾸' => '𤾸', + '𥁄' => '𥁄', + '㿼' => '㿼', + '䀈' => '䀈', + '直' => '直', + '𥃳' => '𥃳', + '𥃲' => '𥃲', + '𥄙' => '𥄙', + '𥄳' => '𥄳', + '眞' => '眞', + '真' => '真', + '真' => '真', + '睊' => '睊', + '䀹' => '䀹', + '瞋' => '瞋', + '䁆' => '䁆', + '䂖' => '䂖', + '𥐝' => '𥐝', + '硎' => '硎', + '碌' => '碌', + '磌' => '磌', + '䃣' => '䃣', + '𥘦' => '𥘦', + '祖' => '祖', + '𥚚' => '𥚚', + '𥛅' => '𥛅', + '福' => '福', + '秫' => '秫', + '䄯' => '䄯', + '穀' => '穀', + '穊' => '穊', + '穏' => '穏', + '𥥼' => '𥥼', + '𥪧' => '𥪧', + '𥪧' => '𥪧', + '竮' => '竮', + '䈂' => '䈂', + '𥮫' => '𥮫', + '篆' => '篆', + '築' => '築', + '䈧' => '䈧', + '𥲀' => '𥲀', + '糒' => '糒', + '䊠' => '䊠', + '糨' => '糨', + '糣' => '糣', + '紀' => '紀', + '𥾆' => '𥾆', + '絣' => '絣', + '䌁' => '䌁', + '緇' => '緇', + '縂' => '縂', + '繅' => '繅', + '䌴' => '䌴', + '𦈨' => '𦈨', + '𦉇' => '𦉇', + '䍙' => '䍙', + '𦋙' => '𦋙', + '罺' => '罺', + '𦌾' => '𦌾', + '羕' => '羕', + '翺' => '翺', + '者' => '者', + '𦓚' => '𦓚', + '𦔣' => '𦔣', + '聠' => '聠', + '𦖨' => '𦖨', + '聰' => '聰', + '𣍟' => '𣍟', + '䏕' => '䏕', + '育' => '育', + '脃' => '脃', + '䐋' => '䐋', + '脾' => '脾', + '媵' => '媵', + '𦞧' => '𦞧', + '𦞵' => '𦞵', + '𣎓' => '𣎓', + '𣎜' => '𣎜', + '舁' => '舁', + '舄' => '舄', + '辞' => '辞', + '䑫' => '䑫', + '芑' => '芑', + '芋' => '芋', + '芝' => '芝', + '劳' => '劳', + '花' => '花', + '芳' => '芳', + '芽' => '芽', + '苦' => '苦', + '𦬼' => '𦬼', + '若' => '若', + '茝' => '茝', + '荣' => '荣', + '莭' => '莭', + '茣' => '茣', + '莽' => '莽', + '菧' => '菧', + '著' => '著', + '荓' => '荓', + '菊' => '菊', + '菌' => '菌', + '菜' => '菜', + '𦰶' => '𦰶', + '𦵫' => '𦵫', + '𦳕' => '𦳕', + '䔫' => '䔫', + '蓱' => '蓱', + '蓳' => '蓳', + '蔖' => '蔖', + '𧏊' => '𧏊', + '蕤' => '蕤', + '𦼬' => '𦼬', + '䕝' => '䕝', + '䕡' => '䕡', + '𦾱' => '𦾱', + '𧃒' => '𧃒', + '䕫' => '䕫', + '虐' => '虐', + '虜' => '虜', + '虧' => '虧', + '虩' => '虩', + '蚩' => '蚩', + '蚈' => '蚈', + '蜎' => '蜎', + '蛢' => '蛢', + '蝹' => '蝹', + '蜨' => '蜨', + '蝫' => '蝫', + '螆' => '螆', + '䗗' => '䗗', + '蟡' => '蟡', + '蠁' => '蠁', + '䗹' => '䗹', + '衠' => '衠', + '衣' => '衣', + '𧙧' => '𧙧', + '裗' => '裗', + '裞' => '裞', + '䘵' => '䘵', + '裺' => '裺', + '㒻' => '㒻', + '𧢮' => '𧢮', + '𧥦' => '𧥦', + '䚾' => '䚾', + '䛇' => '䛇', + '誠' => '誠', + '諭' => '諭', + '變' => '變', + '豕' => '豕', + '𧲨' => '𧲨', + '貫' => '貫', + '賁' => '賁', + '贛' => '贛', + '起' => '起', + '𧼯' => '𧼯', + '𠠄' => '𠠄', + '跋' => '跋', + '趼' => '趼', + '跰' => '跰', + '𠣞' => '𠣞', + '軔' => '軔', + '輸' => '輸', + '𨗒' => '𨗒', + '𨗭' => '𨗭', + '邔' => '邔', + '郱' => '郱', + '鄑' => '鄑', + '𨜮' => '𨜮', + '鄛' => '鄛', + '鈸' => '鈸', + '鋗' => '鋗', + '鋘' => '鋘', + '鉼' => '鉼', + '鏹' => '鏹', + '鐕' => '鐕', + '𨯺' => '𨯺', + '開' => '開', + '䦕' => '䦕', + '閷' => '閷', + '𨵷' => '𨵷', + '䧦' => '䧦', + '雃' => '雃', + '嶲' => '嶲', + '霣' => '霣', + '𩅅' => '𩅅', + '𩈚' => '𩈚', + '䩮' => '䩮', + '䩶' => '䩶', + '韠' => '韠', + '𩐊' => '𩐊', + '䪲' => '䪲', + '𩒖' => '𩒖', + '頋' => '頋', + '頋' => '頋', + '頩' => '頩', + '𩖶' => '𩖶', + '飢' => '飢', + '䬳' => '䬳', + '餩' => '餩', + '馧' => '馧', + '駂' => '駂', + '駾' => '駾', + '䯎' => '䯎', + '𩬰' => '𩬰', + '鬒' => '鬒', + '鱀' => '鱀', + '鳽' => '鳽', + '䳎' => '䳎', + '䳭' => '䳭', + '鵧' => '鵧', + '𪃎' => '𪃎', + '䳸' => '䳸', + '𪄅' => '𪄅', + '𪈎' => '𪈎', + '𪊑' => '𪊑', + '麻' => '麻', + '䵖' => '䵖', + '黹' => '黹', + '黾' => '黾', + '鼅' => '鼅', + '鼏' => '鼏', + '鼖' => '鼖', + '鼻' => '鼻', + '𪘀' => '𪘀', +); diff --git a/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php b/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php new file mode 100644 index 0000000..ec90f36 --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php @@ -0,0 +1,876 @@ + 230, + '́' => 230, + '̂' => 230, + '̃' => 230, + '̄' => 230, + '̅' => 230, + '̆' => 230, + '̇' => 230, + '̈' => 230, + '̉' => 230, + '̊' => 230, + '̋' => 230, + '̌' => 230, + '̍' => 230, + '̎' => 230, + '̏' => 230, + '̐' => 230, + '̑' => 230, + '̒' => 230, + '̓' => 230, + '̔' => 230, + '̕' => 232, + '̖' => 220, + '̗' => 220, + '̘' => 220, + '̙' => 220, + '̚' => 232, + '̛' => 216, + '̜' => 220, + '̝' => 220, + '̞' => 220, + '̟' => 220, + '̠' => 220, + '̡' => 202, + '̢' => 202, + '̣' => 220, + '̤' => 220, + '̥' => 220, + '̦' => 220, + '̧' => 202, + '̨' => 202, + '̩' => 220, + '̪' => 220, + '̫' => 220, + '̬' => 220, + '̭' => 220, + '̮' => 220, + '̯' => 220, + '̰' => 220, + '̱' => 220, + '̲' => 220, + '̳' => 220, + '̴' => 1, + '̵' => 1, + '̶' => 1, + '̷' => 1, + '̸' => 1, + '̹' => 220, + '̺' => 220, + '̻' => 220, + '̼' => 220, + '̽' => 230, + '̾' => 230, + '̿' => 230, + '̀' => 230, + '́' => 230, + '͂' => 230, + '̓' => 230, + '̈́' => 230, + 'ͅ' => 240, + '͆' => 230, + '͇' => 220, + '͈' => 220, + '͉' => 220, + '͊' => 230, + '͋' => 230, + '͌' => 230, + '͍' => 220, + '͎' => 220, + '͐' => 230, + '͑' => 230, + '͒' => 230, + '͓' => 220, + '͔' => 220, + '͕' => 220, + '͖' => 220, + '͗' => 230, + '͘' => 232, + '͙' => 220, + '͚' => 220, + '͛' => 230, + '͜' => 233, + '͝' => 234, + '͞' => 234, + '͟' => 233, + '͠' => 234, + '͡' => 234, + '͢' => 233, + 'ͣ' => 230, + 'ͤ' => 230, + 'ͥ' => 230, + 'ͦ' => 230, + 'ͧ' => 230, + 'ͨ' => 230, + 'ͩ' => 230, + 'ͪ' => 230, + 'ͫ' => 230, + 'ͬ' => 230, + 'ͭ' => 230, + 'ͮ' => 230, + 'ͯ' => 230, + '҃' => 230, + '҄' => 230, + '҅' => 230, + '҆' => 230, + '҇' => 230, + '֑' => 220, + '֒' => 230, + '֓' => 230, + '֔' => 230, + '֕' => 230, + '֖' => 220, + '֗' => 230, + '֘' => 230, + '֙' => 230, + '֚' => 222, + '֛' => 220, + '֜' => 230, + '֝' => 230, + '֞' => 230, + '֟' => 230, + '֠' => 230, + '֡' => 230, + '֢' => 220, + '֣' => 220, + '֤' => 220, + '֥' => 220, + '֦' => 220, + '֧' => 220, + '֨' => 230, + '֩' => 230, + '֪' => 220, + '֫' => 230, + '֬' => 230, + '֭' => 222, + '֮' => 228, + '֯' => 230, + 'ְ' => 10, + 'ֱ' => 11, + 'ֲ' => 12, + 'ֳ' => 13, + 'ִ' => 14, + 'ֵ' => 15, + 'ֶ' => 16, + 'ַ' => 17, + 'ָ' => 18, + 'ֹ' => 19, + 'ֺ' => 19, + 'ֻ' => 20, + 'ּ' => 21, + 'ֽ' => 22, + 'ֿ' => 23, + 'ׁ' => 24, + 'ׂ' => 25, + 'ׄ' => 230, + 'ׅ' => 220, + 'ׇ' => 18, + 'ؐ' => 230, + 'ؑ' => 230, + 'ؒ' => 230, + 'ؓ' => 230, + 'ؔ' => 230, + 'ؕ' => 230, + 'ؖ' => 230, + 'ؗ' => 230, + 'ؘ' => 30, + 'ؙ' => 31, + 'ؚ' => 32, + 'ً' => 27, + 'ٌ' => 28, + 'ٍ' => 29, + 'َ' => 30, + 'ُ' => 31, + 'ِ' => 32, + 'ّ' => 33, + 'ْ' => 34, + 'ٓ' => 230, + 'ٔ' => 230, + 'ٕ' => 220, + 'ٖ' => 220, + 'ٗ' => 230, + '٘' => 230, + 'ٙ' => 230, + 'ٚ' => 230, + 'ٛ' => 230, + 'ٜ' => 220, + 'ٝ' => 230, + 'ٞ' => 230, + 'ٟ' => 220, + 'ٰ' => 35, + 'ۖ' => 230, + 'ۗ' => 230, + 'ۘ' => 230, + 'ۙ' => 230, + 'ۚ' => 230, + 'ۛ' => 230, + 'ۜ' => 230, + '۟' => 230, + '۠' => 230, + 'ۡ' => 230, + 'ۢ' => 230, + 'ۣ' => 220, + 'ۤ' => 230, + 'ۧ' => 230, + 'ۨ' => 230, + '۪' => 220, + '۫' => 230, + '۬' => 230, + 'ۭ' => 220, + 'ܑ' => 36, + 'ܰ' => 230, + 'ܱ' => 220, + 'ܲ' => 230, + 'ܳ' => 230, + 'ܴ' => 220, + 'ܵ' => 230, + 'ܶ' => 230, + 'ܷ' => 220, + 'ܸ' => 220, + 'ܹ' => 220, + 'ܺ' => 230, + 'ܻ' => 220, + 'ܼ' => 220, + 'ܽ' => 230, + 'ܾ' => 220, + 'ܿ' => 230, + '݀' => 230, + '݁' => 230, + '݂' => 220, + '݃' => 230, + '݄' => 220, + '݅' => 230, + '݆' => 220, + '݇' => 230, + '݈' => 220, + '݉' => 230, + '݊' => 230, + '߫' => 230, + '߬' => 230, + '߭' => 230, + '߮' => 230, + '߯' => 230, + '߰' => 230, + '߱' => 230, + '߲' => 220, + '߳' => 230, + '߽' => 220, + 'ࠖ' => 230, + 'ࠗ' => 230, + '࠘' => 230, + '࠙' => 230, + 'ࠛ' => 230, + 'ࠜ' => 230, + 'ࠝ' => 230, + 'ࠞ' => 230, + 'ࠟ' => 230, + 'ࠠ' => 230, + 'ࠡ' => 230, + 'ࠢ' => 230, + 'ࠣ' => 230, + 'ࠥ' => 230, + 'ࠦ' => 230, + 'ࠧ' => 230, + 'ࠩ' => 230, + 'ࠪ' => 230, + 'ࠫ' => 230, + 'ࠬ' => 230, + '࠭' => 230, + '࡙' => 220, + '࡚' => 220, + '࡛' => 220, + '࣓' => 220, + 'ࣔ' => 230, + 'ࣕ' => 230, + 'ࣖ' => 230, + 'ࣗ' => 230, + 'ࣘ' => 230, + 'ࣙ' => 230, + 'ࣚ' => 230, + 'ࣛ' => 230, + 'ࣜ' => 230, + 'ࣝ' => 230, + 'ࣞ' => 230, + 'ࣟ' => 230, + '࣠' => 230, + '࣡' => 230, + 'ࣣ' => 220, + 'ࣤ' => 230, + 'ࣥ' => 230, + 'ࣦ' => 220, + 'ࣧ' => 230, + 'ࣨ' => 230, + 'ࣩ' => 220, + '࣪' => 230, + '࣫' => 230, + '࣬' => 230, + '࣭' => 220, + '࣮' => 220, + '࣯' => 220, + 'ࣰ' => 27, + 'ࣱ' => 28, + 'ࣲ' => 29, + 'ࣳ' => 230, + 'ࣴ' => 230, + 'ࣵ' => 230, + 'ࣶ' => 220, + 'ࣷ' => 230, + 'ࣸ' => 230, + 'ࣹ' => 220, + 'ࣺ' => 220, + 'ࣻ' => 230, + 'ࣼ' => 230, + 'ࣽ' => 230, + 'ࣾ' => 230, + 'ࣿ' => 230, + '़' => 7, + '्' => 9, + '॑' => 230, + '॒' => 220, + '॓' => 230, + '॔' => 230, + '়' => 7, + '্' => 9, + '৾' => 230, + '਼' => 7, + '੍' => 9, + '઼' => 7, + '્' => 9, + '଼' => 7, + '୍' => 9, + '்' => 9, + '్' => 9, + 'ౕ' => 84, + 'ౖ' => 91, + '಼' => 7, + '್' => 9, + '഻' => 9, + '഼' => 9, + '്' => 9, + '්' => 9, + 'ุ' => 103, + 'ู' => 103, + 'ฺ' => 9, + '่' => 107, + '้' => 107, + '๊' => 107, + '๋' => 107, + 'ຸ' => 118, + 'ູ' => 118, + '຺' => 9, + '່' => 122, + '້' => 122, + '໊' => 122, + '໋' => 122, + '༘' => 220, + '༙' => 220, + '༵' => 220, + '༷' => 220, + '༹' => 216, + 'ཱ' => 129, + 'ི' => 130, + 'ུ' => 132, + 'ེ' => 130, + 'ཻ' => 130, + 'ོ' => 130, + 'ཽ' => 130, + 'ྀ' => 130, + 'ྂ' => 230, + 'ྃ' => 230, + '྄' => 9, + '྆' => 230, + '྇' => 230, + '࿆' => 220, + '့' => 7, + '္' => 9, + '်' => 9, + 'ႍ' => 220, + '፝' => 230, + '፞' => 230, + '፟' => 230, + '᜔' => 9, + '᜴' => 9, + '្' => 9, + '៝' => 230, + 'ᢩ' => 228, + '᤹' => 222, + '᤺' => 230, + '᤻' => 220, + 'ᨗ' => 230, + 'ᨘ' => 220, + '᩠' => 9, + '᩵' => 230, + '᩶' => 230, + '᩷' => 230, + '᩸' => 230, + '᩹' => 230, + '᩺' => 230, + '᩻' => 230, + '᩼' => 230, + '᩿' => 220, + '᪰' => 230, + '᪱' => 230, + '᪲' => 230, + '᪳' => 230, + '᪴' => 230, + '᪵' => 220, + '᪶' => 220, + '᪷' => 220, + '᪸' => 220, + '᪹' => 220, + '᪺' => 220, + '᪻' => 230, + '᪼' => 230, + '᪽' => 220, + 'ᪿ' => 220, + 'ᫀ' => 220, + '᬴' => 7, + '᭄' => 9, + '᭫' => 230, + '᭬' => 220, + '᭭' => 230, + '᭮' => 230, + '᭯' => 230, + '᭰' => 230, + '᭱' => 230, + '᭲' => 230, + '᭳' => 230, + '᮪' => 9, + '᮫' => 9, + '᯦' => 7, + '᯲' => 9, + '᯳' => 9, + '᰷' => 7, + '᳐' => 230, + '᳑' => 230, + '᳒' => 230, + '᳔' => 1, + '᳕' => 220, + '᳖' => 220, + '᳗' => 220, + '᳘' => 220, + '᳙' => 220, + '᳚' => 230, + '᳛' => 230, + '᳜' => 220, + '᳝' => 220, + '᳞' => 220, + '᳟' => 220, + '᳠' => 230, + '᳢' => 1, + '᳣' => 1, + '᳤' => 1, + '᳥' => 1, + '᳦' => 1, + '᳧' => 1, + '᳨' => 1, + '᳭' => 220, + '᳴' => 230, + '᳸' => 230, + '᳹' => 230, + '᷀' => 230, + '᷁' => 230, + '᷂' => 220, + '᷃' => 230, + '᷄' => 230, + '᷅' => 230, + '᷆' => 230, + '᷇' => 230, + '᷈' => 230, + '᷉' => 230, + '᷊' => 220, + '᷋' => 230, + '᷌' => 230, + '᷍' => 234, + '᷎' => 214, + '᷏' => 220, + '᷐' => 202, + '᷑' => 230, + '᷒' => 230, + 'ᷓ' => 230, + 'ᷔ' => 230, + 'ᷕ' => 230, + 'ᷖ' => 230, + 'ᷗ' => 230, + 'ᷘ' => 230, + 'ᷙ' => 230, + 'ᷚ' => 230, + 'ᷛ' => 230, + 'ᷜ' => 230, + 'ᷝ' => 230, + 'ᷞ' => 230, + 'ᷟ' => 230, + 'ᷠ' => 230, + 'ᷡ' => 230, + 'ᷢ' => 230, + 'ᷣ' => 230, + 'ᷤ' => 230, + 'ᷥ' => 230, + 'ᷦ' => 230, + 'ᷧ' => 230, + 'ᷨ' => 230, + 'ᷩ' => 230, + 'ᷪ' => 230, + 'ᷫ' => 230, + 'ᷬ' => 230, + 'ᷭ' => 230, + 'ᷮ' => 230, + 'ᷯ' => 230, + 'ᷰ' => 230, + 'ᷱ' => 230, + 'ᷲ' => 230, + 'ᷳ' => 230, + 'ᷴ' => 230, + '᷵' => 230, + '᷶' => 232, + '᷷' => 228, + '᷸' => 228, + '᷹' => 220, + '᷻' => 230, + '᷼' => 233, + '᷽' => 220, + '᷾' => 230, + '᷿' => 220, + '⃐' => 230, + '⃑' => 230, + '⃒' => 1, + '⃓' => 1, + '⃔' => 230, + '⃕' => 230, + '⃖' => 230, + '⃗' => 230, + '⃘' => 1, + '⃙' => 1, + '⃚' => 1, + '⃛' => 230, + '⃜' => 230, + '⃡' => 230, + '⃥' => 1, + '⃦' => 1, + '⃧' => 230, + '⃨' => 220, + '⃩' => 230, + '⃪' => 1, + '⃫' => 1, + '⃬' => 220, + '⃭' => 220, + '⃮' => 220, + '⃯' => 220, + '⃰' => 230, + '⳯' => 230, + '⳰' => 230, + '⳱' => 230, + '⵿' => 9, + 'ⷠ' => 230, + 'ⷡ' => 230, + 'ⷢ' => 230, + 'ⷣ' => 230, + 'ⷤ' => 230, + 'ⷥ' => 230, + 'ⷦ' => 230, + 'ⷧ' => 230, + 'ⷨ' => 230, + 'ⷩ' => 230, + 'ⷪ' => 230, + 'ⷫ' => 230, + 'ⷬ' => 230, + 'ⷭ' => 230, + 'ⷮ' => 230, + 'ⷯ' => 230, + 'ⷰ' => 230, + 'ⷱ' => 230, + 'ⷲ' => 230, + 'ⷳ' => 230, + 'ⷴ' => 230, + 'ⷵ' => 230, + 'ⷶ' => 230, + 'ⷷ' => 230, + 'ⷸ' => 230, + 'ⷹ' => 230, + 'ⷺ' => 230, + 'ⷻ' => 230, + 'ⷼ' => 230, + 'ⷽ' => 230, + 'ⷾ' => 230, + 'ⷿ' => 230, + '〪' => 218, + '〫' => 228, + '〬' => 232, + '〭' => 222, + '〮' => 224, + '〯' => 224, + '゙' => 8, + '゚' => 8, + '꙯' => 230, + 'ꙴ' => 230, + 'ꙵ' => 230, + 'ꙶ' => 230, + 'ꙷ' => 230, + 'ꙸ' => 230, + 'ꙹ' => 230, + 'ꙺ' => 230, + 'ꙻ' => 230, + '꙼' => 230, + '꙽' => 230, + 'ꚞ' => 230, + 'ꚟ' => 230, + '꛰' => 230, + '꛱' => 230, + '꠆' => 9, + '꠬' => 9, + '꣄' => 9, + '꣠' => 230, + '꣡' => 230, + '꣢' => 230, + '꣣' => 230, + '꣤' => 230, + '꣥' => 230, + '꣦' => 230, + '꣧' => 230, + '꣨' => 230, + '꣩' => 230, + '꣪' => 230, + '꣫' => 230, + '꣬' => 230, + '꣭' => 230, + '꣮' => 230, + '꣯' => 230, + '꣰' => 230, + '꣱' => 230, + '꤫' => 220, + '꤬' => 220, + '꤭' => 220, + '꥓' => 9, + '꦳' => 7, + '꧀' => 9, + 'ꪰ' => 230, + 'ꪲ' => 230, + 'ꪳ' => 230, + 'ꪴ' => 220, + 'ꪷ' => 230, + 'ꪸ' => 230, + 'ꪾ' => 230, + '꪿' => 230, + '꫁' => 230, + '꫶' => 9, + '꯭' => 9, + 'ﬞ' => 26, + '︠' => 230, + '︡' => 230, + '︢' => 230, + '︣' => 230, + '︤' => 230, + '︥' => 230, + '︦' => 230, + '︧' => 220, + '︨' => 220, + '︩' => 220, + '︪' => 220, + '︫' => 220, + '︬' => 220, + '︭' => 220, + '︮' => 230, + '︯' => 230, + '𐇽' => 220, + '𐋠' => 220, + '𐍶' => 230, + '𐍷' => 230, + '𐍸' => 230, + '𐍹' => 230, + '𐍺' => 230, + '𐨍' => 220, + '𐨏' => 230, + '𐨸' => 230, + '𐨹' => 1, + '𐨺' => 220, + '𐨿' => 9, + '𐫥' => 230, + '𐫦' => 220, + '𐴤' => 230, + '𐴥' => 230, + '𐴦' => 230, + '𐴧' => 230, + '𐺫' => 230, + '𐺬' => 230, + '𐽆' => 220, + '𐽇' => 220, + '𐽈' => 230, + '𐽉' => 230, + '𐽊' => 230, + '𐽋' => 220, + '𐽌' => 230, + '𐽍' => 220, + '𐽎' => 220, + '𐽏' => 220, + '𐽐' => 220, + '𑁆' => 9, + '𑁿' => 9, + '𑂹' => 9, + '𑂺' => 7, + '𑄀' => 230, + '𑄁' => 230, + '𑄂' => 230, + '𑄳' => 9, + '𑄴' => 9, + '𑅳' => 7, + '𑇀' => 9, + '𑇊' => 7, + '𑈵' => 9, + '𑈶' => 7, + '𑋩' => 7, + '𑋪' => 9, + '𑌻' => 7, + '𑌼' => 7, + '𑍍' => 9, + '𑍦' => 230, + '𑍧' => 230, + '𑍨' => 230, + '𑍩' => 230, + '𑍪' => 230, + '𑍫' => 230, + '𑍬' => 230, + '𑍰' => 230, + '𑍱' => 230, + '𑍲' => 230, + '𑍳' => 230, + '𑍴' => 230, + '𑑂' => 9, + '𑑆' => 7, + '𑑞' => 230, + '𑓂' => 9, + '𑓃' => 7, + '𑖿' => 9, + '𑗀' => 7, + '𑘿' => 9, + '𑚶' => 9, + '𑚷' => 7, + '𑜫' => 9, + '𑠹' => 9, + '𑠺' => 7, + '𑤽' => 9, + '𑤾' => 9, + '𑥃' => 7, + '𑧠' => 9, + '𑨴' => 9, + '𑩇' => 9, + '𑪙' => 9, + '𑰿' => 9, + '𑵂' => 7, + '𑵄' => 9, + '𑵅' => 9, + '𑶗' => 9, + '𖫰' => 1, + '𖫱' => 1, + '𖫲' => 1, + '𖫳' => 1, + '𖫴' => 1, + '𖬰' => 230, + '𖬱' => 230, + '𖬲' => 230, + '𖬳' => 230, + '𖬴' => 230, + '𖬵' => 230, + '𖬶' => 230, + '𖿰' => 6, + '𖿱' => 6, + '𛲞' => 1, + '𝅥' => 216, + '𝅦' => 216, + '𝅧' => 1, + '𝅨' => 1, + '𝅩' => 1, + '𝅭' => 226, + '𝅮' => 216, + '𝅯' => 216, + '𝅰' => 216, + '𝅱' => 216, + '𝅲' => 216, + '𝅻' => 220, + '𝅼' => 220, + '𝅽' => 220, + '𝅾' => 220, + '𝅿' => 220, + '𝆀' => 220, + '𝆁' => 220, + '𝆂' => 220, + '𝆅' => 230, + '𝆆' => 230, + '𝆇' => 230, + '𝆈' => 230, + '𝆉' => 230, + '𝆊' => 220, + '𝆋' => 220, + '𝆪' => 230, + '𝆫' => 230, + '𝆬' => 230, + '𝆭' => 230, + '𝉂' => 230, + '𝉃' => 230, + '𝉄' => 230, + '𞀀' => 230, + '𞀁' => 230, + '𞀂' => 230, + '𞀃' => 230, + '𞀄' => 230, + '𞀅' => 230, + '𞀆' => 230, + '𞀈' => 230, + '𞀉' => 230, + '𞀊' => 230, + '𞀋' => 230, + '𞀌' => 230, + '𞀍' => 230, + '𞀎' => 230, + '𞀏' => 230, + '𞀐' => 230, + '𞀑' => 230, + '𞀒' => 230, + '𞀓' => 230, + '𞀔' => 230, + '𞀕' => 230, + '𞀖' => 230, + '𞀗' => 230, + '𞀘' => 230, + '𞀛' => 230, + '𞀜' => 230, + '𞀝' => 230, + '𞀞' => 230, + '𞀟' => 230, + '𞀠' => 230, + '𞀡' => 230, + '𞀣' => 230, + '𞀤' => 230, + '𞀦' => 230, + '𞀧' => 230, + '𞀨' => 230, + '𞀩' => 230, + '𞀪' => 230, + '𞄰' => 230, + '𞄱' => 230, + '𞄲' => 230, + '𞄳' => 230, + '𞄴' => 230, + '𞄵' => 230, + '𞄶' => 230, + '𞋬' => 230, + '𞋭' => 230, + '𞋮' => 230, + '𞋯' => 230, + '𞣐' => 220, + '𞣑' => 220, + '𞣒' => 220, + '𞣓' => 220, + '𞣔' => 220, + '𞣕' => 220, + '𞣖' => 220, + '𞥄' => 230, + '𞥅' => 230, + '𞥆' => 230, + '𞥇' => 230, + '𞥈' => 230, + '𞥉' => 230, + '𞥊' => 7, +); diff --git a/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php b/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php new file mode 100644 index 0000000..1574902 --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php @@ -0,0 +1,3695 @@ + ' ', + '¨' => ' ̈', + 'ª' => 'a', + '¯' => ' ̄', + '²' => '2', + '³' => '3', + '´' => ' ́', + 'µ' => 'μ', + '¸' => ' ̧', + '¹' => '1', + 'º' => 'o', + '¼' => '1⁄4', + '½' => '1⁄2', + '¾' => '3⁄4', + 'IJ' => 'IJ', + 'ij' => 'ij', + 'Ŀ' => 'L·', + 'ŀ' => 'l·', + 'ʼn' => 'ʼn', + 'ſ' => 's', + 'DŽ' => 'DŽ', + 'Dž' => 'Dž', + 'dž' => 'dž', + 'LJ' => 'LJ', + 'Lj' => 'Lj', + 'lj' => 'lj', + 'NJ' => 'NJ', + 'Nj' => 'Nj', + 'nj' => 'nj', + 'DZ' => 'DZ', + 'Dz' => 'Dz', + 'dz' => 'dz', + 'ʰ' => 'h', + 'ʱ' => 'ɦ', + 'ʲ' => 'j', + 'ʳ' => 'r', + 'ʴ' => 'ɹ', + 'ʵ' => 'ɻ', + 'ʶ' => 'ʁ', + 'ʷ' => 'w', + 'ʸ' => 'y', + '˘' => ' ̆', + '˙' => ' ̇', + '˚' => ' ̊', + '˛' => ' ̨', + '˜' => ' ̃', + '˝' => ' ̋', + 'ˠ' => 'ɣ', + 'ˡ' => 'l', + 'ˢ' => 's', + 'ˣ' => 'x', + 'ˤ' => 'ʕ', + 'ͺ' => ' ͅ', + '΄' => ' ́', + '΅' => ' ̈́', + 'ϐ' => 'β', + 'ϑ' => 'θ', + 'ϒ' => 'Υ', + 'ϓ' => 'Ύ', + 'ϔ' => 'Ϋ', + 'ϕ' => 'φ', + 'ϖ' => 'π', + 'ϰ' => 'κ', + 'ϱ' => 'ρ', + 'ϲ' => 'ς', + 'ϴ' => 'Θ', + 'ϵ' => 'ε', + 'Ϲ' => 'Σ', + 'և' => 'եւ', + 'ٵ' => 'اٴ', + 'ٶ' => 'وٴ', + 'ٷ' => 'ۇٴ', + 'ٸ' => 'يٴ', + 'ำ' => 'ํา', + 'ຳ' => 'ໍາ', + 'ໜ' => 'ຫນ', + 'ໝ' => 'ຫມ', + '༌' => '་', + 'ཷ' => 'ྲཱྀ', + 'ཹ' => 'ླཱྀ', + 'ჼ' => 'ნ', + 'ᴬ' => 'A', + 'ᴭ' => 'Æ', + 'ᴮ' => 'B', + 'ᴰ' => 'D', + 'ᴱ' => 'E', + 'ᴲ' => 'Ǝ', + 'ᴳ' => 'G', + 'ᴴ' => 'H', + 'ᴵ' => 'I', + 'ᴶ' => 'J', + 'ᴷ' => 'K', + 'ᴸ' => 'L', + 'ᴹ' => 'M', + 'ᴺ' => 'N', + 'ᴼ' => 'O', + 'ᴽ' => 'Ȣ', + 'ᴾ' => 'P', + 'ᴿ' => 'R', + 'ᵀ' => 'T', + 'ᵁ' => 'U', + 'ᵂ' => 'W', + 'ᵃ' => 'a', + 'ᵄ' => 'ɐ', + 'ᵅ' => 'ɑ', + 'ᵆ' => 'ᴂ', + 'ᵇ' => 'b', + 'ᵈ' => 'd', + 'ᵉ' => 'e', + 'ᵊ' => 'ə', + 'ᵋ' => 'ɛ', + 'ᵌ' => 'ɜ', + 'ᵍ' => 'g', + 'ᵏ' => 'k', + 'ᵐ' => 'm', + 'ᵑ' => 'ŋ', + 'ᵒ' => 'o', + 'ᵓ' => 'ɔ', + 'ᵔ' => 'ᴖ', + 'ᵕ' => 'ᴗ', + 'ᵖ' => 'p', + 'ᵗ' => 't', + 'ᵘ' => 'u', + 'ᵙ' => 'ᴝ', + 'ᵚ' => 'ɯ', + 'ᵛ' => 'v', + 'ᵜ' => 'ᴥ', + 'ᵝ' => 'β', + 'ᵞ' => 'γ', + 'ᵟ' => 'δ', + 'ᵠ' => 'φ', + 'ᵡ' => 'χ', + 'ᵢ' => 'i', + 'ᵣ' => 'r', + 'ᵤ' => 'u', + 'ᵥ' => 'v', + 'ᵦ' => 'β', + 'ᵧ' => 'γ', + 'ᵨ' => 'ρ', + 'ᵩ' => 'φ', + 'ᵪ' => 'χ', + 'ᵸ' => 'н', + 'ᶛ' => 'ɒ', + 'ᶜ' => 'c', + 'ᶝ' => 'ɕ', + 'ᶞ' => 'ð', + 'ᶟ' => 'ɜ', + 'ᶠ' => 'f', + 'ᶡ' => 'ɟ', + 'ᶢ' => 'ɡ', + 'ᶣ' => 'ɥ', + 'ᶤ' => 'ɨ', + 'ᶥ' => 'ɩ', + 'ᶦ' => 'ɪ', + 'ᶧ' => 'ᵻ', + 'ᶨ' => 'ʝ', + 'ᶩ' => 'ɭ', + 'ᶪ' => 'ᶅ', + 'ᶫ' => 'ʟ', + 'ᶬ' => 'ɱ', + 'ᶭ' => 'ɰ', + 'ᶮ' => 'ɲ', + 'ᶯ' => 'ɳ', + 'ᶰ' => 'ɴ', + 'ᶱ' => 'ɵ', + 'ᶲ' => 'ɸ', + 'ᶳ' => 'ʂ', + 'ᶴ' => 'ʃ', + 'ᶵ' => 'ƫ', + 'ᶶ' => 'ʉ', + 'ᶷ' => 'ʊ', + 'ᶸ' => 'ᴜ', + 'ᶹ' => 'ʋ', + 'ᶺ' => 'ʌ', + 'ᶻ' => 'z', + 'ᶼ' => 'ʐ', + 'ᶽ' => 'ʑ', + 'ᶾ' => 'ʒ', + 'ᶿ' => 'θ', + 'ẚ' => 'aʾ', + 'ẛ' => 'ṡ', + '᾽' => ' ̓', + '᾿' => ' ̓', + '῀' => ' ͂', + '῁' => ' ̈͂', + '῍' => ' ̓̀', + '῎' => ' ̓́', + '῏' => ' ̓͂', + '῝' => ' ̔̀', + '῞' => ' ̔́', + '῟' => ' ̔͂', + '῭' => ' ̈̀', + '΅' => ' ̈́', + '´' => ' ́', + '῾' => ' ̔', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + '‑' => '‐', + '‗' => ' ̳', + '․' => '.', + '‥' => '..', + '…' => '...', + ' ' => ' ', + '″' => '′′', + '‴' => '′′′', + '‶' => '‵‵', + '‷' => '‵‵‵', + '‼' => '!!', + '‾' => ' ̅', + '⁇' => '??', + '⁈' => '?!', + '⁉' => '!?', + '⁗' => '′′′′', + ' ' => ' ', + '⁰' => '0', + 'ⁱ' => 'i', + '⁴' => '4', + '⁵' => '5', + '⁶' => '6', + '⁷' => '7', + '⁸' => '8', + '⁹' => '9', + '⁺' => '+', + '⁻' => '−', + '⁼' => '=', + '⁽' => '(', + '⁾' => ')', + 'ⁿ' => 'n', + '₀' => '0', + '₁' => '1', + '₂' => '2', + '₃' => '3', + '₄' => '4', + '₅' => '5', + '₆' => '6', + '₇' => '7', + '₈' => '8', + '₉' => '9', + '₊' => '+', + '₋' => '−', + '₌' => '=', + '₍' => '(', + '₎' => ')', + 'ₐ' => 'a', + 'ₑ' => 'e', + 'ₒ' => 'o', + 'ₓ' => 'x', + 'ₔ' => 'ə', + 'ₕ' => 'h', + 'ₖ' => 'k', + 'ₗ' => 'l', + 'ₘ' => 'm', + 'ₙ' => 'n', + 'ₚ' => 'p', + 'ₛ' => 's', + 'ₜ' => 't', + '₨' => 'Rs', + '℀' => 'a/c', + '℁' => 'a/s', + 'ℂ' => 'C', + '℃' => '°C', + '℅' => 'c/o', + '℆' => 'c/u', + 'ℇ' => 'Ɛ', + '℉' => '°F', + 'ℊ' => 'g', + 'ℋ' => 'H', + 'ℌ' => 'H', + 'ℍ' => 'H', + 'ℎ' => 'h', + 'ℏ' => 'ħ', + 'ℐ' => 'I', + 'ℑ' => 'I', + 'ℒ' => 'L', + 'ℓ' => 'l', + 'ℕ' => 'N', + '№' => 'No', + 'ℙ' => 'P', + 'ℚ' => 'Q', + 'ℛ' => 'R', + 'ℜ' => 'R', + 'ℝ' => 'R', + '℠' => 'SM', + '℡' => 'TEL', + '™' => 'TM', + 'ℤ' => 'Z', + 'ℨ' => 'Z', + 'ℬ' => 'B', + 'ℭ' => 'C', + 'ℯ' => 'e', + 'ℰ' => 'E', + 'ℱ' => 'F', + 'ℳ' => 'M', + 'ℴ' => 'o', + 'ℵ' => 'א', + 'ℶ' => 'ב', + 'ℷ' => 'ג', + 'ℸ' => 'ד', + 'ℹ' => 'i', + '℻' => 'FAX', + 'ℼ' => 'π', + 'ℽ' => 'γ', + 'ℾ' => 'Γ', + 'ℿ' => 'Π', + '⅀' => '∑', + 'ⅅ' => 'D', + 'ⅆ' => 'd', + 'ⅇ' => 'e', + 'ⅈ' => 'i', + 'ⅉ' => 'j', + '⅐' => '1⁄7', + '⅑' => '1⁄9', + '⅒' => '1⁄10', + '⅓' => '1⁄3', + '⅔' => '2⁄3', + '⅕' => '1⁄5', + '⅖' => '2⁄5', + '⅗' => '3⁄5', + '⅘' => '4⁄5', + '⅙' => '1⁄6', + '⅚' => '5⁄6', + '⅛' => '1⁄8', + '⅜' => '3⁄8', + '⅝' => '5⁄8', + '⅞' => '7⁄8', + '⅟' => '1⁄', + 'Ⅰ' => 'I', + 'Ⅱ' => 'II', + 'Ⅲ' => 'III', + 'Ⅳ' => 'IV', + 'Ⅴ' => 'V', + 'Ⅵ' => 'VI', + 'Ⅶ' => 'VII', + 'Ⅷ' => 'VIII', + 'Ⅸ' => 'IX', + 'Ⅹ' => 'X', + 'Ⅺ' => 'XI', + 'Ⅻ' => 'XII', + 'Ⅼ' => 'L', + 'Ⅽ' => 'C', + 'Ⅾ' => 'D', + 'Ⅿ' => 'M', + 'ⅰ' => 'i', + 'ⅱ' => 'ii', + 'ⅲ' => 'iii', + 'ⅳ' => 'iv', + 'ⅴ' => 'v', + 'ⅵ' => 'vi', + 'ⅶ' => 'vii', + 'ⅷ' => 'viii', + 'ⅸ' => 'ix', + 'ⅹ' => 'x', + 'ⅺ' => 'xi', + 'ⅻ' => 'xii', + 'ⅼ' => 'l', + 'ⅽ' => 'c', + 'ⅾ' => 'd', + 'ⅿ' => 'm', + '↉' => '0⁄3', + '∬' => '∫∫', + '∭' => '∫∫∫', + '∯' => '∮∮', + '∰' => '∮∮∮', + '①' => '1', + '②' => '2', + '③' => '3', + '④' => '4', + '⑤' => '5', + '⑥' => '6', + '⑦' => '7', + '⑧' => '8', + '⑨' => '9', + '⑩' => '10', + '⑪' => '11', + '⑫' => '12', + '⑬' => '13', + '⑭' => '14', + '⑮' => '15', + '⑯' => '16', + '⑰' => '17', + '⑱' => '18', + '⑲' => '19', + '⑳' => '20', + '⑴' => '(1)', + '⑵' => '(2)', + '⑶' => '(3)', + '⑷' => '(4)', + '⑸' => '(5)', + '⑹' => '(6)', + '⑺' => '(7)', + '⑻' => '(8)', + '⑼' => '(9)', + '⑽' => '(10)', + '⑾' => '(11)', + '⑿' => '(12)', + '⒀' => '(13)', + '⒁' => '(14)', + '⒂' => '(15)', + '⒃' => '(16)', + '⒄' => '(17)', + '⒅' => '(18)', + '⒆' => '(19)', + '⒇' => '(20)', + '⒈' => '1.', + '⒉' => '2.', + '⒊' => '3.', + '⒋' => '4.', + '⒌' => '5.', + '⒍' => '6.', + '⒎' => '7.', + '⒏' => '8.', + '⒐' => '9.', + '⒑' => '10.', + '⒒' => '11.', + '⒓' => '12.', + '⒔' => '13.', + '⒕' => '14.', + '⒖' => '15.', + '⒗' => '16.', + '⒘' => '17.', + '⒙' => '18.', + '⒚' => '19.', + '⒛' => '20.', + '⒜' => '(a)', + '⒝' => '(b)', + '⒞' => '(c)', + '⒟' => '(d)', + '⒠' => '(e)', + '⒡' => '(f)', + '⒢' => '(g)', + '⒣' => '(h)', + '⒤' => '(i)', + '⒥' => '(j)', + '⒦' => '(k)', + '⒧' => '(l)', + '⒨' => '(m)', + '⒩' => '(n)', + '⒪' => '(o)', + '⒫' => '(p)', + '⒬' => '(q)', + '⒭' => '(r)', + '⒮' => '(s)', + '⒯' => '(t)', + '⒰' => '(u)', + '⒱' => '(v)', + '⒲' => '(w)', + '⒳' => '(x)', + '⒴' => '(y)', + '⒵' => '(z)', + 'Ⓐ' => 'A', + 'Ⓑ' => 'B', + 'Ⓒ' => 'C', + 'Ⓓ' => 'D', + 'Ⓔ' => 'E', + 'Ⓕ' => 'F', + 'Ⓖ' => 'G', + 'Ⓗ' => 'H', + 'Ⓘ' => 'I', + 'Ⓙ' => 'J', + 'Ⓚ' => 'K', + 'Ⓛ' => 'L', + 'Ⓜ' => 'M', + 'Ⓝ' => 'N', + 'Ⓞ' => 'O', + 'Ⓟ' => 'P', + 'Ⓠ' => 'Q', + 'Ⓡ' => 'R', + 'Ⓢ' => 'S', + 'Ⓣ' => 'T', + 'Ⓤ' => 'U', + 'Ⓥ' => 'V', + 'Ⓦ' => 'W', + 'Ⓧ' => 'X', + 'Ⓨ' => 'Y', + 'Ⓩ' => 'Z', + 'ⓐ' => 'a', + 'ⓑ' => 'b', + 'ⓒ' => 'c', + 'ⓓ' => 'd', + 'ⓔ' => 'e', + 'ⓕ' => 'f', + 'ⓖ' => 'g', + 'ⓗ' => 'h', + 'ⓘ' => 'i', + 'ⓙ' => 'j', + 'ⓚ' => 'k', + 'ⓛ' => 'l', + 'ⓜ' => 'm', + 'ⓝ' => 'n', + 'ⓞ' => 'o', + 'ⓟ' => 'p', + 'ⓠ' => 'q', + 'ⓡ' => 'r', + 'ⓢ' => 's', + 'ⓣ' => 't', + 'ⓤ' => 'u', + 'ⓥ' => 'v', + 'ⓦ' => 'w', + 'ⓧ' => 'x', + 'ⓨ' => 'y', + 'ⓩ' => 'z', + '⓪' => '0', + '⨌' => '∫∫∫∫', + '⩴' => '::=', + '⩵' => '==', + '⩶' => '===', + 'ⱼ' => 'j', + 'ⱽ' => 'V', + 'ⵯ' => 'ⵡ', + '⺟' => '母', + '⻳' => '龟', + '⼀' => '一', + '⼁' => '丨', + '⼂' => '丶', + '⼃' => '丿', + '⼄' => '乙', + '⼅' => '亅', + '⼆' => '二', + '⼇' => '亠', + '⼈' => '人', + '⼉' => '儿', + '⼊' => '入', + '⼋' => '八', + '⼌' => '冂', + '⼍' => '冖', + '⼎' => '冫', + '⼏' => '几', + '⼐' => '凵', + '⼑' => '刀', + '⼒' => '力', + '⼓' => '勹', + '⼔' => '匕', + '⼕' => '匚', + '⼖' => '匸', + '⼗' => '十', + '⼘' => '卜', + '⼙' => '卩', + '⼚' => '厂', + '⼛' => '厶', + '⼜' => '又', + '⼝' => '口', + '⼞' => '囗', + '⼟' => '土', + '⼠' => '士', + '⼡' => '夂', + '⼢' => '夊', + '⼣' => '夕', + '⼤' => '大', + '⼥' => '女', + '⼦' => '子', + '⼧' => '宀', + '⼨' => '寸', + '⼩' => '小', + '⼪' => '尢', + '⼫' => '尸', + '⼬' => '屮', + '⼭' => '山', + '⼮' => '巛', + '⼯' => '工', + '⼰' => '己', + '⼱' => '巾', + '⼲' => '干', + '⼳' => '幺', + '⼴' => '广', + '⼵' => '廴', + '⼶' => '廾', + '⼷' => '弋', + '⼸' => '弓', + '⼹' => '彐', + '⼺' => '彡', + '⼻' => '彳', + '⼼' => '心', + '⼽' => '戈', + '⼾' => '戶', + '⼿' => '手', + '⽀' => '支', + '⽁' => '攴', + '⽂' => '文', + '⽃' => '斗', + '⽄' => '斤', + '⽅' => '方', + '⽆' => '无', + '⽇' => '日', + '⽈' => '曰', + '⽉' => '月', + '⽊' => '木', + '⽋' => '欠', + '⽌' => '止', + '⽍' => '歹', + '⽎' => '殳', + '⽏' => '毋', + '⽐' => '比', + '⽑' => '毛', + '⽒' => '氏', + '⽓' => '气', + '⽔' => '水', + '⽕' => '火', + '⽖' => '爪', + '⽗' => '父', + '⽘' => '爻', + '⽙' => '爿', + '⽚' => '片', + '⽛' => '牙', + '⽜' => '牛', + '⽝' => '犬', + '⽞' => '玄', + '⽟' => '玉', + '⽠' => '瓜', + '⽡' => '瓦', + '⽢' => '甘', + '⽣' => '生', + '⽤' => '用', + '⽥' => '田', + '⽦' => '疋', + '⽧' => '疒', + '⽨' => '癶', + '⽩' => '白', + '⽪' => '皮', + '⽫' => '皿', + '⽬' => '目', + '⽭' => '矛', + '⽮' => '矢', + '⽯' => '石', + '⽰' => '示', + '⽱' => '禸', + '⽲' => '禾', + '⽳' => '穴', + '⽴' => '立', + '⽵' => '竹', + '⽶' => '米', + '⽷' => '糸', + '⽸' => '缶', + '⽹' => '网', + '⽺' => '羊', + '⽻' => '羽', + '⽼' => '老', + '⽽' => '而', + '⽾' => '耒', + '⽿' => '耳', + '⾀' => '聿', + '⾁' => '肉', + '⾂' => '臣', + '⾃' => '自', + '⾄' => '至', + '⾅' => '臼', + '⾆' => '舌', + '⾇' => '舛', + '⾈' => '舟', + '⾉' => '艮', + '⾊' => '色', + '⾋' => '艸', + '⾌' => '虍', + '⾍' => '虫', + '⾎' => '血', + '⾏' => '行', + '⾐' => '衣', + '⾑' => '襾', + '⾒' => '見', + '⾓' => '角', + '⾔' => '言', + '⾕' => '谷', + '⾖' => '豆', + '⾗' => '豕', + '⾘' => '豸', + '⾙' => '貝', + '⾚' => '赤', + '⾛' => '走', + '⾜' => '足', + '⾝' => '身', + '⾞' => '車', + '⾟' => '辛', + '⾠' => '辰', + '⾡' => '辵', + '⾢' => '邑', + '⾣' => '酉', + '⾤' => '釆', + '⾥' => '里', + '⾦' => '金', + '⾧' => '長', + '⾨' => '門', + '⾩' => '阜', + '⾪' => '隶', + '⾫' => '隹', + '⾬' => '雨', + '⾭' => '靑', + '⾮' => '非', + '⾯' => '面', + '⾰' => '革', + '⾱' => '韋', + '⾲' => '韭', + '⾳' => '音', + '⾴' => '頁', + '⾵' => '風', + '⾶' => '飛', + '⾷' => '食', + '⾸' => '首', + '⾹' => '香', + '⾺' => '馬', + '⾻' => '骨', + '⾼' => '高', + '⾽' => '髟', + '⾾' => '鬥', + '⾿' => '鬯', + '⿀' => '鬲', + '⿁' => '鬼', + '⿂' => '魚', + '⿃' => '鳥', + '⿄' => '鹵', + '⿅' => '鹿', + '⿆' => '麥', + '⿇' => '麻', + '⿈' => '黃', + '⿉' => '黍', + '⿊' => '黑', + '⿋' => '黹', + '⿌' => '黽', + '⿍' => '鼎', + '⿎' => '鼓', + '⿏' => '鼠', + '⿐' => '鼻', + '⿑' => '齊', + '⿒' => '齒', + '⿓' => '龍', + '⿔' => '龜', + '⿕' => '龠', + ' ' => ' ', + '〶' => '〒', + '〸' => '十', + '〹' => '卄', + '〺' => '卅', + '゛' => ' ゙', + '゜' => ' ゚', + 'ゟ' => 'より', + 'ヿ' => 'コト', + 'ㄱ' => 'ᄀ', + 'ㄲ' => 'ᄁ', + 'ㄳ' => 'ᆪ', + 'ㄴ' => 'ᄂ', + 'ㄵ' => 'ᆬ', + 'ㄶ' => 'ᆭ', + 'ㄷ' => 'ᄃ', + 'ㄸ' => 'ᄄ', + 'ㄹ' => 'ᄅ', + 'ㄺ' => 'ᆰ', + 'ㄻ' => 'ᆱ', + 'ㄼ' => 'ᆲ', + 'ㄽ' => 'ᆳ', + 'ㄾ' => 'ᆴ', + 'ㄿ' => 'ᆵ', + 'ㅀ' => 'ᄚ', + 'ㅁ' => 'ᄆ', + 'ㅂ' => 'ᄇ', + 'ㅃ' => 'ᄈ', + 'ㅄ' => 'ᄡ', + 'ㅅ' => 'ᄉ', + 'ㅆ' => 'ᄊ', + 'ㅇ' => 'ᄋ', + 'ㅈ' => 'ᄌ', + 'ㅉ' => 'ᄍ', + 'ㅊ' => 'ᄎ', + 'ㅋ' => 'ᄏ', + 'ㅌ' => 'ᄐ', + 'ㅍ' => 'ᄑ', + 'ㅎ' => 'ᄒ', + 'ㅏ' => 'ᅡ', + 'ㅐ' => 'ᅢ', + 'ㅑ' => 'ᅣ', + 'ㅒ' => 'ᅤ', + 'ㅓ' => 'ᅥ', + 'ㅔ' => 'ᅦ', + 'ㅕ' => 'ᅧ', + 'ㅖ' => 'ᅨ', + 'ㅗ' => 'ᅩ', + 'ㅘ' => 'ᅪ', + 'ㅙ' => 'ᅫ', + 'ㅚ' => 'ᅬ', + 'ㅛ' => 'ᅭ', + 'ㅜ' => 'ᅮ', + 'ㅝ' => 'ᅯ', + 'ㅞ' => 'ᅰ', + 'ㅟ' => 'ᅱ', + 'ㅠ' => 'ᅲ', + 'ㅡ' => 'ᅳ', + 'ㅢ' => 'ᅴ', + 'ㅣ' => 'ᅵ', + 'ㅤ' => 'ᅠ', + 'ㅥ' => 'ᄔ', + 'ㅦ' => 'ᄕ', + 'ㅧ' => 'ᇇ', + 'ㅨ' => 'ᇈ', + 'ㅩ' => 'ᇌ', + 'ㅪ' => 'ᇎ', + 'ㅫ' => 'ᇓ', + 'ㅬ' => 'ᇗ', + 'ㅭ' => 'ᇙ', + 'ㅮ' => 'ᄜ', + 'ㅯ' => 'ᇝ', + 'ㅰ' => 'ᇟ', + 'ㅱ' => 'ᄝ', + 'ㅲ' => 'ᄞ', + 'ㅳ' => 'ᄠ', + 'ㅴ' => 'ᄢ', + 'ㅵ' => 'ᄣ', + 'ㅶ' => 'ᄧ', + 'ㅷ' => 'ᄩ', + 'ㅸ' => 'ᄫ', + 'ㅹ' => 'ᄬ', + 'ㅺ' => 'ᄭ', + 'ㅻ' => 'ᄮ', + 'ㅼ' => 'ᄯ', + 'ㅽ' => 'ᄲ', + 'ㅾ' => 'ᄶ', + 'ㅿ' => 'ᅀ', + 'ㆀ' => 'ᅇ', + 'ㆁ' => 'ᅌ', + 'ㆂ' => 'ᇱ', + 'ㆃ' => 'ᇲ', + 'ㆄ' => 'ᅗ', + 'ㆅ' => 'ᅘ', + 'ㆆ' => 'ᅙ', + 'ㆇ' => 'ᆄ', + 'ㆈ' => 'ᆅ', + 'ㆉ' => 'ᆈ', + 'ㆊ' => 'ᆑ', + 'ㆋ' => 'ᆒ', + 'ㆌ' => 'ᆔ', + 'ㆍ' => 'ᆞ', + 'ㆎ' => 'ᆡ', + '㆒' => '一', + '㆓' => '二', + '㆔' => '三', + '㆕' => '四', + '㆖' => '上', + '㆗' => '中', + '㆘' => '下', + '㆙' => '甲', + '㆚' => '乙', + '㆛' => '丙', + '㆜' => '丁', + '㆝' => '天', + '㆞' => '地', + '㆟' => '人', + '㈀' => '(ᄀ)', + '㈁' => '(ᄂ)', + '㈂' => '(ᄃ)', + '㈃' => '(ᄅ)', + '㈄' => '(ᄆ)', + '㈅' => '(ᄇ)', + '㈆' => '(ᄉ)', + '㈇' => '(ᄋ)', + '㈈' => '(ᄌ)', + '㈉' => '(ᄎ)', + '㈊' => '(ᄏ)', + '㈋' => '(ᄐ)', + '㈌' => '(ᄑ)', + '㈍' => '(ᄒ)', + '㈎' => '(가)', + '㈏' => '(나)', + '㈐' => '(다)', + '㈑' => '(라)', + '㈒' => '(마)', + '㈓' => '(바)', + '㈔' => '(사)', + '㈕' => '(아)', + '㈖' => '(자)', + '㈗' => '(차)', + '㈘' => '(카)', + '㈙' => '(타)', + '㈚' => '(파)', + '㈛' => '(하)', + '㈜' => '(주)', + '㈝' => '(오전)', + '㈞' => '(오후)', + '㈠' => '(一)', + '㈡' => '(二)', + '㈢' => '(三)', + '㈣' => '(四)', + '㈤' => '(五)', + '㈥' => '(六)', + '㈦' => '(七)', + '㈧' => '(八)', + '㈨' => '(九)', + '㈩' => '(十)', + '㈪' => '(月)', + '㈫' => '(火)', + '㈬' => '(水)', + '㈭' => '(木)', + '㈮' => '(金)', + '㈯' => '(土)', + '㈰' => '(日)', + '㈱' => '(株)', + '㈲' => '(有)', + '㈳' => '(社)', + '㈴' => '(名)', + '㈵' => '(特)', + '㈶' => '(財)', + '㈷' => '(祝)', + '㈸' => '(労)', + '㈹' => '(代)', + '㈺' => '(呼)', + '㈻' => '(学)', + '㈼' => '(監)', + '㈽' => '(企)', + '㈾' => '(資)', + '㈿' => '(協)', + '㉀' => '(祭)', + '㉁' => '(休)', + '㉂' => '(自)', + '㉃' => '(至)', + '㉄' => '問', + '㉅' => '幼', + '㉆' => '文', + '㉇' => '箏', + '㉐' => 'PTE', + '㉑' => '21', + '㉒' => '22', + '㉓' => '23', + '㉔' => '24', + '㉕' => '25', + '㉖' => '26', + '㉗' => '27', + '㉘' => '28', + '㉙' => '29', + '㉚' => '30', + '㉛' => '31', + '㉜' => '32', + '㉝' => '33', + '㉞' => '34', + '㉟' => '35', + '㉠' => 'ᄀ', + '㉡' => 'ᄂ', + '㉢' => 'ᄃ', + '㉣' => 'ᄅ', + '㉤' => 'ᄆ', + '㉥' => 'ᄇ', + '㉦' => 'ᄉ', + '㉧' => 'ᄋ', + '㉨' => 'ᄌ', + '㉩' => 'ᄎ', + '㉪' => 'ᄏ', + '㉫' => 'ᄐ', + '㉬' => 'ᄑ', + '㉭' => 'ᄒ', + '㉮' => '가', + '㉯' => '나', + '㉰' => '다', + '㉱' => '라', + '㉲' => '마', + '㉳' => '바', + '㉴' => '사', + '㉵' => '아', + '㉶' => '자', + '㉷' => '차', + '㉸' => '카', + '㉹' => '타', + '㉺' => '파', + '㉻' => '하', + '㉼' => '참고', + '㉽' => '주의', + '㉾' => '우', + '㊀' => '一', + '㊁' => '二', + '㊂' => '三', + '㊃' => '四', + '㊄' => '五', + '㊅' => '六', + '㊆' => '七', + '㊇' => '八', + '㊈' => '九', + '㊉' => '十', + '㊊' => '月', + '㊋' => '火', + '㊌' => '水', + '㊍' => '木', + '㊎' => '金', + '㊏' => '土', + '㊐' => '日', + '㊑' => '株', + '㊒' => '有', + '㊓' => '社', + '㊔' => '名', + '㊕' => '特', + '㊖' => '財', + '㊗' => '祝', + '㊘' => '労', + '㊙' => '秘', + '㊚' => '男', + '㊛' => '女', + '㊜' => '適', + '㊝' => '優', + '㊞' => '印', + '㊟' => '注', + '㊠' => '項', + '㊡' => '休', + '㊢' => '写', + '㊣' => '正', + '㊤' => '上', + '㊥' => '中', + '㊦' => '下', + '㊧' => '左', + '㊨' => '右', + '㊩' => '医', + '㊪' => '宗', + '㊫' => '学', + '㊬' => '監', + '㊭' => '企', + '㊮' => '資', + '㊯' => '協', + '㊰' => '夜', + '㊱' => '36', + '㊲' => '37', + '㊳' => '38', + '㊴' => '39', + '㊵' => '40', + '㊶' => '41', + '㊷' => '42', + '㊸' => '43', + '㊹' => '44', + '㊺' => '45', + '㊻' => '46', + '㊼' => '47', + '㊽' => '48', + '㊾' => '49', + '㊿' => '50', + '㋀' => '1月', + '㋁' => '2月', + '㋂' => '3月', + '㋃' => '4月', + '㋄' => '5月', + '㋅' => '6月', + '㋆' => '7月', + '㋇' => '8月', + '㋈' => '9月', + '㋉' => '10月', + '㋊' => '11月', + '㋋' => '12月', + '㋌' => 'Hg', + '㋍' => 'erg', + '㋎' => 'eV', + '㋏' => 'LTD', + '㋐' => 'ア', + '㋑' => 'イ', + '㋒' => 'ウ', + '㋓' => 'エ', + '㋔' => 'オ', + '㋕' => 'カ', + '㋖' => 'キ', + '㋗' => 'ク', + '㋘' => 'ケ', + '㋙' => 'コ', + '㋚' => 'サ', + '㋛' => 'シ', + '㋜' => 'ス', + '㋝' => 'セ', + '㋞' => 'ソ', + '㋟' => 'タ', + '㋠' => 'チ', + '㋡' => 'ツ', + '㋢' => 'テ', + '㋣' => 'ト', + '㋤' => 'ナ', + '㋥' => 'ニ', + '㋦' => 'ヌ', + '㋧' => 'ネ', + '㋨' => 'ノ', + '㋩' => 'ハ', + '㋪' => 'ヒ', + '㋫' => 'フ', + '㋬' => 'ヘ', + '㋭' => 'ホ', + '㋮' => 'マ', + '㋯' => 'ミ', + '㋰' => 'ム', + '㋱' => 'メ', + '㋲' => 'モ', + '㋳' => 'ヤ', + '㋴' => 'ユ', + '㋵' => 'ヨ', + '㋶' => 'ラ', + '㋷' => 'リ', + '㋸' => 'ル', + '㋹' => 'レ', + '㋺' => 'ロ', + '㋻' => 'ワ', + '㋼' => 'ヰ', + '㋽' => 'ヱ', + '㋾' => 'ヲ', + '㋿' => '令和', + '㌀' => 'アパート', + '㌁' => 'アルファ', + '㌂' => 'アンペア', + '㌃' => 'アール', + '㌄' => 'イニング', + '㌅' => 'インチ', + '㌆' => 'ウォン', + '㌇' => 'エスクード', + '㌈' => 'エーカー', + '㌉' => 'オンス', + '㌊' => 'オーム', + '㌋' => 'カイリ', + '㌌' => 'カラット', + '㌍' => 'カロリー', + '㌎' => 'ガロン', + '㌏' => 'ガンマ', + '㌐' => 'ギガ', + '㌑' => 'ギニー', + '㌒' => 'キュリー', + '㌓' => 'ギルダー', + '㌔' => 'キロ', + '㌕' => 'キログラム', + '㌖' => 'キロメートル', + '㌗' => 'キロワット', + '㌘' => 'グラム', + '㌙' => 'グラムトン', + '㌚' => 'クルゼイロ', + '㌛' => 'クローネ', + '㌜' => 'ケース', + '㌝' => 'コルナ', + '㌞' => 'コーポ', + '㌟' => 'サイクル', + '㌠' => 'サンチーム', + '㌡' => 'シリング', + '㌢' => 'センチ', + '㌣' => 'セント', + '㌤' => 'ダース', + '㌥' => 'デシ', + '㌦' => 'ドル', + '㌧' => 'トン', + '㌨' => 'ナノ', + '㌩' => 'ノット', + '㌪' => 'ハイツ', + '㌫' => 'パーセント', + '㌬' => 'パーツ', + '㌭' => 'バーレル', + '㌮' => 'ピアストル', + '㌯' => 'ピクル', + '㌰' => 'ピコ', + '㌱' => 'ビル', + '㌲' => 'ファラッド', + '㌳' => 'フィート', + '㌴' => 'ブッシェル', + '㌵' => 'フラン', + '㌶' => 'ヘクタール', + '㌷' => 'ペソ', + '㌸' => 'ペニヒ', + '㌹' => 'ヘルツ', + '㌺' => 'ペンス', + '㌻' => 'ページ', + '㌼' => 'ベータ', + '㌽' => 'ポイント', + '㌾' => 'ボルト', + '㌿' => 'ホン', + '㍀' => 'ポンド', + '㍁' => 'ホール', + '㍂' => 'ホーン', + '㍃' => 'マイクロ', + '㍄' => 'マイル', + '㍅' => 'マッハ', + '㍆' => 'マルク', + '㍇' => 'マンション', + '㍈' => 'ミクロン', + '㍉' => 'ミリ', + '㍊' => 'ミリバール', + '㍋' => 'メガ', + '㍌' => 'メガトン', + '㍍' => 'メートル', + '㍎' => 'ヤード', + '㍏' => 'ヤール', + '㍐' => 'ユアン', + '㍑' => 'リットル', + '㍒' => 'リラ', + '㍓' => 'ルピー', + '㍔' => 'ルーブル', + '㍕' => 'レム', + '㍖' => 'レントゲン', + '㍗' => 'ワット', + '㍘' => '0点', + '㍙' => '1点', + '㍚' => '2点', + '㍛' => '3点', + '㍜' => '4点', + '㍝' => '5点', + '㍞' => '6点', + '㍟' => '7点', + '㍠' => '8点', + '㍡' => '9点', + '㍢' => '10点', + '㍣' => '11点', + '㍤' => '12点', + '㍥' => '13点', + '㍦' => '14点', + '㍧' => '15点', + '㍨' => '16点', + '㍩' => '17点', + '㍪' => '18点', + '㍫' => '19点', + '㍬' => '20点', + '㍭' => '21点', + '㍮' => '22点', + '㍯' => '23点', + '㍰' => '24点', + '㍱' => 'hPa', + '㍲' => 'da', + '㍳' => 'AU', + '㍴' => 'bar', + '㍵' => 'oV', + '㍶' => 'pc', + '㍷' => 'dm', + '㍸' => 'dm2', + '㍹' => 'dm3', + '㍺' => 'IU', + '㍻' => '平成', + '㍼' => '昭和', + '㍽' => '大正', + '㍾' => '明治', + '㍿' => '株式会社', + '㎀' => 'pA', + '㎁' => 'nA', + '㎂' => 'μA', + '㎃' => 'mA', + '㎄' => 'kA', + '㎅' => 'KB', + '㎆' => 'MB', + '㎇' => 'GB', + '㎈' => 'cal', + '㎉' => 'kcal', + '㎊' => 'pF', + '㎋' => 'nF', + '㎌' => 'μF', + '㎍' => 'μg', + '㎎' => 'mg', + '㎏' => 'kg', + '㎐' => 'Hz', + '㎑' => 'kHz', + '㎒' => 'MHz', + '㎓' => 'GHz', + '㎔' => 'THz', + '㎕' => 'μl', + '㎖' => 'ml', + '㎗' => 'dl', + '㎘' => 'kl', + '㎙' => 'fm', + '㎚' => 'nm', + '㎛' => 'μm', + '㎜' => 'mm', + '㎝' => 'cm', + '㎞' => 'km', + '㎟' => 'mm2', + '㎠' => 'cm2', + '㎡' => 'm2', + '㎢' => 'km2', + '㎣' => 'mm3', + '㎤' => 'cm3', + '㎥' => 'm3', + '㎦' => 'km3', + '㎧' => 'm∕s', + '㎨' => 'm∕s2', + '㎩' => 'Pa', + '㎪' => 'kPa', + '㎫' => 'MPa', + '㎬' => 'GPa', + '㎭' => 'rad', + '㎮' => 'rad∕s', + '㎯' => 'rad∕s2', + '㎰' => 'ps', + '㎱' => 'ns', + '㎲' => 'μs', + '㎳' => 'ms', + '㎴' => 'pV', + '㎵' => 'nV', + '㎶' => 'μV', + '㎷' => 'mV', + '㎸' => 'kV', + '㎹' => 'MV', + '㎺' => 'pW', + '㎻' => 'nW', + '㎼' => 'μW', + '㎽' => 'mW', + '㎾' => 'kW', + '㎿' => 'MW', + '㏀' => 'kΩ', + '㏁' => 'MΩ', + '㏂' => 'a.m.', + '㏃' => 'Bq', + '㏄' => 'cc', + '㏅' => 'cd', + '㏆' => 'C∕kg', + '㏇' => 'Co.', + '㏈' => 'dB', + '㏉' => 'Gy', + '㏊' => 'ha', + '㏋' => 'HP', + '㏌' => 'in', + '㏍' => 'KK', + '㏎' => 'KM', + '㏏' => 'kt', + '㏐' => 'lm', + '㏑' => 'ln', + '㏒' => 'log', + '㏓' => 'lx', + '㏔' => 'mb', + '㏕' => 'mil', + '㏖' => 'mol', + '㏗' => 'PH', + '㏘' => 'p.m.', + '㏙' => 'PPM', + '㏚' => 'PR', + '㏛' => 'sr', + '㏜' => 'Sv', + '㏝' => 'Wb', + '㏞' => 'V∕m', + '㏟' => 'A∕m', + '㏠' => '1日', + '㏡' => '2日', + '㏢' => '3日', + '㏣' => '4日', + '㏤' => '5日', + '㏥' => '6日', + '㏦' => '7日', + '㏧' => '8日', + '㏨' => '9日', + '㏩' => '10日', + '㏪' => '11日', + '㏫' => '12日', + '㏬' => '13日', + '㏭' => '14日', + '㏮' => '15日', + '㏯' => '16日', + '㏰' => '17日', + '㏱' => '18日', + '㏲' => '19日', + '㏳' => '20日', + '㏴' => '21日', + '㏵' => '22日', + '㏶' => '23日', + '㏷' => '24日', + '㏸' => '25日', + '㏹' => '26日', + '㏺' => '27日', + '㏻' => '28日', + '㏼' => '29日', + '㏽' => '30日', + '㏾' => '31日', + '㏿' => 'gal', + 'ꚜ' => 'ъ', + 'ꚝ' => 'ь', + 'ꝰ' => 'ꝯ', + 'ꟸ' => 'Ħ', + 'ꟹ' => 'œ', + 'ꭜ' => 'ꜧ', + 'ꭝ' => 'ꬷ', + 'ꭞ' => 'ɫ', + 'ꭟ' => 'ꭒ', + 'ꭩ' => 'ʍ', + 'ff' => 'ff', + 'fi' => 'fi', + 'fl' => 'fl', + 'ffi' => 'ffi', + 'ffl' => 'ffl', + 'ſt' => 'st', + 'st' => 'st', + 'ﬓ' => 'մն', + 'ﬔ' => 'մե', + 'ﬕ' => 'մի', + 'ﬖ' => 'վն', + 'ﬗ' => 'մխ', + 'ﬠ' => 'ע', + 'ﬡ' => 'א', + 'ﬢ' => 'ד', + 'ﬣ' => 'ה', + 'ﬤ' => 'כ', + 'ﬥ' => 'ל', + 'ﬦ' => 'ם', + 'ﬧ' => 'ר', + 'ﬨ' => 'ת', + '﬩' => '+', + 'ﭏ' => 'אל', + 'ﭐ' => 'ٱ', + 'ﭑ' => 'ٱ', + 'ﭒ' => 'ٻ', + 'ﭓ' => 'ٻ', + 'ﭔ' => 'ٻ', + 'ﭕ' => 'ٻ', + 'ﭖ' => 'پ', + 'ﭗ' => 'پ', + 'ﭘ' => 'پ', + 'ﭙ' => 'پ', + 'ﭚ' => 'ڀ', + 'ﭛ' => 'ڀ', + 'ﭜ' => 'ڀ', + 'ﭝ' => 'ڀ', + 'ﭞ' => 'ٺ', + 'ﭟ' => 'ٺ', + 'ﭠ' => 'ٺ', + 'ﭡ' => 'ٺ', + 'ﭢ' => 'ٿ', + 'ﭣ' => 'ٿ', + 'ﭤ' => 'ٿ', + 'ﭥ' => 'ٿ', + 'ﭦ' => 'ٹ', + 'ﭧ' => 'ٹ', + 'ﭨ' => 'ٹ', + 'ﭩ' => 'ٹ', + 'ﭪ' => 'ڤ', + 'ﭫ' => 'ڤ', + 'ﭬ' => 'ڤ', + 'ﭭ' => 'ڤ', + 'ﭮ' => 'ڦ', + 'ﭯ' => 'ڦ', + 'ﭰ' => 'ڦ', + 'ﭱ' => 'ڦ', + 'ﭲ' => 'ڄ', + 'ﭳ' => 'ڄ', + 'ﭴ' => 'ڄ', + 'ﭵ' => 'ڄ', + 'ﭶ' => 'ڃ', + 'ﭷ' => 'ڃ', + 'ﭸ' => 'ڃ', + 'ﭹ' => 'ڃ', + 'ﭺ' => 'چ', + 'ﭻ' => 'چ', + 'ﭼ' => 'چ', + 'ﭽ' => 'چ', + 'ﭾ' => 'ڇ', + 'ﭿ' => 'ڇ', + 'ﮀ' => 'ڇ', + 'ﮁ' => 'ڇ', + 'ﮂ' => 'ڍ', + 'ﮃ' => 'ڍ', + 'ﮄ' => 'ڌ', + 'ﮅ' => 'ڌ', + 'ﮆ' => 'ڎ', + 'ﮇ' => 'ڎ', + 'ﮈ' => 'ڈ', + 'ﮉ' => 'ڈ', + 'ﮊ' => 'ژ', + 'ﮋ' => 'ژ', + 'ﮌ' => 'ڑ', + 'ﮍ' => 'ڑ', + 'ﮎ' => 'ک', + 'ﮏ' => 'ک', + 'ﮐ' => 'ک', + 'ﮑ' => 'ک', + 'ﮒ' => 'گ', + 'ﮓ' => 'گ', + 'ﮔ' => 'گ', + 'ﮕ' => 'گ', + 'ﮖ' => 'ڳ', + 'ﮗ' => 'ڳ', + 'ﮘ' => 'ڳ', + 'ﮙ' => 'ڳ', + 'ﮚ' => 'ڱ', + 'ﮛ' => 'ڱ', + 'ﮜ' => 'ڱ', + 'ﮝ' => 'ڱ', + 'ﮞ' => 'ں', + 'ﮟ' => 'ں', + 'ﮠ' => 'ڻ', + 'ﮡ' => 'ڻ', + 'ﮢ' => 'ڻ', + 'ﮣ' => 'ڻ', + 'ﮤ' => 'ۀ', + 'ﮥ' => 'ۀ', + 'ﮦ' => 'ہ', + 'ﮧ' => 'ہ', + 'ﮨ' => 'ہ', + 'ﮩ' => 'ہ', + 'ﮪ' => 'ھ', + 'ﮫ' => 'ھ', + 'ﮬ' => 'ھ', + 'ﮭ' => 'ھ', + 'ﮮ' => 'ے', + 'ﮯ' => 'ے', + 'ﮰ' => 'ۓ', + 'ﮱ' => 'ۓ', + 'ﯓ' => 'ڭ', + 'ﯔ' => 'ڭ', + 'ﯕ' => 'ڭ', + 'ﯖ' => 'ڭ', + 'ﯗ' => 'ۇ', + 'ﯘ' => 'ۇ', + 'ﯙ' => 'ۆ', + 'ﯚ' => 'ۆ', + 'ﯛ' => 'ۈ', + 'ﯜ' => 'ۈ', + 'ﯝ' => 'ۇٴ', + 'ﯞ' => 'ۋ', + 'ﯟ' => 'ۋ', + 'ﯠ' => 'ۅ', + 'ﯡ' => 'ۅ', + 'ﯢ' => 'ۉ', + 'ﯣ' => 'ۉ', + 'ﯤ' => 'ې', + 'ﯥ' => 'ې', + 'ﯦ' => 'ې', + 'ﯧ' => 'ې', + 'ﯨ' => 'ى', + 'ﯩ' => 'ى', + 'ﯪ' => 'ئا', + 'ﯫ' => 'ئا', + 'ﯬ' => 'ئە', + 'ﯭ' => 'ئە', + 'ﯮ' => 'ئو', + 'ﯯ' => 'ئو', + 'ﯰ' => 'ئۇ', + 'ﯱ' => 'ئۇ', + 'ﯲ' => 'ئۆ', + 'ﯳ' => 'ئۆ', + 'ﯴ' => 'ئۈ', + 'ﯵ' => 'ئۈ', + 'ﯶ' => 'ئې', + 'ﯷ' => 'ئې', + 'ﯸ' => 'ئې', + 'ﯹ' => 'ئى', + 'ﯺ' => 'ئى', + 'ﯻ' => 'ئى', + 'ﯼ' => 'ی', + 'ﯽ' => 'ی', + 'ﯾ' => 'ی', + 'ﯿ' => 'ی', + 'ﰀ' => 'ئج', + 'ﰁ' => 'ئح', + 'ﰂ' => 'ئم', + 'ﰃ' => 'ئى', + 'ﰄ' => 'ئي', + 'ﰅ' => 'بج', + 'ﰆ' => 'بح', + 'ﰇ' => 'بخ', + 'ﰈ' => 'بم', + 'ﰉ' => 'بى', + 'ﰊ' => 'بي', + 'ﰋ' => 'تج', + 'ﰌ' => 'تح', + 'ﰍ' => 'تخ', + 'ﰎ' => 'تم', + 'ﰏ' => 'تى', + 'ﰐ' => 'تي', + 'ﰑ' => 'ثج', + 'ﰒ' => 'ثم', + 'ﰓ' => 'ثى', + 'ﰔ' => 'ثي', + 'ﰕ' => 'جح', + 'ﰖ' => 'جم', + 'ﰗ' => 'حج', + 'ﰘ' => 'حم', + 'ﰙ' => 'خج', + 'ﰚ' => 'خح', + 'ﰛ' => 'خم', + 'ﰜ' => 'سج', + 'ﰝ' => 'سح', + 'ﰞ' => 'سخ', + 'ﰟ' => 'سم', + 'ﰠ' => 'صح', + 'ﰡ' => 'صم', + 'ﰢ' => 'ضج', + 'ﰣ' => 'ضح', + 'ﰤ' => 'ضخ', + 'ﰥ' => 'ضم', + 'ﰦ' => 'طح', + 'ﰧ' => 'طم', + 'ﰨ' => 'ظم', + 'ﰩ' => 'عج', + 'ﰪ' => 'عم', + 'ﰫ' => 'غج', + 'ﰬ' => 'غم', + 'ﰭ' => 'فج', + 'ﰮ' => 'فح', + 'ﰯ' => 'فخ', + 'ﰰ' => 'فم', + 'ﰱ' => 'فى', + 'ﰲ' => 'في', + 'ﰳ' => 'قح', + 'ﰴ' => 'قم', + 'ﰵ' => 'قى', + 'ﰶ' => 'قي', + 'ﰷ' => 'كا', + 'ﰸ' => 'كج', + 'ﰹ' => 'كح', + 'ﰺ' => 'كخ', + 'ﰻ' => 'كل', + 'ﰼ' => 'كم', + 'ﰽ' => 'كى', + 'ﰾ' => 'كي', + 'ﰿ' => 'لج', + 'ﱀ' => 'لح', + 'ﱁ' => 'لخ', + 'ﱂ' => 'لم', + 'ﱃ' => 'لى', + 'ﱄ' => 'لي', + 'ﱅ' => 'مج', + 'ﱆ' => 'مح', + 'ﱇ' => 'مخ', + 'ﱈ' => 'مم', + 'ﱉ' => 'مى', + 'ﱊ' => 'مي', + 'ﱋ' => 'نج', + 'ﱌ' => 'نح', + 'ﱍ' => 'نخ', + 'ﱎ' => 'نم', + 'ﱏ' => 'نى', + 'ﱐ' => 'ني', + 'ﱑ' => 'هج', + 'ﱒ' => 'هم', + 'ﱓ' => 'هى', + 'ﱔ' => 'هي', + 'ﱕ' => 'يج', + 'ﱖ' => 'يح', + 'ﱗ' => 'يخ', + 'ﱘ' => 'يم', + 'ﱙ' => 'يى', + 'ﱚ' => 'يي', + 'ﱛ' => 'ذٰ', + 'ﱜ' => 'رٰ', + 'ﱝ' => 'ىٰ', + 'ﱞ' => ' ٌّ', + 'ﱟ' => ' ٍّ', + 'ﱠ' => ' َّ', + 'ﱡ' => ' ُّ', + 'ﱢ' => ' ِّ', + 'ﱣ' => ' ّٰ', + 'ﱤ' => 'ئر', + 'ﱥ' => 'ئز', + 'ﱦ' => 'ئم', + 'ﱧ' => 'ئن', + 'ﱨ' => 'ئى', + 'ﱩ' => 'ئي', + 'ﱪ' => 'بر', + 'ﱫ' => 'بز', + 'ﱬ' => 'بم', + 'ﱭ' => 'بن', + 'ﱮ' => 'بى', + 'ﱯ' => 'بي', + 'ﱰ' => 'تر', + 'ﱱ' => 'تز', + 'ﱲ' => 'تم', + 'ﱳ' => 'تن', + 'ﱴ' => 'تى', + 'ﱵ' => 'تي', + 'ﱶ' => 'ثر', + 'ﱷ' => 'ثز', + 'ﱸ' => 'ثم', + 'ﱹ' => 'ثن', + 'ﱺ' => 'ثى', + 'ﱻ' => 'ثي', + 'ﱼ' => 'فى', + 'ﱽ' => 'في', + 'ﱾ' => 'قى', + 'ﱿ' => 'قي', + 'ﲀ' => 'كا', + 'ﲁ' => 'كل', + 'ﲂ' => 'كم', + 'ﲃ' => 'كى', + 'ﲄ' => 'كي', + 'ﲅ' => 'لم', + 'ﲆ' => 'لى', + 'ﲇ' => 'لي', + 'ﲈ' => 'ما', + 'ﲉ' => 'مم', + 'ﲊ' => 'نر', + 'ﲋ' => 'نز', + 'ﲌ' => 'نم', + 'ﲍ' => 'نن', + 'ﲎ' => 'نى', + 'ﲏ' => 'ني', + 'ﲐ' => 'ىٰ', + 'ﲑ' => 'ير', + 'ﲒ' => 'يز', + 'ﲓ' => 'يم', + 'ﲔ' => 'ين', + 'ﲕ' => 'يى', + 'ﲖ' => 'يي', + 'ﲗ' => 'ئج', + 'ﲘ' => 'ئح', + 'ﲙ' => 'ئخ', + 'ﲚ' => 'ئم', + 'ﲛ' => 'ئه', + 'ﲜ' => 'بج', + 'ﲝ' => 'بح', + 'ﲞ' => 'بخ', + 'ﲟ' => 'بم', + 'ﲠ' => 'به', + 'ﲡ' => 'تج', + 'ﲢ' => 'تح', + 'ﲣ' => 'تخ', + 'ﲤ' => 'تم', + 'ﲥ' => 'ته', + 'ﲦ' => 'ثم', + 'ﲧ' => 'جح', + 'ﲨ' => 'جم', + 'ﲩ' => 'حج', + 'ﲪ' => 'حم', + 'ﲫ' => 'خج', + 'ﲬ' => 'خم', + 'ﲭ' => 'سج', + 'ﲮ' => 'سح', + 'ﲯ' => 'سخ', + 'ﲰ' => 'سم', + 'ﲱ' => 'صح', + 'ﲲ' => 'صخ', + 'ﲳ' => 'صم', + 'ﲴ' => 'ضج', + 'ﲵ' => 'ضح', + 'ﲶ' => 'ضخ', + 'ﲷ' => 'ضم', + 'ﲸ' => 'طح', + 'ﲹ' => 'ظم', + 'ﲺ' => 'عج', + 'ﲻ' => 'عم', + 'ﲼ' => 'غج', + 'ﲽ' => 'غم', + 'ﲾ' => 'فج', + 'ﲿ' => 'فح', + 'ﳀ' => 'فخ', + 'ﳁ' => 'فم', + 'ﳂ' => 'قح', + 'ﳃ' => 'قم', + 'ﳄ' => 'كج', + 'ﳅ' => 'كح', + 'ﳆ' => 'كخ', + 'ﳇ' => 'كل', + 'ﳈ' => 'كم', + 'ﳉ' => 'لج', + 'ﳊ' => 'لح', + 'ﳋ' => 'لخ', + 'ﳌ' => 'لم', + 'ﳍ' => 'له', + 'ﳎ' => 'مج', + 'ﳏ' => 'مح', + 'ﳐ' => 'مخ', + 'ﳑ' => 'مم', + 'ﳒ' => 'نج', + 'ﳓ' => 'نح', + 'ﳔ' => 'نخ', + 'ﳕ' => 'نم', + 'ﳖ' => 'نه', + 'ﳗ' => 'هج', + 'ﳘ' => 'هم', + 'ﳙ' => 'هٰ', + 'ﳚ' => 'يج', + 'ﳛ' => 'يح', + 'ﳜ' => 'يخ', + 'ﳝ' => 'يم', + 'ﳞ' => 'يه', + 'ﳟ' => 'ئم', + 'ﳠ' => 'ئه', + 'ﳡ' => 'بم', + 'ﳢ' => 'به', + 'ﳣ' => 'تم', + 'ﳤ' => 'ته', + 'ﳥ' => 'ثم', + 'ﳦ' => 'ثه', + 'ﳧ' => 'سم', + 'ﳨ' => 'سه', + 'ﳩ' => 'شم', + 'ﳪ' => 'شه', + 'ﳫ' => 'كل', + 'ﳬ' => 'كم', + 'ﳭ' => 'لم', + 'ﳮ' => 'نم', + 'ﳯ' => 'نه', + 'ﳰ' => 'يم', + 'ﳱ' => 'يه', + 'ﳲ' => 'ـَّ', + 'ﳳ' => 'ـُّ', + 'ﳴ' => 'ـِّ', + 'ﳵ' => 'طى', + 'ﳶ' => 'طي', + 'ﳷ' => 'عى', + 'ﳸ' => 'عي', + 'ﳹ' => 'غى', + 'ﳺ' => 'غي', + 'ﳻ' => 'سى', + 'ﳼ' => 'سي', + 'ﳽ' => 'شى', + 'ﳾ' => 'شي', + 'ﳿ' => 'حى', + 'ﴀ' => 'حي', + 'ﴁ' => 'جى', + 'ﴂ' => 'جي', + 'ﴃ' => 'خى', + 'ﴄ' => 'خي', + 'ﴅ' => 'صى', + 'ﴆ' => 'صي', + 'ﴇ' => 'ضى', + 'ﴈ' => 'ضي', + 'ﴉ' => 'شج', + 'ﴊ' => 'شح', + 'ﴋ' => 'شخ', + 'ﴌ' => 'شم', + 'ﴍ' => 'شر', + 'ﴎ' => 'سر', + 'ﴏ' => 'صر', + 'ﴐ' => 'ضر', + 'ﴑ' => 'طى', + 'ﴒ' => 'طي', + 'ﴓ' => 'عى', + 'ﴔ' => 'عي', + 'ﴕ' => 'غى', + 'ﴖ' => 'غي', + 'ﴗ' => 'سى', + 'ﴘ' => 'سي', + 'ﴙ' => 'شى', + 'ﴚ' => 'شي', + 'ﴛ' => 'حى', + 'ﴜ' => 'حي', + 'ﴝ' => 'جى', + 'ﴞ' => 'جي', + 'ﴟ' => 'خى', + 'ﴠ' => 'خي', + 'ﴡ' => 'صى', + 'ﴢ' => 'صي', + 'ﴣ' => 'ضى', + 'ﴤ' => 'ضي', + 'ﴥ' => 'شج', + 'ﴦ' => 'شح', + 'ﴧ' => 'شخ', + 'ﴨ' => 'شم', + 'ﴩ' => 'شر', + 'ﴪ' => 'سر', + 'ﴫ' => 'صر', + 'ﴬ' => 'ضر', + 'ﴭ' => 'شج', + 'ﴮ' => 'شح', + 'ﴯ' => 'شخ', + 'ﴰ' => 'شم', + 'ﴱ' => 'سه', + 'ﴲ' => 'شه', + 'ﴳ' => 'طم', + 'ﴴ' => 'سج', + 'ﴵ' => 'سح', + 'ﴶ' => 'سخ', + 'ﴷ' => 'شج', + 'ﴸ' => 'شح', + 'ﴹ' => 'شخ', + 'ﴺ' => 'طم', + 'ﴻ' => 'ظم', + 'ﴼ' => 'اً', + 'ﴽ' => 'اً', + 'ﵐ' => 'تجم', + 'ﵑ' => 'تحج', + 'ﵒ' => 'تحج', + 'ﵓ' => 'تحم', + 'ﵔ' => 'تخم', + 'ﵕ' => 'تمج', + 'ﵖ' => 'تمح', + 'ﵗ' => 'تمخ', + 'ﵘ' => 'جمح', + 'ﵙ' => 'جمح', + 'ﵚ' => 'حمي', + 'ﵛ' => 'حمى', + 'ﵜ' => 'سحج', + 'ﵝ' => 'سجح', + 'ﵞ' => 'سجى', + 'ﵟ' => 'سمح', + 'ﵠ' => 'سمح', + 'ﵡ' => 'سمج', + 'ﵢ' => 'سمم', + 'ﵣ' => 'سمم', + 'ﵤ' => 'صحح', + 'ﵥ' => 'صحح', + 'ﵦ' => 'صمم', + 'ﵧ' => 'شحم', + 'ﵨ' => 'شحم', + 'ﵩ' => 'شجي', + 'ﵪ' => 'شمخ', + 'ﵫ' => 'شمخ', + 'ﵬ' => 'شمم', + 'ﵭ' => 'شمم', + 'ﵮ' => 'ضحى', + 'ﵯ' => 'ضخم', + 'ﵰ' => 'ضخم', + 'ﵱ' => 'طمح', + 'ﵲ' => 'طمح', + 'ﵳ' => 'طمم', + 'ﵴ' => 'طمي', + 'ﵵ' => 'عجم', + 'ﵶ' => 'عمم', + 'ﵷ' => 'عمم', + 'ﵸ' => 'عمى', + 'ﵹ' => 'غمم', + 'ﵺ' => 'غمي', + 'ﵻ' => 'غمى', + 'ﵼ' => 'فخم', + 'ﵽ' => 'فخم', + 'ﵾ' => 'قمح', + 'ﵿ' => 'قمم', + 'ﶀ' => 'لحم', + 'ﶁ' => 'لحي', + 'ﶂ' => 'لحى', + 'ﶃ' => 'لجج', + 'ﶄ' => 'لجج', + 'ﶅ' => 'لخم', + 'ﶆ' => 'لخم', + 'ﶇ' => 'لمح', + 'ﶈ' => 'لمح', + 'ﶉ' => 'محج', + 'ﶊ' => 'محم', + 'ﶋ' => 'محي', + 'ﶌ' => 'مجح', + 'ﶍ' => 'مجم', + 'ﶎ' => 'مخج', + 'ﶏ' => 'مخم', + 'ﶒ' => 'مجخ', + 'ﶓ' => 'همج', + 'ﶔ' => 'همم', + 'ﶕ' => 'نحم', + 'ﶖ' => 'نحى', + 'ﶗ' => 'نجم', + 'ﶘ' => 'نجم', + 'ﶙ' => 'نجى', + 'ﶚ' => 'نمي', + 'ﶛ' => 'نمى', + 'ﶜ' => 'يمم', + 'ﶝ' => 'يمم', + 'ﶞ' => 'بخي', + 'ﶟ' => 'تجي', + 'ﶠ' => 'تجى', + 'ﶡ' => 'تخي', + 'ﶢ' => 'تخى', + 'ﶣ' => 'تمي', + 'ﶤ' => 'تمى', + 'ﶥ' => 'جمي', + 'ﶦ' => 'جحى', + 'ﶧ' => 'جمى', + 'ﶨ' => 'سخى', + 'ﶩ' => 'صحي', + 'ﶪ' => 'شحي', + 'ﶫ' => 'ضحي', + 'ﶬ' => 'لجي', + 'ﶭ' => 'لمي', + 'ﶮ' => 'يحي', + 'ﶯ' => 'يجي', + 'ﶰ' => 'يمي', + 'ﶱ' => 'ممي', + 'ﶲ' => 'قمي', + 'ﶳ' => 'نحي', + 'ﶴ' => 'قمح', + 'ﶵ' => 'لحم', + 'ﶶ' => 'عمي', + 'ﶷ' => 'كمي', + 'ﶸ' => 'نجح', + 'ﶹ' => 'مخي', + 'ﶺ' => 'لجم', + 'ﶻ' => 'كمم', + 'ﶼ' => 'لجم', + 'ﶽ' => 'نجح', + 'ﶾ' => 'جحي', + 'ﶿ' => 'حجي', + 'ﷀ' => 'مجي', + 'ﷁ' => 'فمي', + 'ﷂ' => 'بحي', + 'ﷃ' => 'كمم', + 'ﷄ' => 'عجم', + 'ﷅ' => 'صمم', + 'ﷆ' => 'سخي', + 'ﷇ' => 'نجي', + 'ﷰ' => 'صلے', + 'ﷱ' => 'قلے', + 'ﷲ' => 'الله', + 'ﷳ' => 'اكبر', + 'ﷴ' => 'محمد', + 'ﷵ' => 'صلعم', + 'ﷶ' => 'رسول', + 'ﷷ' => 'عليه', + 'ﷸ' => 'وسلم', + 'ﷹ' => 'صلى', + 'ﷺ' => 'صلى الله عليه وسلم', + 'ﷻ' => 'جل جلاله', + '﷼' => 'ریال', + '︐' => ',', + '︑' => '、', + '︒' => '。', + '︓' => ':', + '︔' => ';', + '︕' => '!', + '︖' => '?', + '︗' => '〖', + '︘' => '〗', + '︙' => '...', + '︰' => '..', + '︱' => '—', + '︲' => '–', + '︳' => '_', + '︴' => '_', + '︵' => '(', + '︶' => ')', + '︷' => '{', + '︸' => '}', + '︹' => '〔', + '︺' => '〕', + '︻' => '【', + '︼' => '】', + '︽' => '《', + '︾' => '》', + '︿' => '〈', + '﹀' => '〉', + '﹁' => '「', + '﹂' => '」', + '﹃' => '『', + '﹄' => '』', + '﹇' => '[', + '﹈' => ']', + '﹉' => ' ̅', + '﹊' => ' ̅', + '﹋' => ' ̅', + '﹌' => ' ̅', + '﹍' => '_', + '﹎' => '_', + '﹏' => '_', + '﹐' => ',', + '﹑' => '、', + '﹒' => '.', + '﹔' => ';', + '﹕' => ':', + '﹖' => '?', + '﹗' => '!', + '﹘' => '—', + '﹙' => '(', + '﹚' => ')', + '﹛' => '{', + '﹜' => '}', + '﹝' => '〔', + '﹞' => '〕', + '﹟' => '#', + '﹠' => '&', + '﹡' => '*', + '﹢' => '+', + '﹣' => '-', + '﹤' => '<', + '﹥' => '>', + '﹦' => '=', + '﹨' => '\\', + '﹩' => '$', + '﹪' => '%', + '﹫' => '@', + 'ﹰ' => ' ً', + 'ﹱ' => 'ـً', + 'ﹲ' => ' ٌ', + 'ﹴ' => ' ٍ', + 'ﹶ' => ' َ', + 'ﹷ' => 'ـَ', + 'ﹸ' => ' ُ', + 'ﹹ' => 'ـُ', + 'ﹺ' => ' ِ', + 'ﹻ' => 'ـِ', + 'ﹼ' => ' ّ', + 'ﹽ' => 'ـّ', + 'ﹾ' => ' ْ', + 'ﹿ' => 'ـْ', + 'ﺀ' => 'ء', + 'ﺁ' => 'آ', + 'ﺂ' => 'آ', + 'ﺃ' => 'أ', + 'ﺄ' => 'أ', + 'ﺅ' => 'ؤ', + 'ﺆ' => 'ؤ', + 'ﺇ' => 'إ', + 'ﺈ' => 'إ', + 'ﺉ' => 'ئ', + 'ﺊ' => 'ئ', + 'ﺋ' => 'ئ', + 'ﺌ' => 'ئ', + 'ﺍ' => 'ا', + 'ﺎ' => 'ا', + 'ﺏ' => 'ب', + 'ﺐ' => 'ب', + 'ﺑ' => 'ب', + 'ﺒ' => 'ب', + 'ﺓ' => 'ة', + 'ﺔ' => 'ة', + 'ﺕ' => 'ت', + 'ﺖ' => 'ت', + 'ﺗ' => 'ت', + 'ﺘ' => 'ت', + 'ﺙ' => 'ث', + 'ﺚ' => 'ث', + 'ﺛ' => 'ث', + 'ﺜ' => 'ث', + 'ﺝ' => 'ج', + 'ﺞ' => 'ج', + 'ﺟ' => 'ج', + 'ﺠ' => 'ج', + 'ﺡ' => 'ح', + 'ﺢ' => 'ح', + 'ﺣ' => 'ح', + 'ﺤ' => 'ح', + 'ﺥ' => 'خ', + 'ﺦ' => 'خ', + 'ﺧ' => 'خ', + 'ﺨ' => 'خ', + 'ﺩ' => 'د', + 'ﺪ' => 'د', + 'ﺫ' => 'ذ', + 'ﺬ' => 'ذ', + 'ﺭ' => 'ر', + 'ﺮ' => 'ر', + 'ﺯ' => 'ز', + 'ﺰ' => 'ز', + 'ﺱ' => 'س', + 'ﺲ' => 'س', + 'ﺳ' => 'س', + 'ﺴ' => 'س', + 'ﺵ' => 'ش', + 'ﺶ' => 'ش', + 'ﺷ' => 'ش', + 'ﺸ' => 'ش', + 'ﺹ' => 'ص', + 'ﺺ' => 'ص', + 'ﺻ' => 'ص', + 'ﺼ' => 'ص', + 'ﺽ' => 'ض', + 'ﺾ' => 'ض', + 'ﺿ' => 'ض', + 'ﻀ' => 'ض', + 'ﻁ' => 'ط', + 'ﻂ' => 'ط', + 'ﻃ' => 'ط', + 'ﻄ' => 'ط', + 'ﻅ' => 'ظ', + 'ﻆ' => 'ظ', + 'ﻇ' => 'ظ', + 'ﻈ' => 'ظ', + 'ﻉ' => 'ع', + 'ﻊ' => 'ع', + 'ﻋ' => 'ع', + 'ﻌ' => 'ع', + 'ﻍ' => 'غ', + 'ﻎ' => 'غ', + 'ﻏ' => 'غ', + 'ﻐ' => 'غ', + 'ﻑ' => 'ف', + 'ﻒ' => 'ف', + 'ﻓ' => 'ف', + 'ﻔ' => 'ف', + 'ﻕ' => 'ق', + 'ﻖ' => 'ق', + 'ﻗ' => 'ق', + 'ﻘ' => 'ق', + 'ﻙ' => 'ك', + 'ﻚ' => 'ك', + 'ﻛ' => 'ك', + 'ﻜ' => 'ك', + 'ﻝ' => 'ل', + 'ﻞ' => 'ل', + 'ﻟ' => 'ل', + 'ﻠ' => 'ل', + 'ﻡ' => 'م', + 'ﻢ' => 'م', + 'ﻣ' => 'م', + 'ﻤ' => 'م', + 'ﻥ' => 'ن', + 'ﻦ' => 'ن', + 'ﻧ' => 'ن', + 'ﻨ' => 'ن', + 'ﻩ' => 'ه', + 'ﻪ' => 'ه', + 'ﻫ' => 'ه', + 'ﻬ' => 'ه', + 'ﻭ' => 'و', + 'ﻮ' => 'و', + 'ﻯ' => 'ى', + 'ﻰ' => 'ى', + 'ﻱ' => 'ي', + 'ﻲ' => 'ي', + 'ﻳ' => 'ي', + 'ﻴ' => 'ي', + 'ﻵ' => 'لآ', + 'ﻶ' => 'لآ', + 'ﻷ' => 'لأ', + 'ﻸ' => 'لأ', + 'ﻹ' => 'لإ', + 'ﻺ' => 'لإ', + 'ﻻ' => 'لا', + 'ﻼ' => 'لا', + '!' => '!', + '"' => '"', + '#' => '#', + '$' => '$', + '%' => '%', + '&' => '&', + ''' => '\'', + '(' => '(', + ')' => ')', + '*' => '*', + '+' => '+', + ',' => ',', + '-' => '-', + '.' => '.', + '/' => '/', + '0' => '0', + '1' => '1', + '2' => '2', + '3' => '3', + '4' => '4', + '5' => '5', + '6' => '6', + '7' => '7', + '8' => '8', + '9' => '9', + ':' => ':', + ';' => ';', + '<' => '<', + '=' => '=', + '>' => '>', + '?' => '?', + '@' => '@', + 'A' => 'A', + 'B' => 'B', + 'C' => 'C', + 'D' => 'D', + 'E' => 'E', + 'F' => 'F', + 'G' => 'G', + 'H' => 'H', + 'I' => 'I', + 'J' => 'J', + 'K' => 'K', + 'L' => 'L', + 'M' => 'M', + 'N' => 'N', + 'O' => 'O', + 'P' => 'P', + 'Q' => 'Q', + 'R' => 'R', + 'S' => 'S', + 'T' => 'T', + 'U' => 'U', + 'V' => 'V', + 'W' => 'W', + 'X' => 'X', + 'Y' => 'Y', + 'Z' => 'Z', + '[' => '[', + '\' => '\\', + ']' => ']', + '^' => '^', + '_' => '_', + '`' => '`', + 'a' => 'a', + 'b' => 'b', + 'c' => 'c', + 'd' => 'd', + 'e' => 'e', + 'f' => 'f', + 'g' => 'g', + 'h' => 'h', + 'i' => 'i', + 'j' => 'j', + 'k' => 'k', + 'l' => 'l', + 'm' => 'm', + 'n' => 'n', + 'o' => 'o', + 'p' => 'p', + 'q' => 'q', + 'r' => 'r', + 's' => 's', + 't' => 't', + 'u' => 'u', + 'v' => 'v', + 'w' => 'w', + 'x' => 'x', + 'y' => 'y', + 'z' => 'z', + '{' => '{', + '|' => '|', + '}' => '}', + '~' => '~', + '⦅' => '⦅', + '⦆' => '⦆', + '。' => '。', + '「' => '「', + '」' => '」', + '、' => '、', + '・' => '・', + 'ヲ' => 'ヲ', + 'ァ' => 'ァ', + 'ィ' => 'ィ', + 'ゥ' => 'ゥ', + 'ェ' => 'ェ', + 'ォ' => 'ォ', + 'ャ' => 'ャ', + 'ュ' => 'ュ', + 'ョ' => 'ョ', + 'ッ' => 'ッ', + 'ー' => 'ー', + 'ア' => 'ア', + 'イ' => 'イ', + 'ウ' => 'ウ', + 'エ' => 'エ', + 'オ' => 'オ', + 'カ' => 'カ', + 'キ' => 'キ', + 'ク' => 'ク', + 'ケ' => 'ケ', + 'コ' => 'コ', + 'サ' => 'サ', + 'シ' => 'シ', + 'ス' => 'ス', + 'セ' => 'セ', + 'ソ' => 'ソ', + 'タ' => 'タ', + 'チ' => 'チ', + 'ツ' => 'ツ', + 'テ' => 'テ', + 'ト' => 'ト', + 'ナ' => 'ナ', + 'ニ' => 'ニ', + 'ヌ' => 'ヌ', + 'ネ' => 'ネ', + 'ノ' => 'ノ', + 'ハ' => 'ハ', + 'ヒ' => 'ヒ', + 'フ' => 'フ', + 'ヘ' => 'ヘ', + 'ホ' => 'ホ', + 'マ' => 'マ', + 'ミ' => 'ミ', + 'ム' => 'ム', + 'メ' => 'メ', + 'モ' => 'モ', + 'ヤ' => 'ヤ', + 'ユ' => 'ユ', + 'ヨ' => 'ヨ', + 'ラ' => 'ラ', + 'リ' => 'リ', + 'ル' => 'ル', + 'レ' => 'レ', + 'ロ' => 'ロ', + 'ワ' => 'ワ', + 'ン' => 'ン', + '゙' => '゙', + '゚' => '゚', + 'ᅠ' => 'ᅠ', + 'ᄀ' => 'ᄀ', + 'ᄁ' => 'ᄁ', + 'ᆪ' => 'ᆪ', + 'ᄂ' => 'ᄂ', + 'ᆬ' => 'ᆬ', + 'ᆭ' => 'ᆭ', + 'ᄃ' => 'ᄃ', + 'ᄄ' => 'ᄄ', + 'ᄅ' => 'ᄅ', + 'ᆰ' => 'ᆰ', + 'ᆱ' => 'ᆱ', + 'ᆲ' => 'ᆲ', + 'ᆳ' => 'ᆳ', + 'ᆴ' => 'ᆴ', + 'ᆵ' => 'ᆵ', + 'ᄚ' => 'ᄚ', + 'ᄆ' => 'ᄆ', + 'ᄇ' => 'ᄇ', + 'ᄈ' => 'ᄈ', + 'ᄡ' => 'ᄡ', + 'ᄉ' => 'ᄉ', + 'ᄊ' => 'ᄊ', + 'ᄋ' => 'ᄋ', + 'ᄌ' => 'ᄌ', + 'ᄍ' => 'ᄍ', + 'ᄎ' => 'ᄎ', + 'ᄏ' => 'ᄏ', + 'ᄐ' => 'ᄐ', + 'ᄑ' => 'ᄑ', + 'ᄒ' => 'ᄒ', + 'ᅡ' => 'ᅡ', + 'ᅢ' => 'ᅢ', + 'ᅣ' => 'ᅣ', + 'ᅤ' => 'ᅤ', + 'ᅥ' => 'ᅥ', + 'ᅦ' => 'ᅦ', + 'ᅧ' => 'ᅧ', + 'ᅨ' => 'ᅨ', + 'ᅩ' => 'ᅩ', + 'ᅪ' => 'ᅪ', + 'ᅫ' => 'ᅫ', + 'ᅬ' => 'ᅬ', + 'ᅭ' => 'ᅭ', + 'ᅮ' => 'ᅮ', + 'ᅯ' => 'ᅯ', + 'ᅰ' => 'ᅰ', + 'ᅱ' => 'ᅱ', + 'ᅲ' => 'ᅲ', + 'ᅳ' => 'ᅳ', + 'ᅴ' => 'ᅴ', + 'ᅵ' => 'ᅵ', + '¢' => '¢', + '£' => '£', + '¬' => '¬', + ' ̄' => ' ̄', + '¦' => '¦', + '¥' => '¥', + '₩' => '₩', + '│' => '│', + '←' => '←', + '↑' => '↑', + '→' => '→', + '↓' => '↓', + '■' => '■', + '○' => '○', + '𝐀' => 'A', + '𝐁' => 'B', + '𝐂' => 'C', + '𝐃' => 'D', + '𝐄' => 'E', + '𝐅' => 'F', + '𝐆' => 'G', + '𝐇' => 'H', + '𝐈' => 'I', + '𝐉' => 'J', + '𝐊' => 'K', + '𝐋' => 'L', + '𝐌' => 'M', + '𝐍' => 'N', + '𝐎' => 'O', + '𝐏' => 'P', + '𝐐' => 'Q', + '𝐑' => 'R', + '𝐒' => 'S', + '𝐓' => 'T', + '𝐔' => 'U', + '𝐕' => 'V', + '𝐖' => 'W', + '𝐗' => 'X', + '𝐘' => 'Y', + '𝐙' => 'Z', + '𝐚' => 'a', + '𝐛' => 'b', + '𝐜' => 'c', + '𝐝' => 'd', + '𝐞' => 'e', + '𝐟' => 'f', + '𝐠' => 'g', + '𝐡' => 'h', + '𝐢' => 'i', + '𝐣' => 'j', + '𝐤' => 'k', + '𝐥' => 'l', + '𝐦' => 'm', + '𝐧' => 'n', + '𝐨' => 'o', + '𝐩' => 'p', + '𝐪' => 'q', + '𝐫' => 'r', + '𝐬' => 's', + '𝐭' => 't', + '𝐮' => 'u', + '𝐯' => 'v', + '𝐰' => 'w', + '𝐱' => 'x', + '𝐲' => 'y', + '𝐳' => 'z', + '𝐴' => 'A', + '𝐵' => 'B', + '𝐶' => 'C', + '𝐷' => 'D', + '𝐸' => 'E', + '𝐹' => 'F', + '𝐺' => 'G', + '𝐻' => 'H', + '𝐼' => 'I', + '𝐽' => 'J', + '𝐾' => 'K', + '𝐿' => 'L', + '𝑀' => 'M', + '𝑁' => 'N', + '𝑂' => 'O', + '𝑃' => 'P', + '𝑄' => 'Q', + '𝑅' => 'R', + '𝑆' => 'S', + '𝑇' => 'T', + '𝑈' => 'U', + '𝑉' => 'V', + '𝑊' => 'W', + '𝑋' => 'X', + '𝑌' => 'Y', + '𝑍' => 'Z', + '𝑎' => 'a', + '𝑏' => 'b', + '𝑐' => 'c', + '𝑑' => 'd', + '𝑒' => 'e', + '𝑓' => 'f', + '𝑔' => 'g', + '𝑖' => 'i', + '𝑗' => 'j', + '𝑘' => 'k', + '𝑙' => 'l', + '𝑚' => 'm', + '𝑛' => 'n', + '𝑜' => 'o', + '𝑝' => 'p', + '𝑞' => 'q', + '𝑟' => 'r', + '𝑠' => 's', + '𝑡' => 't', + '𝑢' => 'u', + '𝑣' => 'v', + '𝑤' => 'w', + '𝑥' => 'x', + '𝑦' => 'y', + '𝑧' => 'z', + '𝑨' => 'A', + '𝑩' => 'B', + '𝑪' => 'C', + '𝑫' => 'D', + '𝑬' => 'E', + '𝑭' => 'F', + '𝑮' => 'G', + '𝑯' => 'H', + '𝑰' => 'I', + '𝑱' => 'J', + '𝑲' => 'K', + '𝑳' => 'L', + '𝑴' => 'M', + '𝑵' => 'N', + '𝑶' => 'O', + '𝑷' => 'P', + '𝑸' => 'Q', + '𝑹' => 'R', + '𝑺' => 'S', + '𝑻' => 'T', + '𝑼' => 'U', + '𝑽' => 'V', + '𝑾' => 'W', + '𝑿' => 'X', + '𝒀' => 'Y', + '𝒁' => 'Z', + '𝒂' => 'a', + '𝒃' => 'b', + '𝒄' => 'c', + '𝒅' => 'd', + '𝒆' => 'e', + '𝒇' => 'f', + '𝒈' => 'g', + '𝒉' => 'h', + '𝒊' => 'i', + '𝒋' => 'j', + '𝒌' => 'k', + '𝒍' => 'l', + '𝒎' => 'm', + '𝒏' => 'n', + '𝒐' => 'o', + '𝒑' => 'p', + '𝒒' => 'q', + '𝒓' => 'r', + '𝒔' => 's', + '𝒕' => 't', + '𝒖' => 'u', + '𝒗' => 'v', + '𝒘' => 'w', + '𝒙' => 'x', + '𝒚' => 'y', + '𝒛' => 'z', + '𝒜' => 'A', + '𝒞' => 'C', + '𝒟' => 'D', + '𝒢' => 'G', + '𝒥' => 'J', + '𝒦' => 'K', + '𝒩' => 'N', + '𝒪' => 'O', + '𝒫' => 'P', + '𝒬' => 'Q', + '𝒮' => 'S', + '𝒯' => 'T', + '𝒰' => 'U', + '𝒱' => 'V', + '𝒲' => 'W', + '𝒳' => 'X', + '𝒴' => 'Y', + '𝒵' => 'Z', + '𝒶' => 'a', + '𝒷' => 'b', + '𝒸' => 'c', + '𝒹' => 'd', + '𝒻' => 'f', + '𝒽' => 'h', + '𝒾' => 'i', + '𝒿' => 'j', + '𝓀' => 'k', + '𝓁' => 'l', + '𝓂' => 'm', + '𝓃' => 'n', + '𝓅' => 'p', + '𝓆' => 'q', + '𝓇' => 'r', + '𝓈' => 's', + '𝓉' => 't', + '𝓊' => 'u', + '𝓋' => 'v', + '𝓌' => 'w', + '𝓍' => 'x', + '𝓎' => 'y', + '𝓏' => 'z', + '𝓐' => 'A', + '𝓑' => 'B', + '𝓒' => 'C', + '𝓓' => 'D', + '𝓔' => 'E', + '𝓕' => 'F', + '𝓖' => 'G', + '𝓗' => 'H', + '𝓘' => 'I', + '𝓙' => 'J', + '𝓚' => 'K', + '𝓛' => 'L', + '𝓜' => 'M', + '𝓝' => 'N', + '𝓞' => 'O', + '𝓟' => 'P', + '𝓠' => 'Q', + '𝓡' => 'R', + '𝓢' => 'S', + '𝓣' => 'T', + '𝓤' => 'U', + '𝓥' => 'V', + '𝓦' => 'W', + '𝓧' => 'X', + '𝓨' => 'Y', + '𝓩' => 'Z', + '𝓪' => 'a', + '𝓫' => 'b', + '𝓬' => 'c', + '𝓭' => 'd', + '𝓮' => 'e', + '𝓯' => 'f', + '𝓰' => 'g', + '𝓱' => 'h', + '𝓲' => 'i', + '𝓳' => 'j', + '𝓴' => 'k', + '𝓵' => 'l', + '𝓶' => 'm', + '𝓷' => 'n', + '𝓸' => 'o', + '𝓹' => 'p', + '𝓺' => 'q', + '𝓻' => 'r', + '𝓼' => 's', + '𝓽' => 't', + '𝓾' => 'u', + '𝓿' => 'v', + '𝔀' => 'w', + '𝔁' => 'x', + '𝔂' => 'y', + '𝔃' => 'z', + '𝔄' => 'A', + '𝔅' => 'B', + '𝔇' => 'D', + '𝔈' => 'E', + '𝔉' => 'F', + '𝔊' => 'G', + '𝔍' => 'J', + '𝔎' => 'K', + '𝔏' => 'L', + '𝔐' => 'M', + '𝔑' => 'N', + '𝔒' => 'O', + '𝔓' => 'P', + '𝔔' => 'Q', + '𝔖' => 'S', + '𝔗' => 'T', + '𝔘' => 'U', + '𝔙' => 'V', + '𝔚' => 'W', + '𝔛' => 'X', + '𝔜' => 'Y', + '𝔞' => 'a', + '𝔟' => 'b', + '𝔠' => 'c', + '𝔡' => 'd', + '𝔢' => 'e', + '𝔣' => 'f', + '𝔤' => 'g', + '𝔥' => 'h', + '𝔦' => 'i', + '𝔧' => 'j', + '𝔨' => 'k', + '𝔩' => 'l', + '𝔪' => 'm', + '𝔫' => 'n', + '𝔬' => 'o', + '𝔭' => 'p', + '𝔮' => 'q', + '𝔯' => 'r', + '𝔰' => 's', + '𝔱' => 't', + '𝔲' => 'u', + '𝔳' => 'v', + '𝔴' => 'w', + '𝔵' => 'x', + '𝔶' => 'y', + '𝔷' => 'z', + '𝔸' => 'A', + '𝔹' => 'B', + '𝔻' => 'D', + '𝔼' => 'E', + '𝔽' => 'F', + '𝔾' => 'G', + '𝕀' => 'I', + '𝕁' => 'J', + '𝕂' => 'K', + '𝕃' => 'L', + '𝕄' => 'M', + '𝕆' => 'O', + '𝕊' => 'S', + '𝕋' => 'T', + '𝕌' => 'U', + '𝕍' => 'V', + '𝕎' => 'W', + '𝕏' => 'X', + '𝕐' => 'Y', + '𝕒' => 'a', + '𝕓' => 'b', + '𝕔' => 'c', + '𝕕' => 'd', + '𝕖' => 'e', + '𝕗' => 'f', + '𝕘' => 'g', + '𝕙' => 'h', + '𝕚' => 'i', + '𝕛' => 'j', + '𝕜' => 'k', + '𝕝' => 'l', + '𝕞' => 'm', + '𝕟' => 'n', + '𝕠' => 'o', + '𝕡' => 'p', + '𝕢' => 'q', + '𝕣' => 'r', + '𝕤' => 's', + '𝕥' => 't', + '𝕦' => 'u', + '𝕧' => 'v', + '𝕨' => 'w', + '𝕩' => 'x', + '𝕪' => 'y', + '𝕫' => 'z', + '𝕬' => 'A', + '𝕭' => 'B', + '𝕮' => 'C', + '𝕯' => 'D', + '𝕰' => 'E', + '𝕱' => 'F', + '𝕲' => 'G', + '𝕳' => 'H', + '𝕴' => 'I', + '𝕵' => 'J', + '𝕶' => 'K', + '𝕷' => 'L', + '𝕸' => 'M', + '𝕹' => 'N', + '𝕺' => 'O', + '𝕻' => 'P', + '𝕼' => 'Q', + '𝕽' => 'R', + '𝕾' => 'S', + '𝕿' => 'T', + '𝖀' => 'U', + '𝖁' => 'V', + '𝖂' => 'W', + '𝖃' => 'X', + '𝖄' => 'Y', + '𝖅' => 'Z', + '𝖆' => 'a', + '𝖇' => 'b', + '𝖈' => 'c', + '𝖉' => 'd', + '𝖊' => 'e', + '𝖋' => 'f', + '𝖌' => 'g', + '𝖍' => 'h', + '𝖎' => 'i', + '𝖏' => 'j', + '𝖐' => 'k', + '𝖑' => 'l', + '𝖒' => 'm', + '𝖓' => 'n', + '𝖔' => 'o', + '𝖕' => 'p', + '𝖖' => 'q', + '𝖗' => 'r', + '𝖘' => 's', + '𝖙' => 't', + '𝖚' => 'u', + '𝖛' => 'v', + '𝖜' => 'w', + '𝖝' => 'x', + '𝖞' => 'y', + '𝖟' => 'z', + '𝖠' => 'A', + '𝖡' => 'B', + '𝖢' => 'C', + '𝖣' => 'D', + '𝖤' => 'E', + '𝖥' => 'F', + '𝖦' => 'G', + '𝖧' => 'H', + '𝖨' => 'I', + '𝖩' => 'J', + '𝖪' => 'K', + '𝖫' => 'L', + '𝖬' => 'M', + '𝖭' => 'N', + '𝖮' => 'O', + '𝖯' => 'P', + '𝖰' => 'Q', + '𝖱' => 'R', + '𝖲' => 'S', + '𝖳' => 'T', + '𝖴' => 'U', + '𝖵' => 'V', + '𝖶' => 'W', + '𝖷' => 'X', + '𝖸' => 'Y', + '𝖹' => 'Z', + '𝖺' => 'a', + '𝖻' => 'b', + '𝖼' => 'c', + '𝖽' => 'd', + '𝖾' => 'e', + '𝖿' => 'f', + '𝗀' => 'g', + '𝗁' => 'h', + '𝗂' => 'i', + '𝗃' => 'j', + '𝗄' => 'k', + '𝗅' => 'l', + '𝗆' => 'm', + '𝗇' => 'n', + '𝗈' => 'o', + '𝗉' => 'p', + '𝗊' => 'q', + '𝗋' => 'r', + '𝗌' => 's', + '𝗍' => 't', + '𝗎' => 'u', + '𝗏' => 'v', + '𝗐' => 'w', + '𝗑' => 'x', + '𝗒' => 'y', + '𝗓' => 'z', + '𝗔' => 'A', + '𝗕' => 'B', + '𝗖' => 'C', + '𝗗' => 'D', + '𝗘' => 'E', + '𝗙' => 'F', + '𝗚' => 'G', + '𝗛' => 'H', + '𝗜' => 'I', + '𝗝' => 'J', + '𝗞' => 'K', + '𝗟' => 'L', + '𝗠' => 'M', + '𝗡' => 'N', + '𝗢' => 'O', + '𝗣' => 'P', + '𝗤' => 'Q', + '𝗥' => 'R', + '𝗦' => 'S', + '𝗧' => 'T', + '𝗨' => 'U', + '𝗩' => 'V', + '𝗪' => 'W', + '𝗫' => 'X', + '𝗬' => 'Y', + '𝗭' => 'Z', + '𝗮' => 'a', + '𝗯' => 'b', + '𝗰' => 'c', + '𝗱' => 'd', + '𝗲' => 'e', + '𝗳' => 'f', + '𝗴' => 'g', + '𝗵' => 'h', + '𝗶' => 'i', + '𝗷' => 'j', + '𝗸' => 'k', + '𝗹' => 'l', + '𝗺' => 'm', + '𝗻' => 'n', + '𝗼' => 'o', + '𝗽' => 'p', + '𝗾' => 'q', + '𝗿' => 'r', + '𝘀' => 's', + '𝘁' => 't', + '𝘂' => 'u', + '𝘃' => 'v', + '𝘄' => 'w', + '𝘅' => 'x', + '𝘆' => 'y', + '𝘇' => 'z', + '𝘈' => 'A', + '𝘉' => 'B', + '𝘊' => 'C', + '𝘋' => 'D', + '𝘌' => 'E', + '𝘍' => 'F', + '𝘎' => 'G', + '𝘏' => 'H', + '𝘐' => 'I', + '𝘑' => 'J', + '𝘒' => 'K', + '𝘓' => 'L', + '𝘔' => 'M', + '𝘕' => 'N', + '𝘖' => 'O', + '𝘗' => 'P', + '𝘘' => 'Q', + '𝘙' => 'R', + '𝘚' => 'S', + '𝘛' => 'T', + '𝘜' => 'U', + '𝘝' => 'V', + '𝘞' => 'W', + '𝘟' => 'X', + '𝘠' => 'Y', + '𝘡' => 'Z', + '𝘢' => 'a', + '𝘣' => 'b', + '𝘤' => 'c', + '𝘥' => 'd', + '𝘦' => 'e', + '𝘧' => 'f', + '𝘨' => 'g', + '𝘩' => 'h', + '𝘪' => 'i', + '𝘫' => 'j', + '𝘬' => 'k', + '𝘭' => 'l', + '𝘮' => 'm', + '𝘯' => 'n', + '𝘰' => 'o', + '𝘱' => 'p', + '𝘲' => 'q', + '𝘳' => 'r', + '𝘴' => 's', + '𝘵' => 't', + '𝘶' => 'u', + '𝘷' => 'v', + '𝘸' => 'w', + '𝘹' => 'x', + '𝘺' => 'y', + '𝘻' => 'z', + '𝘼' => 'A', + '𝘽' => 'B', + '𝘾' => 'C', + '𝘿' => 'D', + '𝙀' => 'E', + '𝙁' => 'F', + '𝙂' => 'G', + '𝙃' => 'H', + '𝙄' => 'I', + '𝙅' => 'J', + '𝙆' => 'K', + '𝙇' => 'L', + '𝙈' => 'M', + '𝙉' => 'N', + '𝙊' => 'O', + '𝙋' => 'P', + '𝙌' => 'Q', + '𝙍' => 'R', + '𝙎' => 'S', + '𝙏' => 'T', + '𝙐' => 'U', + '𝙑' => 'V', + '𝙒' => 'W', + '𝙓' => 'X', + '𝙔' => 'Y', + '𝙕' => 'Z', + '𝙖' => 'a', + '𝙗' => 'b', + '𝙘' => 'c', + '𝙙' => 'd', + '𝙚' => 'e', + '𝙛' => 'f', + '𝙜' => 'g', + '𝙝' => 'h', + '𝙞' => 'i', + '𝙟' => 'j', + '𝙠' => 'k', + '𝙡' => 'l', + '𝙢' => 'm', + '𝙣' => 'n', + '𝙤' => 'o', + '𝙥' => 'p', + '𝙦' => 'q', + '𝙧' => 'r', + '𝙨' => 's', + '𝙩' => 't', + '𝙪' => 'u', + '𝙫' => 'v', + '𝙬' => 'w', + '𝙭' => 'x', + '𝙮' => 'y', + '𝙯' => 'z', + '𝙰' => 'A', + '𝙱' => 'B', + '𝙲' => 'C', + '𝙳' => 'D', + '𝙴' => 'E', + '𝙵' => 'F', + '𝙶' => 'G', + '𝙷' => 'H', + '𝙸' => 'I', + '𝙹' => 'J', + '𝙺' => 'K', + '𝙻' => 'L', + '𝙼' => 'M', + '𝙽' => 'N', + '𝙾' => 'O', + '𝙿' => 'P', + '𝚀' => 'Q', + '𝚁' => 'R', + '𝚂' => 'S', + '𝚃' => 'T', + '𝚄' => 'U', + '𝚅' => 'V', + '𝚆' => 'W', + '𝚇' => 'X', + '𝚈' => 'Y', + '𝚉' => 'Z', + '𝚊' => 'a', + '𝚋' => 'b', + '𝚌' => 'c', + '𝚍' => 'd', + '𝚎' => 'e', + '𝚏' => 'f', + '𝚐' => 'g', + '𝚑' => 'h', + '𝚒' => 'i', + '𝚓' => 'j', + '𝚔' => 'k', + '𝚕' => 'l', + '𝚖' => 'm', + '𝚗' => 'n', + '𝚘' => 'o', + '𝚙' => 'p', + '𝚚' => 'q', + '𝚛' => 'r', + '𝚜' => 's', + '𝚝' => 't', + '𝚞' => 'u', + '𝚟' => 'v', + '𝚠' => 'w', + '𝚡' => 'x', + '𝚢' => 'y', + '𝚣' => 'z', + '𝚤' => 'ı', + '𝚥' => 'ȷ', + '𝚨' => 'Α', + '𝚩' => 'Β', + '𝚪' => 'Γ', + '𝚫' => 'Δ', + '𝚬' => 'Ε', + '𝚭' => 'Ζ', + '𝚮' => 'Η', + '𝚯' => 'Θ', + '𝚰' => 'Ι', + '𝚱' => 'Κ', + '𝚲' => 'Λ', + '𝚳' => 'Μ', + '𝚴' => 'Ν', + '𝚵' => 'Ξ', + '𝚶' => 'Ο', + '𝚷' => 'Π', + '𝚸' => 'Ρ', + '𝚹' => 'Θ', + '𝚺' => 'Σ', + '𝚻' => 'Τ', + '𝚼' => 'Υ', + '𝚽' => 'Φ', + '𝚾' => 'Χ', + '𝚿' => 'Ψ', + '𝛀' => 'Ω', + '𝛁' => '∇', + '𝛂' => 'α', + '𝛃' => 'β', + '𝛄' => 'γ', + '𝛅' => 'δ', + '𝛆' => 'ε', + '𝛇' => 'ζ', + '𝛈' => 'η', + '𝛉' => 'θ', + '𝛊' => 'ι', + '𝛋' => 'κ', + '𝛌' => 'λ', + '𝛍' => 'μ', + '𝛎' => 'ν', + '𝛏' => 'ξ', + '𝛐' => 'ο', + '𝛑' => 'π', + '𝛒' => 'ρ', + '𝛓' => 'ς', + '𝛔' => 'σ', + '𝛕' => 'τ', + '𝛖' => 'υ', + '𝛗' => 'φ', + '𝛘' => 'χ', + '𝛙' => 'ψ', + '𝛚' => 'ω', + '𝛛' => '∂', + '𝛜' => 'ε', + '𝛝' => 'θ', + '𝛞' => 'κ', + '𝛟' => 'φ', + '𝛠' => 'ρ', + '𝛡' => 'π', + '𝛢' => 'Α', + '𝛣' => 'Β', + '𝛤' => 'Γ', + '𝛥' => 'Δ', + '𝛦' => 'Ε', + '𝛧' => 'Ζ', + '𝛨' => 'Η', + '𝛩' => 'Θ', + '𝛪' => 'Ι', + '𝛫' => 'Κ', + '𝛬' => 'Λ', + '𝛭' => 'Μ', + '𝛮' => 'Ν', + '𝛯' => 'Ξ', + '𝛰' => 'Ο', + '𝛱' => 'Π', + '𝛲' => 'Ρ', + '𝛳' => 'Θ', + '𝛴' => 'Σ', + '𝛵' => 'Τ', + '𝛶' => 'Υ', + '𝛷' => 'Φ', + '𝛸' => 'Χ', + '𝛹' => 'Ψ', + '𝛺' => 'Ω', + '𝛻' => '∇', + '𝛼' => 'α', + '𝛽' => 'β', + '𝛾' => 'γ', + '𝛿' => 'δ', + '𝜀' => 'ε', + '𝜁' => 'ζ', + '𝜂' => 'η', + '𝜃' => 'θ', + '𝜄' => 'ι', + '𝜅' => 'κ', + '𝜆' => 'λ', + '𝜇' => 'μ', + '𝜈' => 'ν', + '𝜉' => 'ξ', + '𝜊' => 'ο', + '𝜋' => 'π', + '𝜌' => 'ρ', + '𝜍' => 'ς', + '𝜎' => 'σ', + '𝜏' => 'τ', + '𝜐' => 'υ', + '𝜑' => 'φ', + '𝜒' => 'χ', + '𝜓' => 'ψ', + '𝜔' => 'ω', + '𝜕' => '∂', + '𝜖' => 'ε', + '𝜗' => 'θ', + '𝜘' => 'κ', + '𝜙' => 'φ', + '𝜚' => 'ρ', + '𝜛' => 'π', + '𝜜' => 'Α', + '𝜝' => 'Β', + '𝜞' => 'Γ', + '𝜟' => 'Δ', + '𝜠' => 'Ε', + '𝜡' => 'Ζ', + '𝜢' => 'Η', + '𝜣' => 'Θ', + '𝜤' => 'Ι', + '𝜥' => 'Κ', + '𝜦' => 'Λ', + '𝜧' => 'Μ', + '𝜨' => 'Ν', + '𝜩' => 'Ξ', + '𝜪' => 'Ο', + '𝜫' => 'Π', + '𝜬' => 'Ρ', + '𝜭' => 'Θ', + '𝜮' => 'Σ', + '𝜯' => 'Τ', + '𝜰' => 'Υ', + '𝜱' => 'Φ', + '𝜲' => 'Χ', + '𝜳' => 'Ψ', + '𝜴' => 'Ω', + '𝜵' => '∇', + '𝜶' => 'α', + '𝜷' => 'β', + '𝜸' => 'γ', + '𝜹' => 'δ', + '𝜺' => 'ε', + '𝜻' => 'ζ', + '𝜼' => 'η', + '𝜽' => 'θ', + '𝜾' => 'ι', + '𝜿' => 'κ', + '𝝀' => 'λ', + '𝝁' => 'μ', + '𝝂' => 'ν', + '𝝃' => 'ξ', + '𝝄' => 'ο', + '𝝅' => 'π', + '𝝆' => 'ρ', + '𝝇' => 'ς', + '𝝈' => 'σ', + '𝝉' => 'τ', + '𝝊' => 'υ', + '𝝋' => 'φ', + '𝝌' => 'χ', + '𝝍' => 'ψ', + '𝝎' => 'ω', + '𝝏' => '∂', + '𝝐' => 'ε', + '𝝑' => 'θ', + '𝝒' => 'κ', + '𝝓' => 'φ', + '𝝔' => 'ρ', + '𝝕' => 'π', + '𝝖' => 'Α', + '𝝗' => 'Β', + '𝝘' => 'Γ', + '𝝙' => 'Δ', + '𝝚' => 'Ε', + '𝝛' => 'Ζ', + '𝝜' => 'Η', + '𝝝' => 'Θ', + '𝝞' => 'Ι', + '𝝟' => 'Κ', + '𝝠' => 'Λ', + '𝝡' => 'Μ', + '𝝢' => 'Ν', + '𝝣' => 'Ξ', + '𝝤' => 'Ο', + '𝝥' => 'Π', + '𝝦' => 'Ρ', + '𝝧' => 'Θ', + '𝝨' => 'Σ', + '𝝩' => 'Τ', + '𝝪' => 'Υ', + '𝝫' => 'Φ', + '𝝬' => 'Χ', + '𝝭' => 'Ψ', + '𝝮' => 'Ω', + '𝝯' => '∇', + '𝝰' => 'α', + '𝝱' => 'β', + '𝝲' => 'γ', + '𝝳' => 'δ', + '𝝴' => 'ε', + '𝝵' => 'ζ', + '𝝶' => 'η', + '𝝷' => 'θ', + '𝝸' => 'ι', + '𝝹' => 'κ', + '𝝺' => 'λ', + '𝝻' => 'μ', + '𝝼' => 'ν', + '𝝽' => 'ξ', + '𝝾' => 'ο', + '𝝿' => 'π', + '𝞀' => 'ρ', + '𝞁' => 'ς', + '𝞂' => 'σ', + '𝞃' => 'τ', + '𝞄' => 'υ', + '𝞅' => 'φ', + '𝞆' => 'χ', + '𝞇' => 'ψ', + '𝞈' => 'ω', + '𝞉' => '∂', + '𝞊' => 'ε', + '𝞋' => 'θ', + '𝞌' => 'κ', + '𝞍' => 'φ', + '𝞎' => 'ρ', + '𝞏' => 'π', + '𝞐' => 'Α', + '𝞑' => 'Β', + '𝞒' => 'Γ', + '𝞓' => 'Δ', + '𝞔' => 'Ε', + '𝞕' => 'Ζ', + '𝞖' => 'Η', + '𝞗' => 'Θ', + '𝞘' => 'Ι', + '𝞙' => 'Κ', + '𝞚' => 'Λ', + '𝞛' => 'Μ', + '𝞜' => 'Ν', + '𝞝' => 'Ξ', + '𝞞' => 'Ο', + '𝞟' => 'Π', + '𝞠' => 'Ρ', + '𝞡' => 'Θ', + '𝞢' => 'Σ', + '𝞣' => 'Τ', + '𝞤' => 'Υ', + '𝞥' => 'Φ', + '𝞦' => 'Χ', + '𝞧' => 'Ψ', + '𝞨' => 'Ω', + '𝞩' => '∇', + '𝞪' => 'α', + '𝞫' => 'β', + '𝞬' => 'γ', + '𝞭' => 'δ', + '𝞮' => 'ε', + '𝞯' => 'ζ', + '𝞰' => 'η', + '𝞱' => 'θ', + '𝞲' => 'ι', + '𝞳' => 'κ', + '𝞴' => 'λ', + '𝞵' => 'μ', + '𝞶' => 'ν', + '𝞷' => 'ξ', + '𝞸' => 'ο', + '𝞹' => 'π', + '𝞺' => 'ρ', + '𝞻' => 'ς', + '𝞼' => 'σ', + '𝞽' => 'τ', + '𝞾' => 'υ', + '𝞿' => 'φ', + '𝟀' => 'χ', + '𝟁' => 'ψ', + '𝟂' => 'ω', + '𝟃' => '∂', + '𝟄' => 'ε', + '𝟅' => 'θ', + '𝟆' => 'κ', + '𝟇' => 'φ', + '𝟈' => 'ρ', + '𝟉' => 'π', + '𝟊' => 'Ϝ', + '𝟋' => 'ϝ', + '𝟎' => '0', + '𝟏' => '1', + '𝟐' => '2', + '𝟑' => '3', + '𝟒' => '4', + '𝟓' => '5', + '𝟔' => '6', + '𝟕' => '7', + '𝟖' => '8', + '𝟗' => '9', + '𝟘' => '0', + '𝟙' => '1', + '𝟚' => '2', + '𝟛' => '3', + '𝟜' => '4', + '𝟝' => '5', + '𝟞' => '6', + '𝟟' => '7', + '𝟠' => '8', + '𝟡' => '9', + '𝟢' => '0', + '𝟣' => '1', + '𝟤' => '2', + '𝟥' => '3', + '𝟦' => '4', + '𝟧' => '5', + '𝟨' => '6', + '𝟩' => '7', + '𝟪' => '8', + '𝟫' => '9', + '𝟬' => '0', + '𝟭' => '1', + '𝟮' => '2', + '𝟯' => '3', + '𝟰' => '4', + '𝟱' => '5', + '𝟲' => '6', + '𝟳' => '7', + '𝟴' => '8', + '𝟵' => '9', + '𝟶' => '0', + '𝟷' => '1', + '𝟸' => '2', + '𝟹' => '3', + '𝟺' => '4', + '𝟻' => '5', + '𝟼' => '6', + '𝟽' => '7', + '𝟾' => '8', + '𝟿' => '9', + '𞸀' => 'ا', + '𞸁' => 'ب', + '𞸂' => 'ج', + '𞸃' => 'د', + '𞸅' => 'و', + '𞸆' => 'ز', + '𞸇' => 'ح', + '𞸈' => 'ط', + '𞸉' => 'ي', + '𞸊' => 'ك', + '𞸋' => 'ل', + '𞸌' => 'م', + '𞸍' => 'ن', + '𞸎' => 'س', + '𞸏' => 'ع', + '𞸐' => 'ف', + '𞸑' => 'ص', + '𞸒' => 'ق', + '𞸓' => 'ر', + '𞸔' => 'ش', + '𞸕' => 'ت', + '𞸖' => 'ث', + '𞸗' => 'خ', + '𞸘' => 'ذ', + '𞸙' => 'ض', + '𞸚' => 'ظ', + '𞸛' => 'غ', + '𞸜' => 'ٮ', + '𞸝' => 'ں', + '𞸞' => 'ڡ', + '𞸟' => 'ٯ', + '𞸡' => 'ب', + '𞸢' => 'ج', + '𞸤' => 'ه', + '𞸧' => 'ح', + '𞸩' => 'ي', + '𞸪' => 'ك', + '𞸫' => 'ل', + '𞸬' => 'م', + '𞸭' => 'ن', + '𞸮' => 'س', + '𞸯' => 'ع', + '𞸰' => 'ف', + '𞸱' => 'ص', + '𞸲' => 'ق', + '𞸴' => 'ش', + '𞸵' => 'ت', + '𞸶' => 'ث', + '𞸷' => 'خ', + '𞸹' => 'ض', + '𞸻' => 'غ', + '𞹂' => 'ج', + '𞹇' => 'ح', + '𞹉' => 'ي', + '𞹋' => 'ل', + '𞹍' => 'ن', + '𞹎' => 'س', + '𞹏' => 'ع', + '𞹑' => 'ص', + '𞹒' => 'ق', + '𞹔' => 'ش', + '𞹗' => 'خ', + '𞹙' => 'ض', + '𞹛' => 'غ', + '𞹝' => 'ں', + '𞹟' => 'ٯ', + '𞹡' => 'ب', + '𞹢' => 'ج', + '𞹤' => 'ه', + '𞹧' => 'ح', + '𞹨' => 'ط', + '𞹩' => 'ي', + '𞹪' => 'ك', + '𞹬' => 'م', + '𞹭' => 'ن', + '𞹮' => 'س', + '𞹯' => 'ع', + '𞹰' => 'ف', + '𞹱' => 'ص', + '𞹲' => 'ق', + '𞹴' => 'ش', + '𞹵' => 'ت', + '𞹶' => 'ث', + '𞹷' => 'خ', + '𞹹' => 'ض', + '𞹺' => 'ظ', + '𞹻' => 'غ', + '𞹼' => 'ٮ', + '𞹾' => 'ڡ', + '𞺀' => 'ا', + '𞺁' => 'ب', + '𞺂' => 'ج', + '𞺃' => 'د', + '𞺄' => 'ه', + '𞺅' => 'و', + '𞺆' => 'ز', + '𞺇' => 'ح', + '𞺈' => 'ط', + '𞺉' => 'ي', + '𞺋' => 'ل', + '𞺌' => 'م', + '𞺍' => 'ن', + '𞺎' => 'س', + '𞺏' => 'ع', + '𞺐' => 'ف', + '𞺑' => 'ص', + '𞺒' => 'ق', + '𞺓' => 'ر', + '𞺔' => 'ش', + '𞺕' => 'ت', + '𞺖' => 'ث', + '𞺗' => 'خ', + '𞺘' => 'ذ', + '𞺙' => 'ض', + '𞺚' => 'ظ', + '𞺛' => 'غ', + '𞺡' => 'ب', + '𞺢' => 'ج', + '𞺣' => 'د', + '𞺥' => 'و', + '𞺦' => 'ز', + '𞺧' => 'ح', + '𞺨' => 'ط', + '𞺩' => 'ي', + '𞺫' => 'ل', + '𞺬' => 'م', + '𞺭' => 'ن', + '𞺮' => 'س', + '𞺯' => 'ع', + '𞺰' => 'ف', + '𞺱' => 'ص', + '𞺲' => 'ق', + '𞺳' => 'ر', + '𞺴' => 'ش', + '𞺵' => 'ت', + '𞺶' => 'ث', + '𞺷' => 'خ', + '𞺸' => 'ذ', + '𞺹' => 'ض', + '𞺺' => 'ظ', + '𞺻' => 'غ', + '🄀' => '0.', + '🄁' => '0,', + '🄂' => '1,', + '🄃' => '2,', + '🄄' => '3,', + '🄅' => '4,', + '🄆' => '5,', + '🄇' => '6,', + '🄈' => '7,', + '🄉' => '8,', + '🄊' => '9,', + '🄐' => '(A)', + '🄑' => '(B)', + '🄒' => '(C)', + '🄓' => '(D)', + '🄔' => '(E)', + '🄕' => '(F)', + '🄖' => '(G)', + '🄗' => '(H)', + '🄘' => '(I)', + '🄙' => '(J)', + '🄚' => '(K)', + '🄛' => '(L)', + '🄜' => '(M)', + '🄝' => '(N)', + '🄞' => '(O)', + '🄟' => '(P)', + '🄠' => '(Q)', + '🄡' => '(R)', + '🄢' => '(S)', + '🄣' => '(T)', + '🄤' => '(U)', + '🄥' => '(V)', + '🄦' => '(W)', + '🄧' => '(X)', + '🄨' => '(Y)', + '🄩' => '(Z)', + '🄪' => '〔S〕', + '🄫' => 'C', + '🄬' => 'R', + '🄭' => 'CD', + '🄮' => 'WZ', + '🄰' => 'A', + '🄱' => 'B', + '🄲' => 'C', + '🄳' => 'D', + '🄴' => 'E', + '🄵' => 'F', + '🄶' => 'G', + '🄷' => 'H', + '🄸' => 'I', + '🄹' => 'J', + '🄺' => 'K', + '🄻' => 'L', + '🄼' => 'M', + '🄽' => 'N', + '🄾' => 'O', + '🄿' => 'P', + '🅀' => 'Q', + '🅁' => 'R', + '🅂' => 'S', + '🅃' => 'T', + '🅄' => 'U', + '🅅' => 'V', + '🅆' => 'W', + '🅇' => 'X', + '🅈' => 'Y', + '🅉' => 'Z', + '🅊' => 'HV', + '🅋' => 'MV', + '🅌' => 'SD', + '🅍' => 'SS', + '🅎' => 'PPV', + '🅏' => 'WC', + '🅪' => 'MC', + '🅫' => 'MD', + '🅬' => 'MR', + '🆐' => 'DJ', + '🈀' => 'ほか', + '🈁' => 'ココ', + '🈂' => 'サ', + '🈐' => '手', + '🈑' => '字', + '🈒' => '双', + '🈓' => 'デ', + '🈔' => '二', + '🈕' => '多', + '🈖' => '解', + '🈗' => '天', + '🈘' => '交', + '🈙' => '映', + '🈚' => '無', + '🈛' => '料', + '🈜' => '前', + '🈝' => '後', + '🈞' => '再', + '🈟' => '新', + '🈠' => '初', + '🈡' => '終', + '🈢' => '生', + '🈣' => '販', + '🈤' => '声', + '🈥' => '吹', + '🈦' => '演', + '🈧' => '投', + '🈨' => '捕', + '🈩' => '一', + '🈪' => '三', + '🈫' => '遊', + '🈬' => '左', + '🈭' => '中', + '🈮' => '右', + '🈯' => '指', + '🈰' => '走', + '🈱' => '打', + '🈲' => '禁', + '🈳' => '空', + '🈴' => '合', + '🈵' => '満', + '🈶' => '有', + '🈷' => '月', + '🈸' => '申', + '🈹' => '割', + '🈺' => '営', + '🈻' => '配', + '🉀' => '〔本〕', + '🉁' => '〔三〕', + '🉂' => '〔二〕', + '🉃' => '〔安〕', + '🉄' => '〔点〕', + '🉅' => '〔打〕', + '🉆' => '〔盗〕', + '🉇' => '〔勝〕', + '🉈' => '〔敗〕', + '🉐' => '得', + '🉑' => '可', + '🯰' => '0', + '🯱' => '1', + '🯲' => '2', + '🯳' => '3', + '🯴' => '4', + '🯵' => '5', + '🯶' => '6', + '🯷' => '7', + '🯸' => '8', + '🯹' => '9', +); diff --git a/vendor/symfony/polyfill-intl-normalizer/bootstrap.php b/vendor/symfony/polyfill-intl-normalizer/bootstrap.php new file mode 100644 index 0000000..3608e5c --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/bootstrap.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Normalizer as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('normalizer_is_normalized')) { + function normalizer_is_normalized($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::isNormalized($string, $form); } +} +if (!function_exists('normalizer_normalize')) { + function normalizer_normalize($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::normalize($string, $form); } +} diff --git a/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php b/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php new file mode 100644 index 0000000..e36d1a9 --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Normalizer as p; + +if (!function_exists('normalizer_is_normalized')) { + function normalizer_is_normalized(?string $string, ?int $form = p\Normalizer::FORM_C): bool { return p\Normalizer::isNormalized((string) $string, (int) $form); } +} +if (!function_exists('normalizer_normalize')) { + function normalizer_normalize(?string $string, ?int $form = p\Normalizer::FORM_C): string|false { return p\Normalizer::normalize((string) $string, (int) $form); } +} diff --git a/vendor/symfony/polyfill-intl-normalizer/composer.json b/vendor/symfony/polyfill-intl-normalizer/composer.json new file mode 100644 index 0000000..393edf7 --- /dev/null +++ b/vendor/symfony/polyfill-intl-normalizer/composer.json @@ -0,0 +1,39 @@ +{ + "name": "symfony/polyfill-intl-normalizer", + "type": "library", + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "normalizer"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "suggest": { + "ext-intl": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/polyfill-mbstring/LICENSE b/vendor/symfony/polyfill-mbstring/LICENSE new file mode 100644 index 0000000..4cd8bdd --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-mbstring/Mbstring.php b/vendor/symfony/polyfill-mbstring/Mbstring.php new file mode 100644 index 0000000..b65c54a --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Mbstring.php @@ -0,0 +1,873 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Mbstring; + +/** + * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. + * + * Implemented: + * - mb_chr - Returns a specific character from its Unicode code point + * - mb_convert_encoding - Convert character encoding + * - mb_convert_variables - Convert character code in variable(s) + * - mb_decode_mimeheader - Decode string in MIME header field + * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED + * - mb_decode_numericentity - Decode HTML numeric string reference to character + * - mb_encode_numericentity - Encode character to HTML numeric string reference + * - mb_convert_case - Perform case folding on a string + * - mb_detect_encoding - Detect character encoding + * - mb_get_info - Get internal settings of mbstring + * - mb_http_input - Detect HTTP input character encoding + * - mb_http_output - Set/Get HTTP output character encoding + * - mb_internal_encoding - Set/Get internal character encoding + * - mb_list_encodings - Returns an array of all supported encodings + * - mb_ord - Returns the Unicode code point of a character + * - mb_output_handler - Callback function converts character encoding in output buffer + * - mb_scrub - Replaces ill-formed byte sequences with substitute characters + * - mb_strlen - Get string length + * - mb_strpos - Find position of first occurrence of string in a string + * - mb_strrpos - Find position of last occurrence of a string in a string + * - mb_str_split - Convert a string to an array + * - mb_strtolower - Make a string lowercase + * - mb_strtoupper - Make a string uppercase + * - mb_substitute_character - Set/Get substitution character + * - mb_substr - Get part of string + * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive + * - mb_stristr - Finds first occurrence of a string within another, case insensitive + * - mb_strrchr - Finds the last occurrence of a character in a string within another + * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive + * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive + * - mb_strstr - Finds first occurrence of a string within another + * - mb_strwidth - Return width of string + * - mb_substr_count - Count the number of substring occurrences + * + * Not implemented: + * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) + * - mb_ereg_* - Regular expression with multibyte support + * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable + * - mb_preferred_mime_name - Get MIME charset string + * - mb_regex_encoding - Returns current encoding for multibyte regex as string + * - mb_regex_set_options - Set/Get the default options for mbregex functions + * - mb_send_mail - Send encoded mail + * - mb_split - Split multibyte string using regular expression + * - mb_strcut - Get part of string + * - mb_strimwidth - Get truncated string with specified width + * + * @author Nicolas Grekas + * + * @internal + */ +final class Mbstring +{ + public const MB_CASE_FOLD = \PHP_INT_MAX; + + private const CASE_FOLD = [ + ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], + ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], + ]; + + private static $encodingList = ['ASCII', 'UTF-8']; + private static $language = 'neutral'; + private static $internalEncoding = 'UTF-8'; + + public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) + { + if (\is_array($fromEncoding) || ($fromEncoding !== null && false !== strpos($fromEncoding, ','))) { + $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); + } else { + $fromEncoding = self::getEncoding($fromEncoding); + } + + $toEncoding = self::getEncoding($toEncoding); + + if ('BASE64' === $fromEncoding) { + $s = base64_decode($s); + $fromEncoding = $toEncoding; + } + + if ('BASE64' === $toEncoding) { + return base64_encode($s); + } + + if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { + if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { + $fromEncoding = 'Windows-1252'; + } + if ('UTF-8' !== $fromEncoding) { + $s = \iconv($fromEncoding, 'UTF-8//IGNORE', $s); + } + + return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); + } + + if ('HTML-ENTITIES' === $fromEncoding) { + $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8'); + $fromEncoding = 'UTF-8'; + } + + return \iconv($fromEncoding, $toEncoding.'//IGNORE', $s); + } + + public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars) + { + $ok = true; + array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { + if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { + $ok = false; + } + }); + + return $ok ? $fromEncoding : false; + } + + public static function mb_decode_mimeheader($s) + { + return \iconv_mime_decode($s, 2, self::$internalEncoding); + } + + public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) + { + trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING); + } + + public static function mb_decode_numericentity($s, $convmap, $encoding = null) + { + if (null !== $s && !is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { + trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { + return false; + } + + if (null !== $encoding && !is_scalar($encoding)) { + trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return ''; // Instead of null (cf. mb_encode_numericentity). + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @\iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = \iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $cnt = floor(\count($convmap) / 4) * 4; + + for ($i = 0; $i < $cnt; $i += 4) { + // collector_decode_htmlnumericentity ignores $convmap[$i + 3] + $convmap[$i] += $convmap[$i + 2]; + $convmap[$i + 1] += $convmap[$i + 2]; + } + + $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { + $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; + for ($i = 0; $i < $cnt; $i += 4) { + if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { + return self::mb_chr($c - $convmap[$i + 2]); + } + } + + return $m[0]; + }, $s); + + if (null === $encoding) { + return $s; + } + + return \iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) + { + if (null !== $s && !is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { + trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { + return false; + } + + if (null !== $encoding && !is_scalar($encoding)) { + trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; // Instead of '' (cf. mb_decode_numericentity). + } + + if (null !== $is_hex && !is_scalar($is_hex)) { + trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @\iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = \iconv($encoding, 'UTF-8//IGNORE', $s); + } + + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + + $cnt = floor(\count($convmap) / 4) * 4; + $i = 0; + $len = \strlen($s); + $result = ''; + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + $c = self::mb_ord($uchr); + + for ($j = 0; $j < $cnt; $j += 4) { + if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { + $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; + $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; + continue 2; + } + } + $result .= $uchr; + } + + if (null === $encoding) { + return $result; + } + + return \iconv('UTF-8', $encoding.'//IGNORE', $result); + } + + public static function mb_convert_case($s, $mode, $encoding = null) + { + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @\iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = \iconv($encoding, 'UTF-8//IGNORE', $s); + } + + if (\MB_CASE_TITLE == $mode) { + static $titleRegexp = null; + if (null === $titleRegexp) { + $titleRegexp = self::getData('titleCaseRegexp'); + } + $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); + } else { + if (\MB_CASE_UPPER == $mode) { + static $upper = null; + if (null === $upper) { + $upper = self::getData('upperCase'); + } + $map = $upper; + } else { + if (self::MB_CASE_FOLD === $mode) { + $s = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $s); + } + + static $lower = null; + if (null === $lower) { + $lower = self::getData('lowerCase'); + } + $map = $lower; + } + + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + + $i = 0; + $len = \strlen($s); + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + + if (isset($map[$uchr])) { + $uchr = $map[$uchr]; + $nlen = \strlen($uchr); + + if ($nlen == $ulen) { + $nlen = $i; + do { + $s[--$nlen] = $uchr[--$ulen]; + } while ($ulen); + } else { + $s = substr_replace($s, $uchr, $i - $ulen, $ulen); + $len += $nlen - $ulen; + $i += $nlen - $ulen; + } + } + } + } + + if (null === $encoding) { + return $s; + } + + return \iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_internal_encoding($encoding = null) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + $normalizedEncoding = self::getEncoding($encoding); + + if ('UTF-8' === $normalizedEncoding || false !== @\iconv($normalizedEncoding, $normalizedEncoding, ' ')) { + self::$internalEncoding = $normalizedEncoding; + + return true; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + public static function mb_language($lang = null) + { + if (null === $lang) { + return self::$language; + } + + switch ($normalizedLang = strtolower($lang)) { + case 'uni': + case 'neutral': + self::$language = $normalizedLang; + + return true; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang)); + } + + public static function mb_list_encodings() + { + return ['UTF-8']; + } + + public static function mb_encoding_aliases($encoding) + { + switch (strtoupper($encoding)) { + case 'UTF8': + case 'UTF-8': + return ['utf8']; + } + + return false; + } + + public static function mb_check_encoding($var = null, $encoding = null) + { + if (null === $encoding) { + if (null === $var) { + return false; + } + $encoding = self::$internalEncoding; + } + + return self::mb_detect_encoding($var, [$encoding]) || false !== @\iconv($encoding, $encoding, $var); + } + + public static function mb_detect_encoding($str, $encodingList = null, $strict = false) + { + if (null === $encodingList) { + $encodingList = self::$encodingList; + } else { + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + } + + foreach ($encodingList as $enc) { + switch ($enc) { + case 'ASCII': + if (!preg_match('/[\x80-\xFF]/', $str)) { + return $enc; + } + break; + + case 'UTF8': + case 'UTF-8': + if (preg_match('//u', $str)) { + return 'UTF-8'; + } + break; + + default: + if (0 === strncmp($enc, 'ISO-8859-', 9)) { + return $enc; + } + } + } + + return false; + } + + public static function mb_detect_order($encodingList = null) + { + if (null === $encodingList) { + return self::$encodingList; + } + + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + + foreach ($encodingList as $enc) { + switch ($enc) { + default: + if (strncmp($enc, 'ISO-8859-', 9)) { + return false; + } + // no break + case 'ASCII': + case 'UTF8': + case 'UTF-8': + } + } + + self::$encodingList = $encodingList; + + return true; + } + + public static function mb_strlen($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return \strlen($s); + } + + return @\iconv_strlen($s, $encoding); + } + + public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strpos($haystack, $needle, $offset); + } + + $needle = (string) $needle; + if ('' === $needle) { + if (80000 > \PHP_VERSION_ID) { + trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING); + + return false; + } + + return 0; + } + + return \iconv_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strrpos($haystack, $needle, $offset); + } + + if ($offset != (int) $offset) { + $offset = 0; + } elseif ($offset = (int) $offset) { + if ($offset < 0) { + if (0 > $offset += self::mb_strlen($needle)) { + $haystack = self::mb_substr($haystack, 0, $offset, $encoding); + } + $offset = 0; + } else { + $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); + } + } + + $pos = '' !== $needle || 80000 > \PHP_VERSION_ID + ? \iconv_strrpos($haystack, $needle, $encoding) + : self::mb_strlen($haystack, $encoding); + + return false !== $pos ? $offset + $pos : false; + } + + public static function mb_str_split($string, $split_length = 1, $encoding = null) + { + if (null !== $string && !is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { + trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING); + + return null; + } + + if (1 > $split_length = (int) $split_length) { + if (80000 > \PHP_VERSION_ID) { + trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); + return false; + } + + throw new \ValueError('Argument #2 ($length) must be greater than 0'); + } + + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + if ('UTF-8' === $encoding = self::getEncoding($encoding)) { + $rx = '/('; + while (65535 < $split_length) { + $rx .= '.{65535}'; + $split_length -= 65535; + } + $rx .= '.{'.$split_length.'})/us'; + + return preg_split($rx, $string, null, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); + } + + $result = []; + $length = mb_strlen($string, $encoding); + + for ($i = 0; $i < $length; $i += $split_length) { + $result[] = mb_substr($string, $i, $split_length, $encoding); + } + + return $result; + } + + public static function mb_strtolower($s, $encoding = null) + { + return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding); + } + + public static function mb_strtoupper($s, $encoding = null) + { + return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding); + } + + public static function mb_substitute_character($c = null) + { + if (null === $c) { + return 'none'; + } + if (0 === strcasecmp($c, 'none')) { + return true; + } + if (80000 > \PHP_VERSION_ID) { + return false; + } + if (\is_int($c) || 'long' === $c || 'entity' === $c) { + return false; + } + + throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint'); + } + + public static function mb_substr($s, $start, $length = null, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return (string) substr($s, $start, null === $length ? 2147483647 : $length); + } + + if ($start < 0) { + $start = \iconv_strlen($s, $encoding) + $start; + if ($start < 0) { + $start = 0; + } + } + + if (null === $length) { + $length = 2147483647; + } elseif ($length < 0) { + $length = \iconv_strlen($s, $encoding) + $length - $start; + if ($length < 0) { + return ''; + } + } + + return (string) \iconv_substr($s, $start, $length, $encoding); + } + + public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) + { + $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); + $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); + + return self::mb_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) + { + $pos = self::mb_stripos($haystack, $needle, 0, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + $pos = strrpos($haystack, $needle); + } else { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = \iconv_strrpos($haystack, $needle, $encoding); + } + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) + { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = self::mb_strripos($haystack, $needle, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) + { + $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); + $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); + + return self::mb_strrpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) + { + $pos = strpos($haystack, $needle); + if (false === $pos) { + return false; + } + if ($part) { + return substr($haystack, 0, $pos); + } + + return substr($haystack, $pos); + } + + public static function mb_get_info($type = 'all') + { + $info = [ + 'internal_encoding' => self::$internalEncoding, + 'http_output' => 'pass', + 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', + 'func_overload' => 0, + 'func_overload_list' => 'no overload', + 'mail_charset' => 'UTF-8', + 'mail_header_encoding' => 'BASE64', + 'mail_body_encoding' => 'BASE64', + 'illegal_chars' => 0, + 'encoding_translation' => 'Off', + 'language' => self::$language, + 'detect_order' => self::$encodingList, + 'substitute_character' => 'none', + 'strict_detection' => 'Off', + ]; + + if ('all' === $type) { + return $info; + } + if (isset($info[$type])) { + return $info[$type]; + } + + return false; + } + + public static function mb_http_input($type = '') + { + return false; + } + + public static function mb_http_output($encoding = null) + { + return null !== $encoding ? 'pass' === $encoding : 'pass'; + } + + public static function mb_strwidth($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + + if ('UTF-8' !== $encoding) { + $s = \iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); + + return ($wide << 1) + \iconv_strlen($s, 'UTF-8'); + } + + public static function mb_substr_count($haystack, $needle, $encoding = null) + { + return substr_count($haystack, $needle); + } + + public static function mb_output_handler($contents, $status) + { + return $contents; + } + + public static function mb_chr($code, $encoding = null) + { + if (0x80 > $code %= 0x200000) { + $s = \chr($code); + } elseif (0x800 > $code) { + $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } else { + $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } + + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, $encoding, 'UTF-8'); + } + + return $s; + } + + public static function mb_ord($s, $encoding = null) + { + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, 'UTF-8', $encoding); + } + + if (1 === \strlen($s)) { + return \ord($s); + } + + $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; + if (0xF0 <= $code) { + return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; + } + if (0xE0 <= $code) { + return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; + } + if (0xC0 <= $code) { + return (($code - 0xC0) << 6) + $s[2] - 0x80; + } + + return $code; + } + + private static function getSubpart($pos, $part, $haystack, $encoding) + { + if (false === $pos) { + return false; + } + if ($part) { + return self::mb_substr($haystack, 0, $pos, $encoding); + } + + return self::mb_substr($haystack, $pos, null, $encoding); + } + + private static function html_encoding_callback(array $m) + { + $i = 1; + $entities = ''; + $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8')); + + while (isset($m[$i])) { + if (0x80 > $m[$i]) { + $entities .= \chr($m[$i++]); + continue; + } + if (0xF0 <= $m[$i]) { + $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } elseif (0xE0 <= $m[$i]) { + $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } else { + $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; + } + + $entities .= '&#'.$c.';'; + } + + return $entities; + } + + private static function title_case(array $s) + { + return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); + } + + private static function getData($file) + { + if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { + return require $file; + } + + return false; + } + + private static function getEncoding($encoding) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + if ('UTF-8' === $encoding) { + return 'UTF-8'; + } + + $encoding = strtoupper($encoding); + + if ('8BIT' === $encoding || 'BINARY' === $encoding) { + return 'CP850'; + } + + if ('UTF8' === $encoding) { + return 'UTF-8'; + } + + return $encoding; + } +} diff --git a/vendor/symfony/polyfill-mbstring/README.md b/vendor/symfony/polyfill-mbstring/README.md new file mode 100644 index 0000000..4efb599 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/README.md @@ -0,0 +1,13 @@ +Symfony Polyfill / Mbstring +=========================== + +This component provides a partial, native PHP implementation for the +[Mbstring](https://php.net/mbstring) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php new file mode 100644 index 0000000..fac60b0 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php @@ -0,0 +1,1397 @@ + 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + 'À' => 'à', + 'Á' => 'á', + 'Â' => 'â', + 'Ã' => 'ã', + 'Ä' => 'ä', + 'Å' => 'å', + 'Æ' => 'æ', + 'Ç' => 'ç', + 'È' => 'è', + 'É' => 'é', + 'Ê' => 'ê', + 'Ë' => 'ë', + 'Ì' => 'ì', + 'Í' => 'í', + 'Î' => 'î', + 'Ï' => 'ï', + 'Ð' => 'ð', + 'Ñ' => 'ñ', + 'Ò' => 'ò', + 'Ó' => 'ó', + 'Ô' => 'ô', + 'Õ' => 'õ', + 'Ö' => 'ö', + 'Ø' => 'ø', + 'Ù' => 'ù', + 'Ú' => 'ú', + 'Û' => 'û', + 'Ü' => 'ü', + 'Ý' => 'ý', + 'Þ' => 'þ', + 'Ā' => 'ā', + 'Ă' => 'ă', + 'Ą' => 'ą', + 'Ć' => 'ć', + 'Ĉ' => 'ĉ', + 'Ċ' => 'ċ', + 'Č' => 'č', + 'Ď' => 'ď', + 'Đ' => 'đ', + 'Ē' => 'ē', + 'Ĕ' => 'ĕ', + 'Ė' => 'ė', + 'Ę' => 'ę', + 'Ě' => 'ě', + 'Ĝ' => 'ĝ', + 'Ğ' => 'ğ', + 'Ġ' => 'ġ', + 'Ģ' => 'ģ', + 'Ĥ' => 'ĥ', + 'Ħ' => 'ħ', + 'Ĩ' => 'ĩ', + 'Ī' => 'ī', + 'Ĭ' => 'ĭ', + 'Į' => 'į', + 'İ' => 'i̇', + 'IJ' => 'ij', + 'Ĵ' => 'ĵ', + 'Ķ' => 'ķ', + 'Ĺ' => 'ĺ', + 'Ļ' => 'ļ', + 'Ľ' => 'ľ', + 'Ŀ' => 'ŀ', + 'Ł' => 'ł', + 'Ń' => 'ń', + 'Ņ' => 'ņ', + 'Ň' => 'ň', + 'Ŋ' => 'ŋ', + 'Ō' => 'ō', + 'Ŏ' => 'ŏ', + 'Ő' => 'ő', + 'Œ' => 'œ', + 'Ŕ' => 'ŕ', + 'Ŗ' => 'ŗ', + 'Ř' => 'ř', + 'Ś' => 'ś', + 'Ŝ' => 'ŝ', + 'Ş' => 'ş', + 'Š' => 'š', + 'Ţ' => 'ţ', + 'Ť' => 'ť', + 'Ŧ' => 'ŧ', + 'Ũ' => 'ũ', + 'Ū' => 'ū', + 'Ŭ' => 'ŭ', + 'Ů' => 'ů', + 'Ű' => 'ű', + 'Ų' => 'ų', + 'Ŵ' => 'ŵ', + 'Ŷ' => 'ŷ', + 'Ÿ' => 'ÿ', + 'Ź' => 'ź', + 'Ż' => 'ż', + 'Ž' => 'ž', + 'Ɓ' => 'ɓ', + 'Ƃ' => 'ƃ', + 'Ƅ' => 'ƅ', + 'Ɔ' => 'ɔ', + 'Ƈ' => 'ƈ', + 'Ɖ' => 'ɖ', + 'Ɗ' => 'ɗ', + 'Ƌ' => 'ƌ', + 'Ǝ' => 'ǝ', + 'Ə' => 'ə', + 'Ɛ' => 'ɛ', + 'Ƒ' => 'ƒ', + 'Ɠ' => 'ɠ', + 'Ɣ' => 'ɣ', + 'Ɩ' => 'ɩ', + 'Ɨ' => 'ɨ', + 'Ƙ' => 'ƙ', + 'Ɯ' => 'ɯ', + 'Ɲ' => 'ɲ', + 'Ɵ' => 'ɵ', + 'Ơ' => 'ơ', + 'Ƣ' => 'ƣ', + 'Ƥ' => 'ƥ', + 'Ʀ' => 'ʀ', + 'Ƨ' => 'ƨ', + 'Ʃ' => 'ʃ', + 'Ƭ' => 'ƭ', + 'Ʈ' => 'ʈ', + 'Ư' => 'ư', + 'Ʊ' => 'ʊ', + 'Ʋ' => 'ʋ', + 'Ƴ' => 'ƴ', + 'Ƶ' => 'ƶ', + 'Ʒ' => 'ʒ', + 'Ƹ' => 'ƹ', + 'Ƽ' => 'ƽ', + 'DŽ' => 'dž', + 'Dž' => 'dž', + 'LJ' => 'lj', + 'Lj' => 'lj', + 'NJ' => 'nj', + 'Nj' => 'nj', + 'Ǎ' => 'ǎ', + 'Ǐ' => 'ǐ', + 'Ǒ' => 'ǒ', + 'Ǔ' => 'ǔ', + 'Ǖ' => 'ǖ', + 'Ǘ' => 'ǘ', + 'Ǚ' => 'ǚ', + 'Ǜ' => 'ǜ', + 'Ǟ' => 'ǟ', + 'Ǡ' => 'ǡ', + 'Ǣ' => 'ǣ', + 'Ǥ' => 'ǥ', + 'Ǧ' => 'ǧ', + 'Ǩ' => 'ǩ', + 'Ǫ' => 'ǫ', + 'Ǭ' => 'ǭ', + 'Ǯ' => 'ǯ', + 'DZ' => 'dz', + 'Dz' => 'dz', + 'Ǵ' => 'ǵ', + 'Ƕ' => 'ƕ', + 'Ƿ' => 'ƿ', + 'Ǹ' => 'ǹ', + 'Ǻ' => 'ǻ', + 'Ǽ' => 'ǽ', + 'Ǿ' => 'ǿ', + 'Ȁ' => 'ȁ', + 'Ȃ' => 'ȃ', + 'Ȅ' => 'ȅ', + 'Ȇ' => 'ȇ', + 'Ȉ' => 'ȉ', + 'Ȋ' => 'ȋ', + 'Ȍ' => 'ȍ', + 'Ȏ' => 'ȏ', + 'Ȑ' => 'ȑ', + 'Ȓ' => 'ȓ', + 'Ȕ' => 'ȕ', + 'Ȗ' => 'ȗ', + 'Ș' => 'ș', + 'Ț' => 'ț', + 'Ȝ' => 'ȝ', + 'Ȟ' => 'ȟ', + 'Ƞ' => 'ƞ', + 'Ȣ' => 'ȣ', + 'Ȥ' => 'ȥ', + 'Ȧ' => 'ȧ', + 'Ȩ' => 'ȩ', + 'Ȫ' => 'ȫ', + 'Ȭ' => 'ȭ', + 'Ȯ' => 'ȯ', + 'Ȱ' => 'ȱ', + 'Ȳ' => 'ȳ', + 'Ⱥ' => 'ⱥ', + 'Ȼ' => 'ȼ', + 'Ƚ' => 'ƚ', + 'Ⱦ' => 'ⱦ', + 'Ɂ' => 'ɂ', + 'Ƀ' => 'ƀ', + 'Ʉ' => 'ʉ', + 'Ʌ' => 'ʌ', + 'Ɇ' => 'ɇ', + 'Ɉ' => 'ɉ', + 'Ɋ' => 'ɋ', + 'Ɍ' => 'ɍ', + 'Ɏ' => 'ɏ', + 'Ͱ' => 'ͱ', + 'Ͳ' => 'ͳ', + 'Ͷ' => 'ͷ', + 'Ϳ' => 'ϳ', + 'Ά' => 'ά', + 'Έ' => 'έ', + 'Ή' => 'ή', + 'Ί' => 'ί', + 'Ό' => 'ό', + 'Ύ' => 'ύ', + 'Ώ' => 'ώ', + 'Α' => 'α', + 'Β' => 'β', + 'Γ' => 'γ', + 'Δ' => 'δ', + 'Ε' => 'ε', + 'Ζ' => 'ζ', + 'Η' => 'η', + 'Θ' => 'θ', + 'Ι' => 'ι', + 'Κ' => 'κ', + 'Λ' => 'λ', + 'Μ' => 'μ', + 'Ν' => 'ν', + 'Ξ' => 'ξ', + 'Ο' => 'ο', + 'Π' => 'π', + 'Ρ' => 'ρ', + 'Σ' => 'σ', + 'Τ' => 'τ', + 'Υ' => 'υ', + 'Φ' => 'φ', + 'Χ' => 'χ', + 'Ψ' => 'ψ', + 'Ω' => 'ω', + 'Ϊ' => 'ϊ', + 'Ϋ' => 'ϋ', + 'Ϗ' => 'ϗ', + 'Ϙ' => 'ϙ', + 'Ϛ' => 'ϛ', + 'Ϝ' => 'ϝ', + 'Ϟ' => 'ϟ', + 'Ϡ' => 'ϡ', + 'Ϣ' => 'ϣ', + 'Ϥ' => 'ϥ', + 'Ϧ' => 'ϧ', + 'Ϩ' => 'ϩ', + 'Ϫ' => 'ϫ', + 'Ϭ' => 'ϭ', + 'Ϯ' => 'ϯ', + 'ϴ' => 'θ', + 'Ϸ' => 'ϸ', + 'Ϲ' => 'ϲ', + 'Ϻ' => 'ϻ', + 'Ͻ' => 'ͻ', + 'Ͼ' => 'ͼ', + 'Ͽ' => 'ͽ', + 'Ѐ' => 'ѐ', + 'Ё' => 'ё', + 'Ђ' => 'ђ', + 'Ѓ' => 'ѓ', + 'Є' => 'є', + 'Ѕ' => 'ѕ', + 'І' => 'і', + 'Ї' => 'ї', + 'Ј' => 'ј', + 'Љ' => 'љ', + 'Њ' => 'њ', + 'Ћ' => 'ћ', + 'Ќ' => 'ќ', + 'Ѝ' => 'ѝ', + 'Ў' => 'ў', + 'Џ' => 'џ', + 'А' => 'а', + 'Б' => 'б', + 'В' => 'в', + 'Г' => 'г', + 'Д' => 'д', + 'Е' => 'е', + 'Ж' => 'ж', + 'З' => 'з', + 'И' => 'и', + 'Й' => 'й', + 'К' => 'к', + 'Л' => 'л', + 'М' => 'м', + 'Н' => 'н', + 'О' => 'о', + 'П' => 'п', + 'Р' => 'р', + 'С' => 'с', + 'Т' => 'т', + 'У' => 'у', + 'Ф' => 'ф', + 'Х' => 'х', + 'Ц' => 'ц', + 'Ч' => 'ч', + 'Ш' => 'ш', + 'Щ' => 'щ', + 'Ъ' => 'ъ', + 'Ы' => 'ы', + 'Ь' => 'ь', + 'Э' => 'э', + 'Ю' => 'ю', + 'Я' => 'я', + 'Ѡ' => 'ѡ', + 'Ѣ' => 'ѣ', + 'Ѥ' => 'ѥ', + 'Ѧ' => 'ѧ', + 'Ѩ' => 'ѩ', + 'Ѫ' => 'ѫ', + 'Ѭ' => 'ѭ', + 'Ѯ' => 'ѯ', + 'Ѱ' => 'ѱ', + 'Ѳ' => 'ѳ', + 'Ѵ' => 'ѵ', + 'Ѷ' => 'ѷ', + 'Ѹ' => 'ѹ', + 'Ѻ' => 'ѻ', + 'Ѽ' => 'ѽ', + 'Ѿ' => 'ѿ', + 'Ҁ' => 'ҁ', + 'Ҋ' => 'ҋ', + 'Ҍ' => 'ҍ', + 'Ҏ' => 'ҏ', + 'Ґ' => 'ґ', + 'Ғ' => 'ғ', + 'Ҕ' => 'ҕ', + 'Җ' => 'җ', + 'Ҙ' => 'ҙ', + 'Қ' => 'қ', + 'Ҝ' => 'ҝ', + 'Ҟ' => 'ҟ', + 'Ҡ' => 'ҡ', + 'Ң' => 'ң', + 'Ҥ' => 'ҥ', + 'Ҧ' => 'ҧ', + 'Ҩ' => 'ҩ', + 'Ҫ' => 'ҫ', + 'Ҭ' => 'ҭ', + 'Ү' => 'ү', + 'Ұ' => 'ұ', + 'Ҳ' => 'ҳ', + 'Ҵ' => 'ҵ', + 'Ҷ' => 'ҷ', + 'Ҹ' => 'ҹ', + 'Һ' => 'һ', + 'Ҽ' => 'ҽ', + 'Ҿ' => 'ҿ', + 'Ӏ' => 'ӏ', + 'Ӂ' => 'ӂ', + 'Ӄ' => 'ӄ', + 'Ӆ' => 'ӆ', + 'Ӈ' => 'ӈ', + 'Ӊ' => 'ӊ', + 'Ӌ' => 'ӌ', + 'Ӎ' => 'ӎ', + 'Ӑ' => 'ӑ', + 'Ӓ' => 'ӓ', + 'Ӕ' => 'ӕ', + 'Ӗ' => 'ӗ', + 'Ә' => 'ә', + 'Ӛ' => 'ӛ', + 'Ӝ' => 'ӝ', + 'Ӟ' => 'ӟ', + 'Ӡ' => 'ӡ', + 'Ӣ' => 'ӣ', + 'Ӥ' => 'ӥ', + 'Ӧ' => 'ӧ', + 'Ө' => 'ө', + 'Ӫ' => 'ӫ', + 'Ӭ' => 'ӭ', + 'Ӯ' => 'ӯ', + 'Ӱ' => 'ӱ', + 'Ӳ' => 'ӳ', + 'Ӵ' => 'ӵ', + 'Ӷ' => 'ӷ', + 'Ӹ' => 'ӹ', + 'Ӻ' => 'ӻ', + 'Ӽ' => 'ӽ', + 'Ӿ' => 'ӿ', + 'Ԁ' => 'ԁ', + 'Ԃ' => 'ԃ', + 'Ԅ' => 'ԅ', + 'Ԇ' => 'ԇ', + 'Ԉ' => 'ԉ', + 'Ԋ' => 'ԋ', + 'Ԍ' => 'ԍ', + 'Ԏ' => 'ԏ', + 'Ԑ' => 'ԑ', + 'Ԓ' => 'ԓ', + 'Ԕ' => 'ԕ', + 'Ԗ' => 'ԗ', + 'Ԙ' => 'ԙ', + 'Ԛ' => 'ԛ', + 'Ԝ' => 'ԝ', + 'Ԟ' => 'ԟ', + 'Ԡ' => 'ԡ', + 'Ԣ' => 'ԣ', + 'Ԥ' => 'ԥ', + 'Ԧ' => 'ԧ', + 'Ԩ' => 'ԩ', + 'Ԫ' => 'ԫ', + 'Ԭ' => 'ԭ', + 'Ԯ' => 'ԯ', + 'Ա' => 'ա', + 'Բ' => 'բ', + 'Գ' => 'գ', + 'Դ' => 'դ', + 'Ե' => 'ե', + 'Զ' => 'զ', + 'Է' => 'է', + 'Ը' => 'ը', + 'Թ' => 'թ', + 'Ժ' => 'ժ', + 'Ի' => 'ի', + 'Լ' => 'լ', + 'Խ' => 'խ', + 'Ծ' => 'ծ', + 'Կ' => 'կ', + 'Հ' => 'հ', + 'Ձ' => 'ձ', + 'Ղ' => 'ղ', + 'Ճ' => 'ճ', + 'Մ' => 'մ', + 'Յ' => 'յ', + 'Ն' => 'ն', + 'Շ' => 'շ', + 'Ո' => 'ո', + 'Չ' => 'չ', + 'Պ' => 'պ', + 'Ջ' => 'ջ', + 'Ռ' => 'ռ', + 'Ս' => 'ս', + 'Վ' => 'վ', + 'Տ' => 'տ', + 'Ր' => 'ր', + 'Ց' => 'ց', + 'Ւ' => 'ւ', + 'Փ' => 'փ', + 'Ք' => 'ք', + 'Օ' => 'օ', + 'Ֆ' => 'ֆ', + 'Ⴀ' => 'ⴀ', + 'Ⴁ' => 'ⴁ', + 'Ⴂ' => 'ⴂ', + 'Ⴃ' => 'ⴃ', + 'Ⴄ' => 'ⴄ', + 'Ⴅ' => 'ⴅ', + 'Ⴆ' => 'ⴆ', + 'Ⴇ' => 'ⴇ', + 'Ⴈ' => 'ⴈ', + 'Ⴉ' => 'ⴉ', + 'Ⴊ' => 'ⴊ', + 'Ⴋ' => 'ⴋ', + 'Ⴌ' => 'ⴌ', + 'Ⴍ' => 'ⴍ', + 'Ⴎ' => 'ⴎ', + 'Ⴏ' => 'ⴏ', + 'Ⴐ' => 'ⴐ', + 'Ⴑ' => 'ⴑ', + 'Ⴒ' => 'ⴒ', + 'Ⴓ' => 'ⴓ', + 'Ⴔ' => 'ⴔ', + 'Ⴕ' => 'ⴕ', + 'Ⴖ' => 'ⴖ', + 'Ⴗ' => 'ⴗ', + 'Ⴘ' => 'ⴘ', + 'Ⴙ' => 'ⴙ', + 'Ⴚ' => 'ⴚ', + 'Ⴛ' => 'ⴛ', + 'Ⴜ' => 'ⴜ', + 'Ⴝ' => 'ⴝ', + 'Ⴞ' => 'ⴞ', + 'Ⴟ' => 'ⴟ', + 'Ⴠ' => 'ⴠ', + 'Ⴡ' => 'ⴡ', + 'Ⴢ' => 'ⴢ', + 'Ⴣ' => 'ⴣ', + 'Ⴤ' => 'ⴤ', + 'Ⴥ' => 'ⴥ', + 'Ⴧ' => 'ⴧ', + 'Ⴭ' => 'ⴭ', + 'Ꭰ' => 'ꭰ', + 'Ꭱ' => 'ꭱ', + 'Ꭲ' => 'ꭲ', + 'Ꭳ' => 'ꭳ', + 'Ꭴ' => 'ꭴ', + 'Ꭵ' => 'ꭵ', + 'Ꭶ' => 'ꭶ', + 'Ꭷ' => 'ꭷ', + 'Ꭸ' => 'ꭸ', + 'Ꭹ' => 'ꭹ', + 'Ꭺ' => 'ꭺ', + 'Ꭻ' => 'ꭻ', + 'Ꭼ' => 'ꭼ', + 'Ꭽ' => 'ꭽ', + 'Ꭾ' => 'ꭾ', + 'Ꭿ' => 'ꭿ', + 'Ꮀ' => 'ꮀ', + 'Ꮁ' => 'ꮁ', + 'Ꮂ' => 'ꮂ', + 'Ꮃ' => 'ꮃ', + 'Ꮄ' => 'ꮄ', + 'Ꮅ' => 'ꮅ', + 'Ꮆ' => 'ꮆ', + 'Ꮇ' => 'ꮇ', + 'Ꮈ' => 'ꮈ', + 'Ꮉ' => 'ꮉ', + 'Ꮊ' => 'ꮊ', + 'Ꮋ' => 'ꮋ', + 'Ꮌ' => 'ꮌ', + 'Ꮍ' => 'ꮍ', + 'Ꮎ' => 'ꮎ', + 'Ꮏ' => 'ꮏ', + 'Ꮐ' => 'ꮐ', + 'Ꮑ' => 'ꮑ', + 'Ꮒ' => 'ꮒ', + 'Ꮓ' => 'ꮓ', + 'Ꮔ' => 'ꮔ', + 'Ꮕ' => 'ꮕ', + 'Ꮖ' => 'ꮖ', + 'Ꮗ' => 'ꮗ', + 'Ꮘ' => 'ꮘ', + 'Ꮙ' => 'ꮙ', + 'Ꮚ' => 'ꮚ', + 'Ꮛ' => 'ꮛ', + 'Ꮜ' => 'ꮜ', + 'Ꮝ' => 'ꮝ', + 'Ꮞ' => 'ꮞ', + 'Ꮟ' => 'ꮟ', + 'Ꮠ' => 'ꮠ', + 'Ꮡ' => 'ꮡ', + 'Ꮢ' => 'ꮢ', + 'Ꮣ' => 'ꮣ', + 'Ꮤ' => 'ꮤ', + 'Ꮥ' => 'ꮥ', + 'Ꮦ' => 'ꮦ', + 'Ꮧ' => 'ꮧ', + 'Ꮨ' => 'ꮨ', + 'Ꮩ' => 'ꮩ', + 'Ꮪ' => 'ꮪ', + 'Ꮫ' => 'ꮫ', + 'Ꮬ' => 'ꮬ', + 'Ꮭ' => 'ꮭ', + 'Ꮮ' => 'ꮮ', + 'Ꮯ' => 'ꮯ', + 'Ꮰ' => 'ꮰ', + 'Ꮱ' => 'ꮱ', + 'Ꮲ' => 'ꮲ', + 'Ꮳ' => 'ꮳ', + 'Ꮴ' => 'ꮴ', + 'Ꮵ' => 'ꮵ', + 'Ꮶ' => 'ꮶ', + 'Ꮷ' => 'ꮷ', + 'Ꮸ' => 'ꮸ', + 'Ꮹ' => 'ꮹ', + 'Ꮺ' => 'ꮺ', + 'Ꮻ' => 'ꮻ', + 'Ꮼ' => 'ꮼ', + 'Ꮽ' => 'ꮽ', + 'Ꮾ' => 'ꮾ', + 'Ꮿ' => 'ꮿ', + 'Ᏸ' => 'ᏸ', + 'Ᏹ' => 'ᏹ', + 'Ᏺ' => 'ᏺ', + 'Ᏻ' => 'ᏻ', + 'Ᏼ' => 'ᏼ', + 'Ᏽ' => 'ᏽ', + 'Ა' => 'ა', + 'Ბ' => 'ბ', + 'Გ' => 'გ', + 'Დ' => 'დ', + 'Ე' => 'ე', + 'Ვ' => 'ვ', + 'Ზ' => 'ზ', + 'Თ' => 'თ', + 'Ი' => 'ი', + 'Კ' => 'კ', + 'Ლ' => 'ლ', + 'Მ' => 'მ', + 'Ნ' => 'ნ', + 'Ო' => 'ო', + 'Პ' => 'პ', + 'Ჟ' => 'ჟ', + 'Რ' => 'რ', + 'Ს' => 'ს', + 'Ტ' => 'ტ', + 'Უ' => 'უ', + 'Ფ' => 'ფ', + 'Ქ' => 'ქ', + 'Ღ' => 'ღ', + 'Ყ' => 'ყ', + 'Შ' => 'შ', + 'Ჩ' => 'ჩ', + 'Ც' => 'ც', + 'Ძ' => 'ძ', + 'Წ' => 'წ', + 'Ჭ' => 'ჭ', + 'Ხ' => 'ხ', + 'Ჯ' => 'ჯ', + 'Ჰ' => 'ჰ', + 'Ჱ' => 'ჱ', + 'Ჲ' => 'ჲ', + 'Ჳ' => 'ჳ', + 'Ჴ' => 'ჴ', + 'Ჵ' => 'ჵ', + 'Ჶ' => 'ჶ', + 'Ჷ' => 'ჷ', + 'Ჸ' => 'ჸ', + 'Ჹ' => 'ჹ', + 'Ჺ' => 'ჺ', + 'Ჽ' => 'ჽ', + 'Ჾ' => 'ჾ', + 'Ჿ' => 'ჿ', + 'Ḁ' => 'ḁ', + 'Ḃ' => 'ḃ', + 'Ḅ' => 'ḅ', + 'Ḇ' => 'ḇ', + 'Ḉ' => 'ḉ', + 'Ḋ' => 'ḋ', + 'Ḍ' => 'ḍ', + 'Ḏ' => 'ḏ', + 'Ḑ' => 'ḑ', + 'Ḓ' => 'ḓ', + 'Ḕ' => 'ḕ', + 'Ḗ' => 'ḗ', + 'Ḙ' => 'ḙ', + 'Ḛ' => 'ḛ', + 'Ḝ' => 'ḝ', + 'Ḟ' => 'ḟ', + 'Ḡ' => 'ḡ', + 'Ḣ' => 'ḣ', + 'Ḥ' => 'ḥ', + 'Ḧ' => 'ḧ', + 'Ḩ' => 'ḩ', + 'Ḫ' => 'ḫ', + 'Ḭ' => 'ḭ', + 'Ḯ' => 'ḯ', + 'Ḱ' => 'ḱ', + 'Ḳ' => 'ḳ', + 'Ḵ' => 'ḵ', + 'Ḷ' => 'ḷ', + 'Ḹ' => 'ḹ', + 'Ḻ' => 'ḻ', + 'Ḽ' => 'ḽ', + 'Ḿ' => 'ḿ', + 'Ṁ' => 'ṁ', + 'Ṃ' => 'ṃ', + 'Ṅ' => 'ṅ', + 'Ṇ' => 'ṇ', + 'Ṉ' => 'ṉ', + 'Ṋ' => 'ṋ', + 'Ṍ' => 'ṍ', + 'Ṏ' => 'ṏ', + 'Ṑ' => 'ṑ', + 'Ṓ' => 'ṓ', + 'Ṕ' => 'ṕ', + 'Ṗ' => 'ṗ', + 'Ṙ' => 'ṙ', + 'Ṛ' => 'ṛ', + 'Ṝ' => 'ṝ', + 'Ṟ' => 'ṟ', + 'Ṡ' => 'ṡ', + 'Ṣ' => 'ṣ', + 'Ṥ' => 'ṥ', + 'Ṧ' => 'ṧ', + 'Ṩ' => 'ṩ', + 'Ṫ' => 'ṫ', + 'Ṭ' => 'ṭ', + 'Ṯ' => 'ṯ', + 'Ṱ' => 'ṱ', + 'Ṳ' => 'ṳ', + 'Ṵ' => 'ṵ', + 'Ṷ' => 'ṷ', + 'Ṹ' => 'ṹ', + 'Ṻ' => 'ṻ', + 'Ṽ' => 'ṽ', + 'Ṿ' => 'ṿ', + 'Ẁ' => 'ẁ', + 'Ẃ' => 'ẃ', + 'Ẅ' => 'ẅ', + 'Ẇ' => 'ẇ', + 'Ẉ' => 'ẉ', + 'Ẋ' => 'ẋ', + 'Ẍ' => 'ẍ', + 'Ẏ' => 'ẏ', + 'Ẑ' => 'ẑ', + 'Ẓ' => 'ẓ', + 'Ẕ' => 'ẕ', + 'ẞ' => 'ß', + 'Ạ' => 'ạ', + 'Ả' => 'ả', + 'Ấ' => 'ấ', + 'Ầ' => 'ầ', + 'Ẩ' => 'ẩ', + 'Ẫ' => 'ẫ', + 'Ậ' => 'ậ', + 'Ắ' => 'ắ', + 'Ằ' => 'ằ', + 'Ẳ' => 'ẳ', + 'Ẵ' => 'ẵ', + 'Ặ' => 'ặ', + 'Ẹ' => 'ẹ', + 'Ẻ' => 'ẻ', + 'Ẽ' => 'ẽ', + 'Ế' => 'ế', + 'Ề' => 'ề', + 'Ể' => 'ể', + 'Ễ' => 'ễ', + 'Ệ' => 'ệ', + 'Ỉ' => 'ỉ', + 'Ị' => 'ị', + 'Ọ' => 'ọ', + 'Ỏ' => 'ỏ', + 'Ố' => 'ố', + 'Ồ' => 'ồ', + 'Ổ' => 'ổ', + 'Ỗ' => 'ỗ', + 'Ộ' => 'ộ', + 'Ớ' => 'ớ', + 'Ờ' => 'ờ', + 'Ở' => 'ở', + 'Ỡ' => 'ỡ', + 'Ợ' => 'ợ', + 'Ụ' => 'ụ', + 'Ủ' => 'ủ', + 'Ứ' => 'ứ', + 'Ừ' => 'ừ', + 'Ử' => 'ử', + 'Ữ' => 'ữ', + 'Ự' => 'ự', + 'Ỳ' => 'ỳ', + 'Ỵ' => 'ỵ', + 'Ỷ' => 'ỷ', + 'Ỹ' => 'ỹ', + 'Ỻ' => 'ỻ', + 'Ỽ' => 'ỽ', + 'Ỿ' => 'ỿ', + 'Ἀ' => 'ἀ', + 'Ἁ' => 'ἁ', + 'Ἂ' => 'ἂ', + 'Ἃ' => 'ἃ', + 'Ἄ' => 'ἄ', + 'Ἅ' => 'ἅ', + 'Ἆ' => 'ἆ', + 'Ἇ' => 'ἇ', + 'Ἐ' => 'ἐ', + 'Ἑ' => 'ἑ', + 'Ἒ' => 'ἒ', + 'Ἓ' => 'ἓ', + 'Ἔ' => 'ἔ', + 'Ἕ' => 'ἕ', + 'Ἠ' => 'ἠ', + 'Ἡ' => 'ἡ', + 'Ἢ' => 'ἢ', + 'Ἣ' => 'ἣ', + 'Ἤ' => 'ἤ', + 'Ἥ' => 'ἥ', + 'Ἦ' => 'ἦ', + 'Ἧ' => 'ἧ', + 'Ἰ' => 'ἰ', + 'Ἱ' => 'ἱ', + 'Ἲ' => 'ἲ', + 'Ἳ' => 'ἳ', + 'Ἴ' => 'ἴ', + 'Ἵ' => 'ἵ', + 'Ἶ' => 'ἶ', + 'Ἷ' => 'ἷ', + 'Ὀ' => 'ὀ', + 'Ὁ' => 'ὁ', + 'Ὂ' => 'ὂ', + 'Ὃ' => 'ὃ', + 'Ὄ' => 'ὄ', + 'Ὅ' => 'ὅ', + 'Ὑ' => 'ὑ', + 'Ὓ' => 'ὓ', + 'Ὕ' => 'ὕ', + 'Ὗ' => 'ὗ', + 'Ὠ' => 'ὠ', + 'Ὡ' => 'ὡ', + 'Ὢ' => 'ὢ', + 'Ὣ' => 'ὣ', + 'Ὤ' => 'ὤ', + 'Ὥ' => 'ὥ', + 'Ὦ' => 'ὦ', + 'Ὧ' => 'ὧ', + 'ᾈ' => 'ᾀ', + 'ᾉ' => 'ᾁ', + 'ᾊ' => 'ᾂ', + 'ᾋ' => 'ᾃ', + 'ᾌ' => 'ᾄ', + 'ᾍ' => 'ᾅ', + 'ᾎ' => 'ᾆ', + 'ᾏ' => 'ᾇ', + 'ᾘ' => 'ᾐ', + 'ᾙ' => 'ᾑ', + 'ᾚ' => 'ᾒ', + 'ᾛ' => 'ᾓ', + 'ᾜ' => 'ᾔ', + 'ᾝ' => 'ᾕ', + 'ᾞ' => 'ᾖ', + 'ᾟ' => 'ᾗ', + 'ᾨ' => 'ᾠ', + 'ᾩ' => 'ᾡ', + 'ᾪ' => 'ᾢ', + 'ᾫ' => 'ᾣ', + 'ᾬ' => 'ᾤ', + 'ᾭ' => 'ᾥ', + 'ᾮ' => 'ᾦ', + 'ᾯ' => 'ᾧ', + 'Ᾰ' => 'ᾰ', + 'Ᾱ' => 'ᾱ', + 'Ὰ' => 'ὰ', + 'Ά' => 'ά', + 'ᾼ' => 'ᾳ', + 'Ὲ' => 'ὲ', + 'Έ' => 'έ', + 'Ὴ' => 'ὴ', + 'Ή' => 'ή', + 'ῌ' => 'ῃ', + 'Ῐ' => 'ῐ', + 'Ῑ' => 'ῑ', + 'Ὶ' => 'ὶ', + 'Ί' => 'ί', + 'Ῠ' => 'ῠ', + 'Ῡ' => 'ῡ', + 'Ὺ' => 'ὺ', + 'Ύ' => 'ύ', + 'Ῥ' => 'ῥ', + 'Ὸ' => 'ὸ', + 'Ό' => 'ό', + 'Ὼ' => 'ὼ', + 'Ώ' => 'ώ', + 'ῼ' => 'ῳ', + 'Ω' => 'ω', + 'K' => 'k', + 'Å' => 'å', + 'Ⅎ' => 'ⅎ', + 'Ⅰ' => 'ⅰ', + 'Ⅱ' => 'ⅱ', + 'Ⅲ' => 'ⅲ', + 'Ⅳ' => 'ⅳ', + 'Ⅴ' => 'ⅴ', + 'Ⅵ' => 'ⅵ', + 'Ⅶ' => 'ⅶ', + 'Ⅷ' => 'ⅷ', + 'Ⅸ' => 'ⅸ', + 'Ⅹ' => 'ⅹ', + 'Ⅺ' => 'ⅺ', + 'Ⅻ' => 'ⅻ', + 'Ⅼ' => 'ⅼ', + 'Ⅽ' => 'ⅽ', + 'Ⅾ' => 'ⅾ', + 'Ⅿ' => 'ⅿ', + 'Ↄ' => 'ↄ', + 'Ⓐ' => 'ⓐ', + 'Ⓑ' => 'ⓑ', + 'Ⓒ' => 'ⓒ', + 'Ⓓ' => 'ⓓ', + 'Ⓔ' => 'ⓔ', + 'Ⓕ' => 'ⓕ', + 'Ⓖ' => 'ⓖ', + 'Ⓗ' => 'ⓗ', + 'Ⓘ' => 'ⓘ', + 'Ⓙ' => 'ⓙ', + 'Ⓚ' => 'ⓚ', + 'Ⓛ' => 'ⓛ', + 'Ⓜ' => 'ⓜ', + 'Ⓝ' => 'ⓝ', + 'Ⓞ' => 'ⓞ', + 'Ⓟ' => 'ⓟ', + 'Ⓠ' => 'ⓠ', + 'Ⓡ' => 'ⓡ', + 'Ⓢ' => 'ⓢ', + 'Ⓣ' => 'ⓣ', + 'Ⓤ' => 'ⓤ', + 'Ⓥ' => 'ⓥ', + 'Ⓦ' => 'ⓦ', + 'Ⓧ' => 'ⓧ', + 'Ⓨ' => 'ⓨ', + 'Ⓩ' => 'ⓩ', + 'Ⰰ' => 'ⰰ', + 'Ⰱ' => 'ⰱ', + 'Ⰲ' => 'ⰲ', + 'Ⰳ' => 'ⰳ', + 'Ⰴ' => 'ⰴ', + 'Ⰵ' => 'ⰵ', + 'Ⰶ' => 'ⰶ', + 'Ⰷ' => 'ⰷ', + 'Ⰸ' => 'ⰸ', + 'Ⰹ' => 'ⰹ', + 'Ⰺ' => 'ⰺ', + 'Ⰻ' => 'ⰻ', + 'Ⰼ' => 'ⰼ', + 'Ⰽ' => 'ⰽ', + 'Ⰾ' => 'ⰾ', + 'Ⰿ' => 'ⰿ', + 'Ⱀ' => 'ⱀ', + 'Ⱁ' => 'ⱁ', + 'Ⱂ' => 'ⱂ', + 'Ⱃ' => 'ⱃ', + 'Ⱄ' => 'ⱄ', + 'Ⱅ' => 'ⱅ', + 'Ⱆ' => 'ⱆ', + 'Ⱇ' => 'ⱇ', + 'Ⱈ' => 'ⱈ', + 'Ⱉ' => 'ⱉ', + 'Ⱊ' => 'ⱊ', + 'Ⱋ' => 'ⱋ', + 'Ⱌ' => 'ⱌ', + 'Ⱍ' => 'ⱍ', + 'Ⱎ' => 'ⱎ', + 'Ⱏ' => 'ⱏ', + 'Ⱐ' => 'ⱐ', + 'Ⱑ' => 'ⱑ', + 'Ⱒ' => 'ⱒ', + 'Ⱓ' => 'ⱓ', + 'Ⱔ' => 'ⱔ', + 'Ⱕ' => 'ⱕ', + 'Ⱖ' => 'ⱖ', + 'Ⱗ' => 'ⱗ', + 'Ⱘ' => 'ⱘ', + 'Ⱙ' => 'ⱙ', + 'Ⱚ' => 'ⱚ', + 'Ⱛ' => 'ⱛ', + 'Ⱜ' => 'ⱜ', + 'Ⱝ' => 'ⱝ', + 'Ⱞ' => 'ⱞ', + 'Ⱡ' => 'ⱡ', + 'Ɫ' => 'ɫ', + 'Ᵽ' => 'ᵽ', + 'Ɽ' => 'ɽ', + 'Ⱨ' => 'ⱨ', + 'Ⱪ' => 'ⱪ', + 'Ⱬ' => 'ⱬ', + 'Ɑ' => 'ɑ', + 'Ɱ' => 'ɱ', + 'Ɐ' => 'ɐ', + 'Ɒ' => 'ɒ', + 'Ⱳ' => 'ⱳ', + 'Ⱶ' => 'ⱶ', + 'Ȿ' => 'ȿ', + 'Ɀ' => 'ɀ', + 'Ⲁ' => 'ⲁ', + 'Ⲃ' => 'ⲃ', + 'Ⲅ' => 'ⲅ', + 'Ⲇ' => 'ⲇ', + 'Ⲉ' => 'ⲉ', + 'Ⲋ' => 'ⲋ', + 'Ⲍ' => 'ⲍ', + 'Ⲏ' => 'ⲏ', + 'Ⲑ' => 'ⲑ', + 'Ⲓ' => 'ⲓ', + 'Ⲕ' => 'ⲕ', + 'Ⲗ' => 'ⲗ', + 'Ⲙ' => 'ⲙ', + 'Ⲛ' => 'ⲛ', + 'Ⲝ' => 'ⲝ', + 'Ⲟ' => 'ⲟ', + 'Ⲡ' => 'ⲡ', + 'Ⲣ' => 'ⲣ', + 'Ⲥ' => 'ⲥ', + 'Ⲧ' => 'ⲧ', + 'Ⲩ' => 'ⲩ', + 'Ⲫ' => 'ⲫ', + 'Ⲭ' => 'ⲭ', + 'Ⲯ' => 'ⲯ', + 'Ⲱ' => 'ⲱ', + 'Ⲳ' => 'ⲳ', + 'Ⲵ' => 'ⲵ', + 'Ⲷ' => 'ⲷ', + 'Ⲹ' => 'ⲹ', + 'Ⲻ' => 'ⲻ', + 'Ⲽ' => 'ⲽ', + 'Ⲿ' => 'ⲿ', + 'Ⳁ' => 'ⳁ', + 'Ⳃ' => 'ⳃ', + 'Ⳅ' => 'ⳅ', + 'Ⳇ' => 'ⳇ', + 'Ⳉ' => 'ⳉ', + 'Ⳋ' => 'ⳋ', + 'Ⳍ' => 'ⳍ', + 'Ⳏ' => 'ⳏ', + 'Ⳑ' => 'ⳑ', + 'Ⳓ' => 'ⳓ', + 'Ⳕ' => 'ⳕ', + 'Ⳗ' => 'ⳗ', + 'Ⳙ' => 'ⳙ', + 'Ⳛ' => 'ⳛ', + 'Ⳝ' => 'ⳝ', + 'Ⳟ' => 'ⳟ', + 'Ⳡ' => 'ⳡ', + 'Ⳣ' => 'ⳣ', + 'Ⳬ' => 'ⳬ', + 'Ⳮ' => 'ⳮ', + 'Ⳳ' => 'ⳳ', + 'Ꙁ' => 'ꙁ', + 'Ꙃ' => 'ꙃ', + 'Ꙅ' => 'ꙅ', + 'Ꙇ' => 'ꙇ', + 'Ꙉ' => 'ꙉ', + 'Ꙋ' => 'ꙋ', + 'Ꙍ' => 'ꙍ', + 'Ꙏ' => 'ꙏ', + 'Ꙑ' => 'ꙑ', + 'Ꙓ' => 'ꙓ', + 'Ꙕ' => 'ꙕ', + 'Ꙗ' => 'ꙗ', + 'Ꙙ' => 'ꙙ', + 'Ꙛ' => 'ꙛ', + 'Ꙝ' => 'ꙝ', + 'Ꙟ' => 'ꙟ', + 'Ꙡ' => 'ꙡ', + 'Ꙣ' => 'ꙣ', + 'Ꙥ' => 'ꙥ', + 'Ꙧ' => 'ꙧ', + 'Ꙩ' => 'ꙩ', + 'Ꙫ' => 'ꙫ', + 'Ꙭ' => 'ꙭ', + 'Ꚁ' => 'ꚁ', + 'Ꚃ' => 'ꚃ', + 'Ꚅ' => 'ꚅ', + 'Ꚇ' => 'ꚇ', + 'Ꚉ' => 'ꚉ', + 'Ꚋ' => 'ꚋ', + 'Ꚍ' => 'ꚍ', + 'Ꚏ' => 'ꚏ', + 'Ꚑ' => 'ꚑ', + 'Ꚓ' => 'ꚓ', + 'Ꚕ' => 'ꚕ', + 'Ꚗ' => 'ꚗ', + 'Ꚙ' => 'ꚙ', + 'Ꚛ' => 'ꚛ', + 'Ꜣ' => 'ꜣ', + 'Ꜥ' => 'ꜥ', + 'Ꜧ' => 'ꜧ', + 'Ꜩ' => 'ꜩ', + 'Ꜫ' => 'ꜫ', + 'Ꜭ' => 'ꜭ', + 'Ꜯ' => 'ꜯ', + 'Ꜳ' => 'ꜳ', + 'Ꜵ' => 'ꜵ', + 'Ꜷ' => 'ꜷ', + 'Ꜹ' => 'ꜹ', + 'Ꜻ' => 'ꜻ', + 'Ꜽ' => 'ꜽ', + 'Ꜿ' => 'ꜿ', + 'Ꝁ' => 'ꝁ', + 'Ꝃ' => 'ꝃ', + 'Ꝅ' => 'ꝅ', + 'Ꝇ' => 'ꝇ', + 'Ꝉ' => 'ꝉ', + 'Ꝋ' => 'ꝋ', + 'Ꝍ' => 'ꝍ', + 'Ꝏ' => 'ꝏ', + 'Ꝑ' => 'ꝑ', + 'Ꝓ' => 'ꝓ', + 'Ꝕ' => 'ꝕ', + 'Ꝗ' => 'ꝗ', + 'Ꝙ' => 'ꝙ', + 'Ꝛ' => 'ꝛ', + 'Ꝝ' => 'ꝝ', + 'Ꝟ' => 'ꝟ', + 'Ꝡ' => 'ꝡ', + 'Ꝣ' => 'ꝣ', + 'Ꝥ' => 'ꝥ', + 'Ꝧ' => 'ꝧ', + 'Ꝩ' => 'ꝩ', + 'Ꝫ' => 'ꝫ', + 'Ꝭ' => 'ꝭ', + 'Ꝯ' => 'ꝯ', + 'Ꝺ' => 'ꝺ', + 'Ꝼ' => 'ꝼ', + 'Ᵹ' => 'ᵹ', + 'Ꝿ' => 'ꝿ', + 'Ꞁ' => 'ꞁ', + 'Ꞃ' => 'ꞃ', + 'Ꞅ' => 'ꞅ', + 'Ꞇ' => 'ꞇ', + 'Ꞌ' => 'ꞌ', + 'Ɥ' => 'ɥ', + 'Ꞑ' => 'ꞑ', + 'Ꞓ' => 'ꞓ', + 'Ꞗ' => 'ꞗ', + 'Ꞙ' => 'ꞙ', + 'Ꞛ' => 'ꞛ', + 'Ꞝ' => 'ꞝ', + 'Ꞟ' => 'ꞟ', + 'Ꞡ' => 'ꞡ', + 'Ꞣ' => 'ꞣ', + 'Ꞥ' => 'ꞥ', + 'Ꞧ' => 'ꞧ', + 'Ꞩ' => 'ꞩ', + 'Ɦ' => 'ɦ', + 'Ɜ' => 'ɜ', + 'Ɡ' => 'ɡ', + 'Ɬ' => 'ɬ', + 'Ɪ' => 'ɪ', + 'Ʞ' => 'ʞ', + 'Ʇ' => 'ʇ', + 'Ʝ' => 'ʝ', + 'Ꭓ' => 'ꭓ', + 'Ꞵ' => 'ꞵ', + 'Ꞷ' => 'ꞷ', + 'Ꞹ' => 'ꞹ', + 'Ꞻ' => 'ꞻ', + 'Ꞽ' => 'ꞽ', + 'Ꞿ' => 'ꞿ', + 'Ꟃ' => 'ꟃ', + 'Ꞔ' => 'ꞔ', + 'Ʂ' => 'ʂ', + 'Ᶎ' => 'ᶎ', + 'Ꟈ' => 'ꟈ', + 'Ꟊ' => 'ꟊ', + 'Ꟶ' => 'ꟶ', + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + '𐐀' => '𐐨', + '𐐁' => '𐐩', + '𐐂' => '𐐪', + '𐐃' => '𐐫', + '𐐄' => '𐐬', + '𐐅' => '𐐭', + '𐐆' => '𐐮', + '𐐇' => '𐐯', + '𐐈' => '𐐰', + '𐐉' => '𐐱', + '𐐊' => '𐐲', + '𐐋' => '𐐳', + '𐐌' => '𐐴', + '𐐍' => '𐐵', + '𐐎' => '𐐶', + '𐐏' => '𐐷', + '𐐐' => '𐐸', + '𐐑' => '𐐹', + '𐐒' => '𐐺', + '𐐓' => '𐐻', + '𐐔' => '𐐼', + '𐐕' => '𐐽', + '𐐖' => '𐐾', + '𐐗' => '𐐿', + '𐐘' => '𐑀', + '𐐙' => '𐑁', + '𐐚' => '𐑂', + '𐐛' => '𐑃', + '𐐜' => '𐑄', + '𐐝' => '𐑅', + '𐐞' => '𐑆', + '𐐟' => '𐑇', + '𐐠' => '𐑈', + '𐐡' => '𐑉', + '𐐢' => '𐑊', + '𐐣' => '𐑋', + '𐐤' => '𐑌', + '𐐥' => '𐑍', + '𐐦' => '𐑎', + '𐐧' => '𐑏', + '𐒰' => '𐓘', + '𐒱' => '𐓙', + '𐒲' => '𐓚', + '𐒳' => '𐓛', + '𐒴' => '𐓜', + '𐒵' => '𐓝', + '𐒶' => '𐓞', + '𐒷' => '𐓟', + '𐒸' => '𐓠', + '𐒹' => '𐓡', + '𐒺' => '𐓢', + '𐒻' => '𐓣', + '𐒼' => '𐓤', + '𐒽' => '𐓥', + '𐒾' => '𐓦', + '𐒿' => '𐓧', + '𐓀' => '𐓨', + '𐓁' => '𐓩', + '𐓂' => '𐓪', + '𐓃' => '𐓫', + '𐓄' => '𐓬', + '𐓅' => '𐓭', + '𐓆' => '𐓮', + '𐓇' => '𐓯', + '𐓈' => '𐓰', + '𐓉' => '𐓱', + '𐓊' => '𐓲', + '𐓋' => '𐓳', + '𐓌' => '𐓴', + '𐓍' => '𐓵', + '𐓎' => '𐓶', + '𐓏' => '𐓷', + '𐓐' => '𐓸', + '𐓑' => '𐓹', + '𐓒' => '𐓺', + '𐓓' => '𐓻', + '𐲀' => '𐳀', + '𐲁' => '𐳁', + '𐲂' => '𐳂', + '𐲃' => '𐳃', + '𐲄' => '𐳄', + '𐲅' => '𐳅', + '𐲆' => '𐳆', + '𐲇' => '𐳇', + '𐲈' => '𐳈', + '𐲉' => '𐳉', + '𐲊' => '𐳊', + '𐲋' => '𐳋', + '𐲌' => '𐳌', + '𐲍' => '𐳍', + '𐲎' => '𐳎', + '𐲏' => '𐳏', + '𐲐' => '𐳐', + '𐲑' => '𐳑', + '𐲒' => '𐳒', + '𐲓' => '𐳓', + '𐲔' => '𐳔', + '𐲕' => '𐳕', + '𐲖' => '𐳖', + '𐲗' => '𐳗', + '𐲘' => '𐳘', + '𐲙' => '𐳙', + '𐲚' => '𐳚', + '𐲛' => '𐳛', + '𐲜' => '𐳜', + '𐲝' => '𐳝', + '𐲞' => '𐳞', + '𐲟' => '𐳟', + '𐲠' => '𐳠', + '𐲡' => '𐳡', + '𐲢' => '𐳢', + '𐲣' => '𐳣', + '𐲤' => '𐳤', + '𐲥' => '𐳥', + '𐲦' => '𐳦', + '𐲧' => '𐳧', + '𐲨' => '𐳨', + '𐲩' => '𐳩', + '𐲪' => '𐳪', + '𐲫' => '𐳫', + '𐲬' => '𐳬', + '𐲭' => '𐳭', + '𐲮' => '𐳮', + '𐲯' => '𐳯', + '𐲰' => '𐳰', + '𐲱' => '𐳱', + '𐲲' => '𐳲', + '𑢠' => '𑣀', + '𑢡' => '𑣁', + '𑢢' => '𑣂', + '𑢣' => '𑣃', + '𑢤' => '𑣄', + '𑢥' => '𑣅', + '𑢦' => '𑣆', + '𑢧' => '𑣇', + '𑢨' => '𑣈', + '𑢩' => '𑣉', + '𑢪' => '𑣊', + '𑢫' => '𑣋', + '𑢬' => '𑣌', + '𑢭' => '𑣍', + '𑢮' => '𑣎', + '𑢯' => '𑣏', + '𑢰' => '𑣐', + '𑢱' => '𑣑', + '𑢲' => '𑣒', + '𑢳' => '𑣓', + '𑢴' => '𑣔', + '𑢵' => '𑣕', + '𑢶' => '𑣖', + '𑢷' => '𑣗', + '𑢸' => '𑣘', + '𑢹' => '𑣙', + '𑢺' => '𑣚', + '𑢻' => '𑣛', + '𑢼' => '𑣜', + '𑢽' => '𑣝', + '𑢾' => '𑣞', + '𑢿' => '𑣟', + '𖹀' => '𖹠', + '𖹁' => '𖹡', + '𖹂' => '𖹢', + '𖹃' => '𖹣', + '𖹄' => '𖹤', + '𖹅' => '𖹥', + '𖹆' => '𖹦', + '𖹇' => '𖹧', + '𖹈' => '𖹨', + '𖹉' => '𖹩', + '𖹊' => '𖹪', + '𖹋' => '𖹫', + '𖹌' => '𖹬', + '𖹍' => '𖹭', + '𖹎' => '𖹮', + '𖹏' => '𖹯', + '𖹐' => '𖹰', + '𖹑' => '𖹱', + '𖹒' => '𖹲', + '𖹓' => '𖹳', + '𖹔' => '𖹴', + '𖹕' => '𖹵', + '𖹖' => '𖹶', + '𖹗' => '𖹷', + '𖹘' => '𖹸', + '𖹙' => '𖹹', + '𖹚' => '𖹺', + '𖹛' => '𖹻', + '𖹜' => '𖹼', + '𖹝' => '𖹽', + '𖹞' => '𖹾', + '𖹟' => '𖹿', + '𞤀' => '𞤢', + '𞤁' => '𞤣', + '𞤂' => '𞤤', + '𞤃' => '𞤥', + '𞤄' => '𞤦', + '𞤅' => '𞤧', + '𞤆' => '𞤨', + '𞤇' => '𞤩', + '𞤈' => '𞤪', + '𞤉' => '𞤫', + '𞤊' => '𞤬', + '𞤋' => '𞤭', + '𞤌' => '𞤮', + '𞤍' => '𞤯', + '𞤎' => '𞤰', + '𞤏' => '𞤱', + '𞤐' => '𞤲', + '𞤑' => '𞤳', + '𞤒' => '𞤴', + '𞤓' => '𞤵', + '𞤔' => '𞤶', + '𞤕' => '𞤷', + '𞤖' => '𞤸', + '𞤗' => '𞤹', + '𞤘' => '𞤺', + '𞤙' => '𞤻', + '𞤚' => '𞤼', + '𞤛' => '𞤽', + '𞤜' => '𞤾', + '𞤝' => '𞤿', + '𞤞' => '𞥀', + '𞤟' => '𞥁', + '𞤠' => '𞥂', + '𞤡' => '𞥃', +); diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php new file mode 100644 index 0000000..2a8f6e7 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php @@ -0,0 +1,5 @@ + 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + 'µ' => 'Μ', + 'à' => 'À', + 'á' => 'Á', + 'â' => 'Â', + 'ã' => 'Ã', + 'ä' => 'Ä', + 'å' => 'Å', + 'æ' => 'Æ', + 'ç' => 'Ç', + 'è' => 'È', + 'é' => 'É', + 'ê' => 'Ê', + 'ë' => 'Ë', + 'ì' => 'Ì', + 'í' => 'Í', + 'î' => 'Î', + 'ï' => 'Ï', + 'ð' => 'Ð', + 'ñ' => 'Ñ', + 'ò' => 'Ò', + 'ó' => 'Ó', + 'ô' => 'Ô', + 'õ' => 'Õ', + 'ö' => 'Ö', + 'ø' => 'Ø', + 'ù' => 'Ù', + 'ú' => 'Ú', + 'û' => 'Û', + 'ü' => 'Ü', + 'ý' => 'Ý', + 'þ' => 'Þ', + 'ÿ' => 'Ÿ', + 'ā' => 'Ā', + 'ă' => 'Ă', + 'ą' => 'Ą', + 'ć' => 'Ć', + 'ĉ' => 'Ĉ', + 'ċ' => 'Ċ', + 'č' => 'Č', + 'ď' => 'Ď', + 'đ' => 'Đ', + 'ē' => 'Ē', + 'ĕ' => 'Ĕ', + 'ė' => 'Ė', + 'ę' => 'Ę', + 'ě' => 'Ě', + 'ĝ' => 'Ĝ', + 'ğ' => 'Ğ', + 'ġ' => 'Ġ', + 'ģ' => 'Ģ', + 'ĥ' => 'Ĥ', + 'ħ' => 'Ħ', + 'ĩ' => 'Ĩ', + 'ī' => 'Ī', + 'ĭ' => 'Ĭ', + 'į' => 'Į', + 'ı' => 'I', + 'ij' => 'IJ', + 'ĵ' => 'Ĵ', + 'ķ' => 'Ķ', + 'ĺ' => 'Ĺ', + 'ļ' => 'Ļ', + 'ľ' => 'Ľ', + 'ŀ' => 'Ŀ', + 'ł' => 'Ł', + 'ń' => 'Ń', + 'ņ' => 'Ņ', + 'ň' => 'Ň', + 'ŋ' => 'Ŋ', + 'ō' => 'Ō', + 'ŏ' => 'Ŏ', + 'ő' => 'Ő', + 'œ' => 'Œ', + 'ŕ' => 'Ŕ', + 'ŗ' => 'Ŗ', + 'ř' => 'Ř', + 'ś' => 'Ś', + 'ŝ' => 'Ŝ', + 'ş' => 'Ş', + 'š' => 'Š', + 'ţ' => 'Ţ', + 'ť' => 'Ť', + 'ŧ' => 'Ŧ', + 'ũ' => 'Ũ', + 'ū' => 'Ū', + 'ŭ' => 'Ŭ', + 'ů' => 'Ů', + 'ű' => 'Ű', + 'ų' => 'Ų', + 'ŵ' => 'Ŵ', + 'ŷ' => 'Ŷ', + 'ź' => 'Ź', + 'ż' => 'Ż', + 'ž' => 'Ž', + 'ſ' => 'S', + 'ƀ' => 'Ƀ', + 'ƃ' => 'Ƃ', + 'ƅ' => 'Ƅ', + 'ƈ' => 'Ƈ', + 'ƌ' => 'Ƌ', + 'ƒ' => 'Ƒ', + 'ƕ' => 'Ƕ', + 'ƙ' => 'Ƙ', + 'ƚ' => 'Ƚ', + 'ƞ' => 'Ƞ', + 'ơ' => 'Ơ', + 'ƣ' => 'Ƣ', + 'ƥ' => 'Ƥ', + 'ƨ' => 'Ƨ', + 'ƭ' => 'Ƭ', + 'ư' => 'Ư', + 'ƴ' => 'Ƴ', + 'ƶ' => 'Ƶ', + 'ƹ' => 'Ƹ', + 'ƽ' => 'Ƽ', + 'ƿ' => 'Ƿ', + 'Dž' => 'DŽ', + 'dž' => 'DŽ', + 'Lj' => 'LJ', + 'lj' => 'LJ', + 'Nj' => 'NJ', + 'nj' => 'NJ', + 'ǎ' => 'Ǎ', + 'ǐ' => 'Ǐ', + 'ǒ' => 'Ǒ', + 'ǔ' => 'Ǔ', + 'ǖ' => 'Ǖ', + 'ǘ' => 'Ǘ', + 'ǚ' => 'Ǚ', + 'ǜ' => 'Ǜ', + 'ǝ' => 'Ǝ', + 'ǟ' => 'Ǟ', + 'ǡ' => 'Ǡ', + 'ǣ' => 'Ǣ', + 'ǥ' => 'Ǥ', + 'ǧ' => 'Ǧ', + 'ǩ' => 'Ǩ', + 'ǫ' => 'Ǫ', + 'ǭ' => 'Ǭ', + 'ǯ' => 'Ǯ', + 'Dz' => 'DZ', + 'dz' => 'DZ', + 'ǵ' => 'Ǵ', + 'ǹ' => 'Ǹ', + 'ǻ' => 'Ǻ', + 'ǽ' => 'Ǽ', + 'ǿ' => 'Ǿ', + 'ȁ' => 'Ȁ', + 'ȃ' => 'Ȃ', + 'ȅ' => 'Ȅ', + 'ȇ' => 'Ȇ', + 'ȉ' => 'Ȉ', + 'ȋ' => 'Ȋ', + 'ȍ' => 'Ȍ', + 'ȏ' => 'Ȏ', + 'ȑ' => 'Ȑ', + 'ȓ' => 'Ȓ', + 'ȕ' => 'Ȕ', + 'ȗ' => 'Ȗ', + 'ș' => 'Ș', + 'ț' => 'Ț', + 'ȝ' => 'Ȝ', + 'ȟ' => 'Ȟ', + 'ȣ' => 'Ȣ', + 'ȥ' => 'Ȥ', + 'ȧ' => 'Ȧ', + 'ȩ' => 'Ȩ', + 'ȫ' => 'Ȫ', + 'ȭ' => 'Ȭ', + 'ȯ' => 'Ȯ', + 'ȱ' => 'Ȱ', + 'ȳ' => 'Ȳ', + 'ȼ' => 'Ȼ', + 'ȿ' => 'Ȿ', + 'ɀ' => 'Ɀ', + 'ɂ' => 'Ɂ', + 'ɇ' => 'Ɇ', + 'ɉ' => 'Ɉ', + 'ɋ' => 'Ɋ', + 'ɍ' => 'Ɍ', + 'ɏ' => 'Ɏ', + 'ɐ' => 'Ɐ', + 'ɑ' => 'Ɑ', + 'ɒ' => 'Ɒ', + 'ɓ' => 'Ɓ', + 'ɔ' => 'Ɔ', + 'ɖ' => 'Ɖ', + 'ɗ' => 'Ɗ', + 'ə' => 'Ə', + 'ɛ' => 'Ɛ', + 'ɜ' => 'Ɜ', + 'ɠ' => 'Ɠ', + 'ɡ' => 'Ɡ', + 'ɣ' => 'Ɣ', + 'ɥ' => 'Ɥ', + 'ɦ' => 'Ɦ', + 'ɨ' => 'Ɨ', + 'ɩ' => 'Ɩ', + 'ɪ' => 'Ɪ', + 'ɫ' => 'Ɫ', + 'ɬ' => 'Ɬ', + 'ɯ' => 'Ɯ', + 'ɱ' => 'Ɱ', + 'ɲ' => 'Ɲ', + 'ɵ' => 'Ɵ', + 'ɽ' => 'Ɽ', + 'ʀ' => 'Ʀ', + 'ʂ' => 'Ʂ', + 'ʃ' => 'Ʃ', + 'ʇ' => 'Ʇ', + 'ʈ' => 'Ʈ', + 'ʉ' => 'Ʉ', + 'ʊ' => 'Ʊ', + 'ʋ' => 'Ʋ', + 'ʌ' => 'Ʌ', + 'ʒ' => 'Ʒ', + 'ʝ' => 'Ʝ', + 'ʞ' => 'Ʞ', + 'ͅ' => 'Ι', + 'ͱ' => 'Ͱ', + 'ͳ' => 'Ͳ', + 'ͷ' => 'Ͷ', + 'ͻ' => 'Ͻ', + 'ͼ' => 'Ͼ', + 'ͽ' => 'Ͽ', + 'ά' => 'Ά', + 'έ' => 'Έ', + 'ή' => 'Ή', + 'ί' => 'Ί', + 'α' => 'Α', + 'β' => 'Β', + 'γ' => 'Γ', + 'δ' => 'Δ', + 'ε' => 'Ε', + 'ζ' => 'Ζ', + 'η' => 'Η', + 'θ' => 'Θ', + 'ι' => 'Ι', + 'κ' => 'Κ', + 'λ' => 'Λ', + 'μ' => 'Μ', + 'ν' => 'Ν', + 'ξ' => 'Ξ', + 'ο' => 'Ο', + 'π' => 'Π', + 'ρ' => 'Ρ', + 'ς' => 'Σ', + 'σ' => 'Σ', + 'τ' => 'Τ', + 'υ' => 'Υ', + 'φ' => 'Φ', + 'χ' => 'Χ', + 'ψ' => 'Ψ', + 'ω' => 'Ω', + 'ϊ' => 'Ϊ', + 'ϋ' => 'Ϋ', + 'ό' => 'Ό', + 'ύ' => 'Ύ', + 'ώ' => 'Ώ', + 'ϐ' => 'Β', + 'ϑ' => 'Θ', + 'ϕ' => 'Φ', + 'ϖ' => 'Π', + 'ϗ' => 'Ϗ', + 'ϙ' => 'Ϙ', + 'ϛ' => 'Ϛ', + 'ϝ' => 'Ϝ', + 'ϟ' => 'Ϟ', + 'ϡ' => 'Ϡ', + 'ϣ' => 'Ϣ', + 'ϥ' => 'Ϥ', + 'ϧ' => 'Ϧ', + 'ϩ' => 'Ϩ', + 'ϫ' => 'Ϫ', + 'ϭ' => 'Ϭ', + 'ϯ' => 'Ϯ', + 'ϰ' => 'Κ', + 'ϱ' => 'Ρ', + 'ϲ' => 'Ϲ', + 'ϳ' => 'Ϳ', + 'ϵ' => 'Ε', + 'ϸ' => 'Ϸ', + 'ϻ' => 'Ϻ', + 'а' => 'А', + 'б' => 'Б', + 'в' => 'В', + 'г' => 'Г', + 'д' => 'Д', + 'е' => 'Е', + 'ж' => 'Ж', + 'з' => 'З', + 'и' => 'И', + 'й' => 'Й', + 'к' => 'К', + 'л' => 'Л', + 'м' => 'М', + 'н' => 'Н', + 'о' => 'О', + 'п' => 'П', + 'р' => 'Р', + 'с' => 'С', + 'т' => 'Т', + 'у' => 'У', + 'ф' => 'Ф', + 'х' => 'Х', + 'ц' => 'Ц', + 'ч' => 'Ч', + 'ш' => 'Ш', + 'щ' => 'Щ', + 'ъ' => 'Ъ', + 'ы' => 'Ы', + 'ь' => 'Ь', + 'э' => 'Э', + 'ю' => 'Ю', + 'я' => 'Я', + 'ѐ' => 'Ѐ', + 'ё' => 'Ё', + 'ђ' => 'Ђ', + 'ѓ' => 'Ѓ', + 'є' => 'Є', + 'ѕ' => 'Ѕ', + 'і' => 'І', + 'ї' => 'Ї', + 'ј' => 'Ј', + 'љ' => 'Љ', + 'њ' => 'Њ', + 'ћ' => 'Ћ', + 'ќ' => 'Ќ', + 'ѝ' => 'Ѝ', + 'ў' => 'Ў', + 'џ' => 'Џ', + 'ѡ' => 'Ѡ', + 'ѣ' => 'Ѣ', + 'ѥ' => 'Ѥ', + 'ѧ' => 'Ѧ', + 'ѩ' => 'Ѩ', + 'ѫ' => 'Ѫ', + 'ѭ' => 'Ѭ', + 'ѯ' => 'Ѯ', + 'ѱ' => 'Ѱ', + 'ѳ' => 'Ѳ', + 'ѵ' => 'Ѵ', + 'ѷ' => 'Ѷ', + 'ѹ' => 'Ѹ', + 'ѻ' => 'Ѻ', + 'ѽ' => 'Ѽ', + 'ѿ' => 'Ѿ', + 'ҁ' => 'Ҁ', + 'ҋ' => 'Ҋ', + 'ҍ' => 'Ҍ', + 'ҏ' => 'Ҏ', + 'ґ' => 'Ґ', + 'ғ' => 'Ғ', + 'ҕ' => 'Ҕ', + 'җ' => 'Җ', + 'ҙ' => 'Ҙ', + 'қ' => 'Қ', + 'ҝ' => 'Ҝ', + 'ҟ' => 'Ҟ', + 'ҡ' => 'Ҡ', + 'ң' => 'Ң', + 'ҥ' => 'Ҥ', + 'ҧ' => 'Ҧ', + 'ҩ' => 'Ҩ', + 'ҫ' => 'Ҫ', + 'ҭ' => 'Ҭ', + 'ү' => 'Ү', + 'ұ' => 'Ұ', + 'ҳ' => 'Ҳ', + 'ҵ' => 'Ҵ', + 'ҷ' => 'Ҷ', + 'ҹ' => 'Ҹ', + 'һ' => 'Һ', + 'ҽ' => 'Ҽ', + 'ҿ' => 'Ҿ', + 'ӂ' => 'Ӂ', + 'ӄ' => 'Ӄ', + 'ӆ' => 'Ӆ', + 'ӈ' => 'Ӈ', + 'ӊ' => 'Ӊ', + 'ӌ' => 'Ӌ', + 'ӎ' => 'Ӎ', + 'ӏ' => 'Ӏ', + 'ӑ' => 'Ӑ', + 'ӓ' => 'Ӓ', + 'ӕ' => 'Ӕ', + 'ӗ' => 'Ӗ', + 'ә' => 'Ә', + 'ӛ' => 'Ӛ', + 'ӝ' => 'Ӝ', + 'ӟ' => 'Ӟ', + 'ӡ' => 'Ӡ', + 'ӣ' => 'Ӣ', + 'ӥ' => 'Ӥ', + 'ӧ' => 'Ӧ', + 'ө' => 'Ө', + 'ӫ' => 'Ӫ', + 'ӭ' => 'Ӭ', + 'ӯ' => 'Ӯ', + 'ӱ' => 'Ӱ', + 'ӳ' => 'Ӳ', + 'ӵ' => 'Ӵ', + 'ӷ' => 'Ӷ', + 'ӹ' => 'Ӹ', + 'ӻ' => 'Ӻ', + 'ӽ' => 'Ӽ', + 'ӿ' => 'Ӿ', + 'ԁ' => 'Ԁ', + 'ԃ' => 'Ԃ', + 'ԅ' => 'Ԅ', + 'ԇ' => 'Ԇ', + 'ԉ' => 'Ԉ', + 'ԋ' => 'Ԋ', + 'ԍ' => 'Ԍ', + 'ԏ' => 'Ԏ', + 'ԑ' => 'Ԑ', + 'ԓ' => 'Ԓ', + 'ԕ' => 'Ԕ', + 'ԗ' => 'Ԗ', + 'ԙ' => 'Ԙ', + 'ԛ' => 'Ԛ', + 'ԝ' => 'Ԝ', + 'ԟ' => 'Ԟ', + 'ԡ' => 'Ԡ', + 'ԣ' => 'Ԣ', + 'ԥ' => 'Ԥ', + 'ԧ' => 'Ԧ', + 'ԩ' => 'Ԩ', + 'ԫ' => 'Ԫ', + 'ԭ' => 'Ԭ', + 'ԯ' => 'Ԯ', + 'ա' => 'Ա', + 'բ' => 'Բ', + 'գ' => 'Գ', + 'դ' => 'Դ', + 'ե' => 'Ե', + 'զ' => 'Զ', + 'է' => 'Է', + 'ը' => 'Ը', + 'թ' => 'Թ', + 'ժ' => 'Ժ', + 'ի' => 'Ի', + 'լ' => 'Լ', + 'խ' => 'Խ', + 'ծ' => 'Ծ', + 'կ' => 'Կ', + 'հ' => 'Հ', + 'ձ' => 'Ձ', + 'ղ' => 'Ղ', + 'ճ' => 'Ճ', + 'մ' => 'Մ', + 'յ' => 'Յ', + 'ն' => 'Ն', + 'շ' => 'Շ', + 'ո' => 'Ո', + 'չ' => 'Չ', + 'պ' => 'Պ', + 'ջ' => 'Ջ', + 'ռ' => 'Ռ', + 'ս' => 'Ս', + 'վ' => 'Վ', + 'տ' => 'Տ', + 'ր' => 'Ր', + 'ց' => 'Ց', + 'ւ' => 'Ւ', + 'փ' => 'Փ', + 'ք' => 'Ք', + 'օ' => 'Օ', + 'ֆ' => 'Ֆ', + 'ა' => 'Ა', + 'ბ' => 'Ბ', + 'გ' => 'Გ', + 'დ' => 'Დ', + 'ე' => 'Ე', + 'ვ' => 'Ვ', + 'ზ' => 'Ზ', + 'თ' => 'Თ', + 'ი' => 'Ი', + 'კ' => 'Კ', + 'ლ' => 'Ლ', + 'მ' => 'Მ', + 'ნ' => 'Ნ', + 'ო' => 'Ო', + 'პ' => 'Პ', + 'ჟ' => 'Ჟ', + 'რ' => 'Რ', + 'ს' => 'Ს', + 'ტ' => 'Ტ', + 'უ' => 'Უ', + 'ფ' => 'Ფ', + 'ქ' => 'Ქ', + 'ღ' => 'Ღ', + 'ყ' => 'Ყ', + 'შ' => 'Შ', + 'ჩ' => 'Ჩ', + 'ც' => 'Ც', + 'ძ' => 'Ძ', + 'წ' => 'Წ', + 'ჭ' => 'Ჭ', + 'ხ' => 'Ხ', + 'ჯ' => 'Ჯ', + 'ჰ' => 'Ჰ', + 'ჱ' => 'Ჱ', + 'ჲ' => 'Ჲ', + 'ჳ' => 'Ჳ', + 'ჴ' => 'Ჴ', + 'ჵ' => 'Ჵ', + 'ჶ' => 'Ჶ', + 'ჷ' => 'Ჷ', + 'ჸ' => 'Ჸ', + 'ჹ' => 'Ჹ', + 'ჺ' => 'Ჺ', + 'ჽ' => 'Ჽ', + 'ჾ' => 'Ჾ', + 'ჿ' => 'Ჿ', + 'ᏸ' => 'Ᏸ', + 'ᏹ' => 'Ᏹ', + 'ᏺ' => 'Ᏺ', + 'ᏻ' => 'Ᏻ', + 'ᏼ' => 'Ᏼ', + 'ᏽ' => 'Ᏽ', + 'ᲀ' => 'В', + 'ᲁ' => 'Д', + 'ᲂ' => 'О', + 'ᲃ' => 'С', + 'ᲄ' => 'Т', + 'ᲅ' => 'Т', + 'ᲆ' => 'Ъ', + 'ᲇ' => 'Ѣ', + 'ᲈ' => 'Ꙋ', + 'ᵹ' => 'Ᵹ', + 'ᵽ' => 'Ᵽ', + 'ᶎ' => 'Ᶎ', + 'ḁ' => 'Ḁ', + 'ḃ' => 'Ḃ', + 'ḅ' => 'Ḅ', + 'ḇ' => 'Ḇ', + 'ḉ' => 'Ḉ', + 'ḋ' => 'Ḋ', + 'ḍ' => 'Ḍ', + 'ḏ' => 'Ḏ', + 'ḑ' => 'Ḑ', + 'ḓ' => 'Ḓ', + 'ḕ' => 'Ḕ', + 'ḗ' => 'Ḗ', + 'ḙ' => 'Ḙ', + 'ḛ' => 'Ḛ', + 'ḝ' => 'Ḝ', + 'ḟ' => 'Ḟ', + 'ḡ' => 'Ḡ', + 'ḣ' => 'Ḣ', + 'ḥ' => 'Ḥ', + 'ḧ' => 'Ḧ', + 'ḩ' => 'Ḩ', + 'ḫ' => 'Ḫ', + 'ḭ' => 'Ḭ', + 'ḯ' => 'Ḯ', + 'ḱ' => 'Ḱ', + 'ḳ' => 'Ḳ', + 'ḵ' => 'Ḵ', + 'ḷ' => 'Ḷ', + 'ḹ' => 'Ḹ', + 'ḻ' => 'Ḻ', + 'ḽ' => 'Ḽ', + 'ḿ' => 'Ḿ', + 'ṁ' => 'Ṁ', + 'ṃ' => 'Ṃ', + 'ṅ' => 'Ṅ', + 'ṇ' => 'Ṇ', + 'ṉ' => 'Ṉ', + 'ṋ' => 'Ṋ', + 'ṍ' => 'Ṍ', + 'ṏ' => 'Ṏ', + 'ṑ' => 'Ṑ', + 'ṓ' => 'Ṓ', + 'ṕ' => 'Ṕ', + 'ṗ' => 'Ṗ', + 'ṙ' => 'Ṙ', + 'ṛ' => 'Ṛ', + 'ṝ' => 'Ṝ', + 'ṟ' => 'Ṟ', + 'ṡ' => 'Ṡ', + 'ṣ' => 'Ṣ', + 'ṥ' => 'Ṥ', + 'ṧ' => 'Ṧ', + 'ṩ' => 'Ṩ', + 'ṫ' => 'Ṫ', + 'ṭ' => 'Ṭ', + 'ṯ' => 'Ṯ', + 'ṱ' => 'Ṱ', + 'ṳ' => 'Ṳ', + 'ṵ' => 'Ṵ', + 'ṷ' => 'Ṷ', + 'ṹ' => 'Ṹ', + 'ṻ' => 'Ṻ', + 'ṽ' => 'Ṽ', + 'ṿ' => 'Ṿ', + 'ẁ' => 'Ẁ', + 'ẃ' => 'Ẃ', + 'ẅ' => 'Ẅ', + 'ẇ' => 'Ẇ', + 'ẉ' => 'Ẉ', + 'ẋ' => 'Ẋ', + 'ẍ' => 'Ẍ', + 'ẏ' => 'Ẏ', + 'ẑ' => 'Ẑ', + 'ẓ' => 'Ẓ', + 'ẕ' => 'Ẕ', + 'ẛ' => 'Ṡ', + 'ạ' => 'Ạ', + 'ả' => 'Ả', + 'ấ' => 'Ấ', + 'ầ' => 'Ầ', + 'ẩ' => 'Ẩ', + 'ẫ' => 'Ẫ', + 'ậ' => 'Ậ', + 'ắ' => 'Ắ', + 'ằ' => 'Ằ', + 'ẳ' => 'Ẳ', + 'ẵ' => 'Ẵ', + 'ặ' => 'Ặ', + 'ẹ' => 'Ẹ', + 'ẻ' => 'Ẻ', + 'ẽ' => 'Ẽ', + 'ế' => 'Ế', + 'ề' => 'Ề', + 'ể' => 'Ể', + 'ễ' => 'Ễ', + 'ệ' => 'Ệ', + 'ỉ' => 'Ỉ', + 'ị' => 'Ị', + 'ọ' => 'Ọ', + 'ỏ' => 'Ỏ', + 'ố' => 'Ố', + 'ồ' => 'Ồ', + 'ổ' => 'Ổ', + 'ỗ' => 'Ỗ', + 'ộ' => 'Ộ', + 'ớ' => 'Ớ', + 'ờ' => 'Ờ', + 'ở' => 'Ở', + 'ỡ' => 'Ỡ', + 'ợ' => 'Ợ', + 'ụ' => 'Ụ', + 'ủ' => 'Ủ', + 'ứ' => 'Ứ', + 'ừ' => 'Ừ', + 'ử' => 'Ử', + 'ữ' => 'Ữ', + 'ự' => 'Ự', + 'ỳ' => 'Ỳ', + 'ỵ' => 'Ỵ', + 'ỷ' => 'Ỷ', + 'ỹ' => 'Ỹ', + 'ỻ' => 'Ỻ', + 'ỽ' => 'Ỽ', + 'ỿ' => 'Ỿ', + 'ἀ' => 'Ἀ', + 'ἁ' => 'Ἁ', + 'ἂ' => 'Ἂ', + 'ἃ' => 'Ἃ', + 'ἄ' => 'Ἄ', + 'ἅ' => 'Ἅ', + 'ἆ' => 'Ἆ', + 'ἇ' => 'Ἇ', + 'ἐ' => 'Ἐ', + 'ἑ' => 'Ἑ', + 'ἒ' => 'Ἒ', + 'ἓ' => 'Ἓ', + 'ἔ' => 'Ἔ', + 'ἕ' => 'Ἕ', + 'ἠ' => 'Ἠ', + 'ἡ' => 'Ἡ', + 'ἢ' => 'Ἢ', + 'ἣ' => 'Ἣ', + 'ἤ' => 'Ἤ', + 'ἥ' => 'Ἥ', + 'ἦ' => 'Ἦ', + 'ἧ' => 'Ἧ', + 'ἰ' => 'Ἰ', + 'ἱ' => 'Ἱ', + 'ἲ' => 'Ἲ', + 'ἳ' => 'Ἳ', + 'ἴ' => 'Ἴ', + 'ἵ' => 'Ἵ', + 'ἶ' => 'Ἶ', + 'ἷ' => 'Ἷ', + 'ὀ' => 'Ὀ', + 'ὁ' => 'Ὁ', + 'ὂ' => 'Ὂ', + 'ὃ' => 'Ὃ', + 'ὄ' => 'Ὄ', + 'ὅ' => 'Ὅ', + 'ὑ' => 'Ὑ', + 'ὓ' => 'Ὓ', + 'ὕ' => 'Ὕ', + 'ὗ' => 'Ὗ', + 'ὠ' => 'Ὠ', + 'ὡ' => 'Ὡ', + 'ὢ' => 'Ὢ', + 'ὣ' => 'Ὣ', + 'ὤ' => 'Ὤ', + 'ὥ' => 'Ὥ', + 'ὦ' => 'Ὦ', + 'ὧ' => 'Ὧ', + 'ὰ' => 'Ὰ', + 'ά' => 'Ά', + 'ὲ' => 'Ὲ', + 'έ' => 'Έ', + 'ὴ' => 'Ὴ', + 'ή' => 'Ή', + 'ὶ' => 'Ὶ', + 'ί' => 'Ί', + 'ὸ' => 'Ὸ', + 'ό' => 'Ό', + 'ὺ' => 'Ὺ', + 'ύ' => 'Ύ', + 'ὼ' => 'Ὼ', + 'ώ' => 'Ώ', + 'ᾀ' => 'ἈΙ', + 'ᾁ' => 'ἉΙ', + 'ᾂ' => 'ἊΙ', + 'ᾃ' => 'ἋΙ', + 'ᾄ' => 'ἌΙ', + 'ᾅ' => 'ἍΙ', + 'ᾆ' => 'ἎΙ', + 'ᾇ' => 'ἏΙ', + 'ᾐ' => 'ἨΙ', + 'ᾑ' => 'ἩΙ', + 'ᾒ' => 'ἪΙ', + 'ᾓ' => 'ἫΙ', + 'ᾔ' => 'ἬΙ', + 'ᾕ' => 'ἭΙ', + 'ᾖ' => 'ἮΙ', + 'ᾗ' => 'ἯΙ', + 'ᾠ' => 'ὨΙ', + 'ᾡ' => 'ὩΙ', + 'ᾢ' => 'ὪΙ', + 'ᾣ' => 'ὫΙ', + 'ᾤ' => 'ὬΙ', + 'ᾥ' => 'ὭΙ', + 'ᾦ' => 'ὮΙ', + 'ᾧ' => 'ὯΙ', + 'ᾰ' => 'Ᾰ', + 'ᾱ' => 'Ᾱ', + 'ᾳ' => 'ΑΙ', + 'ι' => 'Ι', + 'ῃ' => 'ΗΙ', + 'ῐ' => 'Ῐ', + 'ῑ' => 'Ῑ', + 'ῠ' => 'Ῠ', + 'ῡ' => 'Ῡ', + 'ῥ' => 'Ῥ', + 'ῳ' => 'ΩΙ', + 'ⅎ' => 'Ⅎ', + 'ⅰ' => 'Ⅰ', + 'ⅱ' => 'Ⅱ', + 'ⅲ' => 'Ⅲ', + 'ⅳ' => 'Ⅳ', + 'ⅴ' => 'Ⅴ', + 'ⅵ' => 'Ⅵ', + 'ⅶ' => 'Ⅶ', + 'ⅷ' => 'Ⅷ', + 'ⅸ' => 'Ⅸ', + 'ⅹ' => 'Ⅹ', + 'ⅺ' => 'Ⅺ', + 'ⅻ' => 'Ⅻ', + 'ⅼ' => 'Ⅼ', + 'ⅽ' => 'Ⅽ', + 'ⅾ' => 'Ⅾ', + 'ⅿ' => 'Ⅿ', + 'ↄ' => 'Ↄ', + 'ⓐ' => 'Ⓐ', + 'ⓑ' => 'Ⓑ', + 'ⓒ' => 'Ⓒ', + 'ⓓ' => 'Ⓓ', + 'ⓔ' => 'Ⓔ', + 'ⓕ' => 'Ⓕ', + 'ⓖ' => 'Ⓖ', + 'ⓗ' => 'Ⓗ', + 'ⓘ' => 'Ⓘ', + 'ⓙ' => 'Ⓙ', + 'ⓚ' => 'Ⓚ', + 'ⓛ' => 'Ⓛ', + 'ⓜ' => 'Ⓜ', + 'ⓝ' => 'Ⓝ', + 'ⓞ' => 'Ⓞ', + 'ⓟ' => 'Ⓟ', + 'ⓠ' => 'Ⓠ', + 'ⓡ' => 'Ⓡ', + 'ⓢ' => 'Ⓢ', + 'ⓣ' => 'Ⓣ', + 'ⓤ' => 'Ⓤ', + 'ⓥ' => 'Ⓥ', + 'ⓦ' => 'Ⓦ', + 'ⓧ' => 'Ⓧ', + 'ⓨ' => 'Ⓨ', + 'ⓩ' => 'Ⓩ', + 'ⰰ' => 'Ⰰ', + 'ⰱ' => 'Ⰱ', + 'ⰲ' => 'Ⰲ', + 'ⰳ' => 'Ⰳ', + 'ⰴ' => 'Ⰴ', + 'ⰵ' => 'Ⰵ', + 'ⰶ' => 'Ⰶ', + 'ⰷ' => 'Ⰷ', + 'ⰸ' => 'Ⰸ', + 'ⰹ' => 'Ⰹ', + 'ⰺ' => 'Ⰺ', + 'ⰻ' => 'Ⰻ', + 'ⰼ' => 'Ⰼ', + 'ⰽ' => 'Ⰽ', + 'ⰾ' => 'Ⰾ', + 'ⰿ' => 'Ⰿ', + 'ⱀ' => 'Ⱀ', + 'ⱁ' => 'Ⱁ', + 'ⱂ' => 'Ⱂ', + 'ⱃ' => 'Ⱃ', + 'ⱄ' => 'Ⱄ', + 'ⱅ' => 'Ⱅ', + 'ⱆ' => 'Ⱆ', + 'ⱇ' => 'Ⱇ', + 'ⱈ' => 'Ⱈ', + 'ⱉ' => 'Ⱉ', + 'ⱊ' => 'Ⱊ', + 'ⱋ' => 'Ⱋ', + 'ⱌ' => 'Ⱌ', + 'ⱍ' => 'Ⱍ', + 'ⱎ' => 'Ⱎ', + 'ⱏ' => 'Ⱏ', + 'ⱐ' => 'Ⱐ', + 'ⱑ' => 'Ⱑ', + 'ⱒ' => 'Ⱒ', + 'ⱓ' => 'Ⱓ', + 'ⱔ' => 'Ⱔ', + 'ⱕ' => 'Ⱕ', + 'ⱖ' => 'Ⱖ', + 'ⱗ' => 'Ⱗ', + 'ⱘ' => 'Ⱘ', + 'ⱙ' => 'Ⱙ', + 'ⱚ' => 'Ⱚ', + 'ⱛ' => 'Ⱛ', + 'ⱜ' => 'Ⱜ', + 'ⱝ' => 'Ⱝ', + 'ⱞ' => 'Ⱞ', + 'ⱡ' => 'Ⱡ', + 'ⱥ' => 'Ⱥ', + 'ⱦ' => 'Ⱦ', + 'ⱨ' => 'Ⱨ', + 'ⱪ' => 'Ⱪ', + 'ⱬ' => 'Ⱬ', + 'ⱳ' => 'Ⱳ', + 'ⱶ' => 'Ⱶ', + 'ⲁ' => 'Ⲁ', + 'ⲃ' => 'Ⲃ', + 'ⲅ' => 'Ⲅ', + 'ⲇ' => 'Ⲇ', + 'ⲉ' => 'Ⲉ', + 'ⲋ' => 'Ⲋ', + 'ⲍ' => 'Ⲍ', + 'ⲏ' => 'Ⲏ', + 'ⲑ' => 'Ⲑ', + 'ⲓ' => 'Ⲓ', + 'ⲕ' => 'Ⲕ', + 'ⲗ' => 'Ⲗ', + 'ⲙ' => 'Ⲙ', + 'ⲛ' => 'Ⲛ', + 'ⲝ' => 'Ⲝ', + 'ⲟ' => 'Ⲟ', + 'ⲡ' => 'Ⲡ', + 'ⲣ' => 'Ⲣ', + 'ⲥ' => 'Ⲥ', + 'ⲧ' => 'Ⲧ', + 'ⲩ' => 'Ⲩ', + 'ⲫ' => 'Ⲫ', + 'ⲭ' => 'Ⲭ', + 'ⲯ' => 'Ⲯ', + 'ⲱ' => 'Ⲱ', + 'ⲳ' => 'Ⲳ', + 'ⲵ' => 'Ⲵ', + 'ⲷ' => 'Ⲷ', + 'ⲹ' => 'Ⲹ', + 'ⲻ' => 'Ⲻ', + 'ⲽ' => 'Ⲽ', + 'ⲿ' => 'Ⲿ', + 'ⳁ' => 'Ⳁ', + 'ⳃ' => 'Ⳃ', + 'ⳅ' => 'Ⳅ', + 'ⳇ' => 'Ⳇ', + 'ⳉ' => 'Ⳉ', + 'ⳋ' => 'Ⳋ', + 'ⳍ' => 'Ⳍ', + 'ⳏ' => 'Ⳏ', + 'ⳑ' => 'Ⳑ', + 'ⳓ' => 'Ⳓ', + 'ⳕ' => 'Ⳕ', + 'ⳗ' => 'Ⳗ', + 'ⳙ' => 'Ⳙ', + 'ⳛ' => 'Ⳛ', + 'ⳝ' => 'Ⳝ', + 'ⳟ' => 'Ⳟ', + 'ⳡ' => 'Ⳡ', + 'ⳣ' => 'Ⳣ', + 'ⳬ' => 'Ⳬ', + 'ⳮ' => 'Ⳮ', + 'ⳳ' => 'Ⳳ', + 'ⴀ' => 'Ⴀ', + 'ⴁ' => 'Ⴁ', + 'ⴂ' => 'Ⴂ', + 'ⴃ' => 'Ⴃ', + 'ⴄ' => 'Ⴄ', + 'ⴅ' => 'Ⴅ', + 'ⴆ' => 'Ⴆ', + 'ⴇ' => 'Ⴇ', + 'ⴈ' => 'Ⴈ', + 'ⴉ' => 'Ⴉ', + 'ⴊ' => 'Ⴊ', + 'ⴋ' => 'Ⴋ', + 'ⴌ' => 'Ⴌ', + 'ⴍ' => 'Ⴍ', + 'ⴎ' => 'Ⴎ', + 'ⴏ' => 'Ⴏ', + 'ⴐ' => 'Ⴐ', + 'ⴑ' => 'Ⴑ', + 'ⴒ' => 'Ⴒ', + 'ⴓ' => 'Ⴓ', + 'ⴔ' => 'Ⴔ', + 'ⴕ' => 'Ⴕ', + 'ⴖ' => 'Ⴖ', + 'ⴗ' => 'Ⴗ', + 'ⴘ' => 'Ⴘ', + 'ⴙ' => 'Ⴙ', + 'ⴚ' => 'Ⴚ', + 'ⴛ' => 'Ⴛ', + 'ⴜ' => 'Ⴜ', + 'ⴝ' => 'Ⴝ', + 'ⴞ' => 'Ⴞ', + 'ⴟ' => 'Ⴟ', + 'ⴠ' => 'Ⴠ', + 'ⴡ' => 'Ⴡ', + 'ⴢ' => 'Ⴢ', + 'ⴣ' => 'Ⴣ', + 'ⴤ' => 'Ⴤ', + 'ⴥ' => 'Ⴥ', + 'ⴧ' => 'Ⴧ', + 'ⴭ' => 'Ⴭ', + 'ꙁ' => 'Ꙁ', + 'ꙃ' => 'Ꙃ', + 'ꙅ' => 'Ꙅ', + 'ꙇ' => 'Ꙇ', + 'ꙉ' => 'Ꙉ', + 'ꙋ' => 'Ꙋ', + 'ꙍ' => 'Ꙍ', + 'ꙏ' => 'Ꙏ', + 'ꙑ' => 'Ꙑ', + 'ꙓ' => 'Ꙓ', + 'ꙕ' => 'Ꙕ', + 'ꙗ' => 'Ꙗ', + 'ꙙ' => 'Ꙙ', + 'ꙛ' => 'Ꙛ', + 'ꙝ' => 'Ꙝ', + 'ꙟ' => 'Ꙟ', + 'ꙡ' => 'Ꙡ', + 'ꙣ' => 'Ꙣ', + 'ꙥ' => 'Ꙥ', + 'ꙧ' => 'Ꙧ', + 'ꙩ' => 'Ꙩ', + 'ꙫ' => 'Ꙫ', + 'ꙭ' => 'Ꙭ', + 'ꚁ' => 'Ꚁ', + 'ꚃ' => 'Ꚃ', + 'ꚅ' => 'Ꚅ', + 'ꚇ' => 'Ꚇ', + 'ꚉ' => 'Ꚉ', + 'ꚋ' => 'Ꚋ', + 'ꚍ' => 'Ꚍ', + 'ꚏ' => 'Ꚏ', + 'ꚑ' => 'Ꚑ', + 'ꚓ' => 'Ꚓ', + 'ꚕ' => 'Ꚕ', + 'ꚗ' => 'Ꚗ', + 'ꚙ' => 'Ꚙ', + 'ꚛ' => 'Ꚛ', + 'ꜣ' => 'Ꜣ', + 'ꜥ' => 'Ꜥ', + 'ꜧ' => 'Ꜧ', + 'ꜩ' => 'Ꜩ', + 'ꜫ' => 'Ꜫ', + 'ꜭ' => 'Ꜭ', + 'ꜯ' => 'Ꜯ', + 'ꜳ' => 'Ꜳ', + 'ꜵ' => 'Ꜵ', + 'ꜷ' => 'Ꜷ', + 'ꜹ' => 'Ꜹ', + 'ꜻ' => 'Ꜻ', + 'ꜽ' => 'Ꜽ', + 'ꜿ' => 'Ꜿ', + 'ꝁ' => 'Ꝁ', + 'ꝃ' => 'Ꝃ', + 'ꝅ' => 'Ꝅ', + 'ꝇ' => 'Ꝇ', + 'ꝉ' => 'Ꝉ', + 'ꝋ' => 'Ꝋ', + 'ꝍ' => 'Ꝍ', + 'ꝏ' => 'Ꝏ', + 'ꝑ' => 'Ꝑ', + 'ꝓ' => 'Ꝓ', + 'ꝕ' => 'Ꝕ', + 'ꝗ' => 'Ꝗ', + 'ꝙ' => 'Ꝙ', + 'ꝛ' => 'Ꝛ', + 'ꝝ' => 'Ꝝ', + 'ꝟ' => 'Ꝟ', + 'ꝡ' => 'Ꝡ', + 'ꝣ' => 'Ꝣ', + 'ꝥ' => 'Ꝥ', + 'ꝧ' => 'Ꝧ', + 'ꝩ' => 'Ꝩ', + 'ꝫ' => 'Ꝫ', + 'ꝭ' => 'Ꝭ', + 'ꝯ' => 'Ꝯ', + 'ꝺ' => 'Ꝺ', + 'ꝼ' => 'Ꝼ', + 'ꝿ' => 'Ꝿ', + 'ꞁ' => 'Ꞁ', + 'ꞃ' => 'Ꞃ', + 'ꞅ' => 'Ꞅ', + 'ꞇ' => 'Ꞇ', + 'ꞌ' => 'Ꞌ', + 'ꞑ' => 'Ꞑ', + 'ꞓ' => 'Ꞓ', + 'ꞔ' => 'Ꞔ', + 'ꞗ' => 'Ꞗ', + 'ꞙ' => 'Ꞙ', + 'ꞛ' => 'Ꞛ', + 'ꞝ' => 'Ꞝ', + 'ꞟ' => 'Ꞟ', + 'ꞡ' => 'Ꞡ', + 'ꞣ' => 'Ꞣ', + 'ꞥ' => 'Ꞥ', + 'ꞧ' => 'Ꞧ', + 'ꞩ' => 'Ꞩ', + 'ꞵ' => 'Ꞵ', + 'ꞷ' => 'Ꞷ', + 'ꞹ' => 'Ꞹ', + 'ꞻ' => 'Ꞻ', + 'ꞽ' => 'Ꞽ', + 'ꞿ' => 'Ꞿ', + 'ꟃ' => 'Ꟃ', + 'ꟈ' => 'Ꟈ', + 'ꟊ' => 'Ꟊ', + 'ꟶ' => 'Ꟶ', + 'ꭓ' => 'Ꭓ', + 'ꭰ' => 'Ꭰ', + 'ꭱ' => 'Ꭱ', + 'ꭲ' => 'Ꭲ', + 'ꭳ' => 'Ꭳ', + 'ꭴ' => 'Ꭴ', + 'ꭵ' => 'Ꭵ', + 'ꭶ' => 'Ꭶ', + 'ꭷ' => 'Ꭷ', + 'ꭸ' => 'Ꭸ', + 'ꭹ' => 'Ꭹ', + 'ꭺ' => 'Ꭺ', + 'ꭻ' => 'Ꭻ', + 'ꭼ' => 'Ꭼ', + 'ꭽ' => 'Ꭽ', + 'ꭾ' => 'Ꭾ', + 'ꭿ' => 'Ꭿ', + 'ꮀ' => 'Ꮀ', + 'ꮁ' => 'Ꮁ', + 'ꮂ' => 'Ꮂ', + 'ꮃ' => 'Ꮃ', + 'ꮄ' => 'Ꮄ', + 'ꮅ' => 'Ꮅ', + 'ꮆ' => 'Ꮆ', + 'ꮇ' => 'Ꮇ', + 'ꮈ' => 'Ꮈ', + 'ꮉ' => 'Ꮉ', + 'ꮊ' => 'Ꮊ', + 'ꮋ' => 'Ꮋ', + 'ꮌ' => 'Ꮌ', + 'ꮍ' => 'Ꮍ', + 'ꮎ' => 'Ꮎ', + 'ꮏ' => 'Ꮏ', + 'ꮐ' => 'Ꮐ', + 'ꮑ' => 'Ꮑ', + 'ꮒ' => 'Ꮒ', + 'ꮓ' => 'Ꮓ', + 'ꮔ' => 'Ꮔ', + 'ꮕ' => 'Ꮕ', + 'ꮖ' => 'Ꮖ', + 'ꮗ' => 'Ꮗ', + 'ꮘ' => 'Ꮘ', + 'ꮙ' => 'Ꮙ', + 'ꮚ' => 'Ꮚ', + 'ꮛ' => 'Ꮛ', + 'ꮜ' => 'Ꮜ', + 'ꮝ' => 'Ꮝ', + 'ꮞ' => 'Ꮞ', + 'ꮟ' => 'Ꮟ', + 'ꮠ' => 'Ꮠ', + 'ꮡ' => 'Ꮡ', + 'ꮢ' => 'Ꮢ', + 'ꮣ' => 'Ꮣ', + 'ꮤ' => 'Ꮤ', + 'ꮥ' => 'Ꮥ', + 'ꮦ' => 'Ꮦ', + 'ꮧ' => 'Ꮧ', + 'ꮨ' => 'Ꮨ', + 'ꮩ' => 'Ꮩ', + 'ꮪ' => 'Ꮪ', + 'ꮫ' => 'Ꮫ', + 'ꮬ' => 'Ꮬ', + 'ꮭ' => 'Ꮭ', + 'ꮮ' => 'Ꮮ', + 'ꮯ' => 'Ꮯ', + 'ꮰ' => 'Ꮰ', + 'ꮱ' => 'Ꮱ', + 'ꮲ' => 'Ꮲ', + 'ꮳ' => 'Ꮳ', + 'ꮴ' => 'Ꮴ', + 'ꮵ' => 'Ꮵ', + 'ꮶ' => 'Ꮶ', + 'ꮷ' => 'Ꮷ', + 'ꮸ' => 'Ꮸ', + 'ꮹ' => 'Ꮹ', + 'ꮺ' => 'Ꮺ', + 'ꮻ' => 'Ꮻ', + 'ꮼ' => 'Ꮼ', + 'ꮽ' => 'Ꮽ', + 'ꮾ' => 'Ꮾ', + 'ꮿ' => 'Ꮿ', + 'a' => 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + '𐐨' => '𐐀', + '𐐩' => '𐐁', + '𐐪' => '𐐂', + '𐐫' => '𐐃', + '𐐬' => '𐐄', + '𐐭' => '𐐅', + '𐐮' => '𐐆', + '𐐯' => '𐐇', + '𐐰' => '𐐈', + '𐐱' => '𐐉', + '𐐲' => '𐐊', + '𐐳' => '𐐋', + '𐐴' => '𐐌', + '𐐵' => '𐐍', + '𐐶' => '𐐎', + '𐐷' => '𐐏', + '𐐸' => '𐐐', + '𐐹' => '𐐑', + '𐐺' => '𐐒', + '𐐻' => '𐐓', + '𐐼' => '𐐔', + '𐐽' => '𐐕', + '𐐾' => '𐐖', + '𐐿' => '𐐗', + '𐑀' => '𐐘', + '𐑁' => '𐐙', + '𐑂' => '𐐚', + '𐑃' => '𐐛', + '𐑄' => '𐐜', + '𐑅' => '𐐝', + '𐑆' => '𐐞', + '𐑇' => '𐐟', + '𐑈' => '𐐠', + '𐑉' => '𐐡', + '𐑊' => '𐐢', + '𐑋' => '𐐣', + '𐑌' => '𐐤', + '𐑍' => '𐐥', + '𐑎' => '𐐦', + '𐑏' => '𐐧', + '𐓘' => '𐒰', + '𐓙' => '𐒱', + '𐓚' => '𐒲', + '𐓛' => '𐒳', + '𐓜' => '𐒴', + '𐓝' => '𐒵', + '𐓞' => '𐒶', + '𐓟' => '𐒷', + '𐓠' => '𐒸', + '𐓡' => '𐒹', + '𐓢' => '𐒺', + '𐓣' => '𐒻', + '𐓤' => '𐒼', + '𐓥' => '𐒽', + '𐓦' => '𐒾', + '𐓧' => '𐒿', + '𐓨' => '𐓀', + '𐓩' => '𐓁', + '𐓪' => '𐓂', + '𐓫' => '𐓃', + '𐓬' => '𐓄', + '𐓭' => '𐓅', + '𐓮' => '𐓆', + '𐓯' => '𐓇', + '𐓰' => '𐓈', + '𐓱' => '𐓉', + '𐓲' => '𐓊', + '𐓳' => '𐓋', + '𐓴' => '𐓌', + '𐓵' => '𐓍', + '𐓶' => '𐓎', + '𐓷' => '𐓏', + '𐓸' => '𐓐', + '𐓹' => '𐓑', + '𐓺' => '𐓒', + '𐓻' => '𐓓', + '𐳀' => '𐲀', + '𐳁' => '𐲁', + '𐳂' => '𐲂', + '𐳃' => '𐲃', + '𐳄' => '𐲄', + '𐳅' => '𐲅', + '𐳆' => '𐲆', + '𐳇' => '𐲇', + '𐳈' => '𐲈', + '𐳉' => '𐲉', + '𐳊' => '𐲊', + '𐳋' => '𐲋', + '𐳌' => '𐲌', + '𐳍' => '𐲍', + '𐳎' => '𐲎', + '𐳏' => '𐲏', + '𐳐' => '𐲐', + '𐳑' => '𐲑', + '𐳒' => '𐲒', + '𐳓' => '𐲓', + '𐳔' => '𐲔', + '𐳕' => '𐲕', + '𐳖' => '𐲖', + '𐳗' => '𐲗', + '𐳘' => '𐲘', + '𐳙' => '𐲙', + '𐳚' => '𐲚', + '𐳛' => '𐲛', + '𐳜' => '𐲜', + '𐳝' => '𐲝', + '𐳞' => '𐲞', + '𐳟' => '𐲟', + '𐳠' => '𐲠', + '𐳡' => '𐲡', + '𐳢' => '𐲢', + '𐳣' => '𐲣', + '𐳤' => '𐲤', + '𐳥' => '𐲥', + '𐳦' => '𐲦', + '𐳧' => '𐲧', + '𐳨' => '𐲨', + '𐳩' => '𐲩', + '𐳪' => '𐲪', + '𐳫' => '𐲫', + '𐳬' => '𐲬', + '𐳭' => '𐲭', + '𐳮' => '𐲮', + '𐳯' => '𐲯', + '𐳰' => '𐲰', + '𐳱' => '𐲱', + '𐳲' => '𐲲', + '𑣀' => '𑢠', + '𑣁' => '𑢡', + '𑣂' => '𑢢', + '𑣃' => '𑢣', + '𑣄' => '𑢤', + '𑣅' => '𑢥', + '𑣆' => '𑢦', + '𑣇' => '𑢧', + '𑣈' => '𑢨', + '𑣉' => '𑢩', + '𑣊' => '𑢪', + '𑣋' => '𑢫', + '𑣌' => '𑢬', + '𑣍' => '𑢭', + '𑣎' => '𑢮', + '𑣏' => '𑢯', + '𑣐' => '𑢰', + '𑣑' => '𑢱', + '𑣒' => '𑢲', + '𑣓' => '𑢳', + '𑣔' => '𑢴', + '𑣕' => '𑢵', + '𑣖' => '𑢶', + '𑣗' => '𑢷', + '𑣘' => '𑢸', + '𑣙' => '𑢹', + '𑣚' => '𑢺', + '𑣛' => '𑢻', + '𑣜' => '𑢼', + '𑣝' => '𑢽', + '𑣞' => '𑢾', + '𑣟' => '𑢿', + '𖹠' => '𖹀', + '𖹡' => '𖹁', + '𖹢' => '𖹂', + '𖹣' => '𖹃', + '𖹤' => '𖹄', + '𖹥' => '𖹅', + '𖹦' => '𖹆', + '𖹧' => '𖹇', + '𖹨' => '𖹈', + '𖹩' => '𖹉', + '𖹪' => '𖹊', + '𖹫' => '𖹋', + '𖹬' => '𖹌', + '𖹭' => '𖹍', + '𖹮' => '𖹎', + '𖹯' => '𖹏', + '𖹰' => '𖹐', + '𖹱' => '𖹑', + '𖹲' => '𖹒', + '𖹳' => '𖹓', + '𖹴' => '𖹔', + '𖹵' => '𖹕', + '𖹶' => '𖹖', + '𖹷' => '𖹗', + '𖹸' => '𖹘', + '𖹹' => '𖹙', + '𖹺' => '𖹚', + '𖹻' => '𖹛', + '𖹼' => '𖹜', + '𖹽' => '𖹝', + '𖹾' => '𖹞', + '𖹿' => '𖹟', + '𞤢' => '𞤀', + '𞤣' => '𞤁', + '𞤤' => '𞤂', + '𞤥' => '𞤃', + '𞤦' => '𞤄', + '𞤧' => '𞤅', + '𞤨' => '𞤆', + '𞤩' => '𞤇', + '𞤪' => '𞤈', + '𞤫' => '𞤉', + '𞤬' => '𞤊', + '𞤭' => '𞤋', + '𞤮' => '𞤌', + '𞤯' => '𞤍', + '𞤰' => '𞤎', + '𞤱' => '𞤏', + '𞤲' => '𞤐', + '𞤳' => '𞤑', + '𞤴' => '𞤒', + '𞤵' => '𞤓', + '𞤶' => '𞤔', + '𞤷' => '𞤕', + '𞤸' => '𞤖', + '𞤹' => '𞤗', + '𞤺' => '𞤘', + '𞤻' => '𞤙', + '𞤼' => '𞤚', + '𞤽' => '𞤛', + '𞤾' => '𞤜', + '𞤿' => '𞤝', + '𞥀' => '𞤞', + '𞥁' => '𞤟', + '𞥂' => '𞤠', + '𞥃' => '𞤡', + 'ß' => 'SS', + 'ff' => 'FF', + 'fi' => 'FI', + 'fl' => 'FL', + 'ffi' => 'FFI', + 'ffl' => 'FFL', + 'ſt' => 'ST', + 'st' => 'ST', + 'և' => 'ԵՒ', + 'ﬓ' => 'ՄՆ', + 'ﬔ' => 'ՄԵ', + 'ﬕ' => 'ՄԻ', + 'ﬖ' => 'ՎՆ', + 'ﬗ' => 'ՄԽ', + 'ʼn' => 'ʼN', + 'ΐ' => 'Ϊ́', + 'ΰ' => 'Ϋ́', + 'ǰ' => 'J̌', + 'ẖ' => 'H̱', + 'ẗ' => 'T̈', + 'ẘ' => 'W̊', + 'ẙ' => 'Y̊', + 'ẚ' => 'Aʾ', + 'ὐ' => 'Υ̓', + 'ὒ' => 'Υ̓̀', + 'ὔ' => 'Υ̓́', + 'ὖ' => 'Υ̓͂', + 'ᾶ' => 'Α͂', + 'ῆ' => 'Η͂', + 'ῒ' => 'Ϊ̀', + 'ΐ' => 'Ϊ́', + 'ῖ' => 'Ι͂', + 'ῗ' => 'Ϊ͂', + 'ῢ' => 'Ϋ̀', + 'ΰ' => 'Ϋ́', + 'ῤ' => 'Ρ̓', + 'ῦ' => 'Υ͂', + 'ῧ' => 'Ϋ͂', + 'ῶ' => 'Ω͂', + 'ᾈ' => 'ἈΙ', + 'ᾉ' => 'ἉΙ', + 'ᾊ' => 'ἊΙ', + 'ᾋ' => 'ἋΙ', + 'ᾌ' => 'ἌΙ', + 'ᾍ' => 'ἍΙ', + 'ᾎ' => 'ἎΙ', + 'ᾏ' => 'ἏΙ', + 'ᾘ' => 'ἨΙ', + 'ᾙ' => 'ἩΙ', + 'ᾚ' => 'ἪΙ', + 'ᾛ' => 'ἫΙ', + 'ᾜ' => 'ἬΙ', + 'ᾝ' => 'ἭΙ', + 'ᾞ' => 'ἮΙ', + 'ᾟ' => 'ἯΙ', + 'ᾨ' => 'ὨΙ', + 'ᾩ' => 'ὩΙ', + 'ᾪ' => 'ὪΙ', + 'ᾫ' => 'ὫΙ', + 'ᾬ' => 'ὬΙ', + 'ᾭ' => 'ὭΙ', + 'ᾮ' => 'ὮΙ', + 'ᾯ' => 'ὯΙ', + 'ᾼ' => 'ΑΙ', + 'ῌ' => 'ΗΙ', + 'ῼ' => 'ΩΙ', + 'ᾲ' => 'ᾺΙ', + 'ᾴ' => 'ΆΙ', + 'ῂ' => 'ῊΙ', + 'ῄ' => 'ΉΙ', + 'ῲ' => 'ῺΙ', + 'ῴ' => 'ΏΙ', + 'ᾷ' => 'Α͂Ι', + 'ῇ' => 'Η͂Ι', + 'ῷ' => 'Ω͂Ι', +); diff --git a/vendor/symfony/polyfill-mbstring/bootstrap.php b/vendor/symfony/polyfill-mbstring/bootstrap.php new file mode 100644 index 0000000..1fedd1f --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/bootstrap.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('mb_convert_encoding')) { + function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } +} +if (!function_exists('mb_decode_mimeheader')) { + function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } +} +if (!function_exists('mb_encode_mimeheader')) { + function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } +} +if (!function_exists('mb_decode_numericentity')) { + function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } +} +if (!function_exists('mb_encode_numericentity')) { + function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } +} +if (!function_exists('mb_convert_case')) { + function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } +} +if (!function_exists('mb_internal_encoding')) { + function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } +} +if (!function_exists('mb_language')) { + function mb_language($language = null) { return p\Mbstring::mb_language($language); } +} +if (!function_exists('mb_list_encodings')) { + function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } +} +if (!function_exists('mb_encoding_aliases')) { + function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } +} +if (!function_exists('mb_check_encoding')) { + function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } +} +if (!function_exists('mb_detect_encoding')) { + function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } +} +if (!function_exists('mb_detect_order')) { + function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } +} +if (!function_exists('mb_parse_str')) { + function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; } +} +if (!function_exists('mb_strlen')) { + function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } +} +if (!function_exists('mb_strpos')) { + function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strtolower')) { + function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } +} +if (!function_exists('mb_strtoupper')) { + function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } +} +if (!function_exists('mb_substitute_character')) { + function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } +} +if (!function_exists('mb_substr')) { + function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } +} +if (!function_exists('mb_stripos')) { + function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_stristr')) { + function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strrchr')) { + function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strrichr')) { + function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strripos')) { + function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strrpos')) { + function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strstr')) { + function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_get_info')) { + function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } +} +if (!function_exists('mb_http_output')) { + function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } +} +if (!function_exists('mb_strwidth')) { + function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } +} +if (!function_exists('mb_substr_count')) { + function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } +} +if (!function_exists('mb_output_handler')) { + function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } +} +if (!function_exists('mb_http_input')) { + function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); } +} + +if (!function_exists('mb_convert_variables')) { + function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } +} + +if (!function_exists('mb_ord')) { + function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } +} +if (!function_exists('mb_chr')) { + function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } +} +if (!function_exists('mb_scrub')) { + function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } +} +if (!function_exists('mb_str_split')) { + function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } +} + +if (extension_loaded('mbstring')) { + return; +} + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); +} +if (!defined('MB_CASE_LOWER')) { + define('MB_CASE_LOWER', 1); +} +if (!defined('MB_CASE_TITLE')) { + define('MB_CASE_TITLE', 2); +} diff --git a/vendor/symfony/polyfill-mbstring/bootstrap80.php b/vendor/symfony/polyfill-mbstring/bootstrap80.php new file mode 100644 index 0000000..82f5ac4 --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/bootstrap80.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (!function_exists('mb_convert_encoding')) { + function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); } +} +if (!function_exists('mb_decode_mimeheader')) { + function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); } +} +if (!function_exists('mb_encode_mimeheader')) { + function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); } +} +if (!function_exists('mb_decode_numericentity')) { + function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); } +} +if (!function_exists('mb_encode_numericentity')) { + function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); } +} +if (!function_exists('mb_convert_case')) { + function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); } +} +if (!function_exists('mb_internal_encoding')) { + function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); } +} +if (!function_exists('mb_language')) { + function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); } +} +if (!function_exists('mb_list_encodings')) { + function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); } +} +if (!function_exists('mb_encoding_aliases')) { + function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); } +} +if (!function_exists('mb_check_encoding')) { + function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); } +} +if (!function_exists('mb_detect_encoding')) { + function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); } +} +if (!function_exists('mb_detect_order')) { + function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); } +} +if (!function_exists('mb_parse_str')) { + function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; } +} +if (!function_exists('mb_strlen')) { + function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); } +} +if (!function_exists('mb_strpos')) { + function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strtolower')) { + function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); } +} +if (!function_exists('mb_strtoupper')) { + function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); } +} +if (!function_exists('mb_substitute_character')) { + function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); } +} +if (!function_exists('mb_substr')) { + function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); } +} +if (!function_exists('mb_stripos')) { + function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_stristr')) { + function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strrchr')) { + function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strrichr')) { + function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strripos')) { + function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strrpos')) { + function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strstr')) { + function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_get_info')) { + function mb_get_info(?string $type = 'all'): array|string|int|false { return p\Mbstring::mb_get_info((string) $type); } +} +if (!function_exists('mb_http_output')) { + function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); } +} +if (!function_exists('mb_strwidth')) { + function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); } +} +if (!function_exists('mb_substr_count')) { + function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); } +} +if (!function_exists('mb_output_handler')) { + function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); } +} +if (!function_exists('mb_http_input')) { + function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); } +} + +if (!function_exists('mb_convert_variables')) { + function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); } +} + +if (!function_exists('mb_ord')) { + function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); } +} +if (!function_exists('mb_chr')) { + function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); } +} +if (!function_exists('mb_scrub')) { + function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); } +} +if (!function_exists('mb_str_split')) { + function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); } +} + +if (extension_loaded('mbstring')) { + return; +} + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); +} +if (!defined('MB_CASE_LOWER')) { + define('MB_CASE_LOWER', 1); +} +if (!defined('MB_CASE_TITLE')) { + define('MB_CASE_TITLE', 2); +} diff --git a/vendor/symfony/polyfill-mbstring/composer.json b/vendor/symfony/polyfill-mbstring/composer.json new file mode 100644 index 0000000..1fa21ca --- /dev/null +++ b/vendor/symfony/polyfill-mbstring/composer.json @@ -0,0 +1,41 @@ +{ + "name": "symfony/polyfill-mbstring", + "type": "library", + "description": "Symfony polyfill for the Mbstring extension", + "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/polyfill-php73/LICENSE b/vendor/symfony/polyfill-php73/LICENSE new file mode 100644 index 0000000..3f853aa --- /dev/null +++ b/vendor/symfony/polyfill-php73/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-php73/Php73.php b/vendor/symfony/polyfill-php73/Php73.php new file mode 100644 index 0000000..65c35a6 --- /dev/null +++ b/vendor/symfony/polyfill-php73/Php73.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php73; + +/** + * @author Gabriel Caruso + * @author Ion Bazan + * + * @internal + */ +final class Php73 +{ + public static $startAt = 1533462603; + + /** + * @param bool $asNum + * + * @return array|float|int + */ + public static function hrtime($asNum = false) + { + $ns = microtime(false); + $s = substr($ns, 11) - self::$startAt; + $ns = 1E9 * (float) $ns; + + if ($asNum) { + $ns += $s * 1E9; + + return \PHP_INT_SIZE === 4 ? $ns : (int) $ns; + } + + return [$s, (int) $ns]; + } +} diff --git a/vendor/symfony/polyfill-php73/README.md b/vendor/symfony/polyfill-php73/README.md new file mode 100644 index 0000000..b3ebbce --- /dev/null +++ b/vendor/symfony/polyfill-php73/README.md @@ -0,0 +1,18 @@ +Symfony Polyfill / Php73 +======================== + +This component provides functions added to PHP 7.3 core: + +- [`array_key_first`](https://php.net/array_key_first) +- [`array_key_last`](https://php.net/array_key_last) +- [`hrtime`](https://php.net/function.hrtime) +- [`is_countable`](https://php.net/is_countable) +- [`JsonException`](https://php.net/JsonException) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php b/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php new file mode 100644 index 0000000..f06d6c2 --- /dev/null +++ b/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 70300) { + class JsonException extends Exception + { + } +} diff --git a/vendor/symfony/polyfill-php73/bootstrap.php b/vendor/symfony/polyfill-php73/bootstrap.php new file mode 100644 index 0000000..d6b2153 --- /dev/null +++ b/vendor/symfony/polyfill-php73/bootstrap.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php73 as p; + +if (\PHP_VERSION_ID >= 70300) { + return; +} + +if (!function_exists('is_countable')) { + function is_countable($value) { return is_array($value) || $value instanceof Countable || $value instanceof ResourceBundle || $value instanceof SimpleXmlElement; } +} +if (!function_exists('hrtime')) { + require_once __DIR__.'/Php73.php'; + p\Php73::$startAt = (int) microtime(true); + function hrtime($as_number = false) { return p\Php73::hrtime($as_number); } +} +if (!function_exists('array_key_first')) { + function array_key_first(array $array) { foreach ($array as $key => $value) { return $key; } } +} +if (!function_exists('array_key_last')) { + function array_key_last(array $array) { return key(array_slice($array, -1, 1, true)); } +} diff --git a/vendor/symfony/polyfill-php73/composer.json b/vendor/symfony/polyfill-php73/composer.json new file mode 100644 index 0000000..a7fe478 --- /dev/null +++ b/vendor/symfony/polyfill-php73/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/polyfill-php73", + "type": "library", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php73\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/polyfill-php80/LICENSE b/vendor/symfony/polyfill-php80/LICENSE new file mode 100644 index 0000000..5593b1d --- /dev/null +++ b/vendor/symfony/polyfill-php80/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-php80/Php80.php b/vendor/symfony/polyfill-php80/Php80.php new file mode 100644 index 0000000..5fef511 --- /dev/null +++ b/vendor/symfony/polyfill-php80/Php80.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Ion Bazan + * @author Nico Oelgart + * @author Nicolas Grekas + * + * @internal + */ +final class Php80 +{ + public static function fdiv(float $dividend, float $divisor): float + { + return @($dividend / $divisor); + } + + public static function get_debug_type($value): string + { + switch (true) { + case null === $value: return 'null'; + case \is_bool($value): return 'bool'; + case \is_string($value): return 'string'; + case \is_array($value): return 'array'; + case \is_int($value): return 'int'; + case \is_float($value): return 'float'; + case \is_object($value): break; + case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; + default: + if (null === $type = @get_resource_type($value)) { + return 'unknown'; + } + + if ('Unknown' === $type) { + $type = 'closed'; + } + + return "resource ($type)"; + } + + $class = \get_class($value); + + if (false === strpos($class, '@')) { + return $class; + } + + return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; + } + + public static function get_resource_id($res): int + { + if (!\is_resource($res) && null === @get_resource_type($res)) { + throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); + } + + return (int) $res; + } + + public static function preg_last_error_msg(): string + { + switch (preg_last_error()) { + case \PREG_INTERNAL_ERROR: + return 'Internal error'; + case \PREG_BAD_UTF8_ERROR: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + case \PREG_BAD_UTF8_OFFSET_ERROR: + return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; + case \PREG_BACKTRACK_LIMIT_ERROR: + return 'Backtrack limit exhausted'; + case \PREG_RECURSION_LIMIT_ERROR: + return 'Recursion limit exhausted'; + case \PREG_JIT_STACKLIMIT_ERROR: + return 'JIT stack limit exhausted'; + case \PREG_NO_ERROR: + return 'No error'; + default: + return 'Unknown error'; + } + } + + public static function str_contains(string $haystack, string $needle): bool + { + return '' === $needle || false !== strpos($haystack, $needle); + } + + public static function str_starts_with(string $haystack, string $needle): bool + { + return 0 === strncmp($haystack, $needle, \strlen($needle)); + } + + public static function str_ends_with(string $haystack, string $needle): bool + { + return '' === $needle || ('' !== $haystack && 0 === substr_compare($haystack, $needle, -\strlen($needle))); + } +} diff --git a/vendor/symfony/polyfill-php80/README.md b/vendor/symfony/polyfill-php80/README.md new file mode 100644 index 0000000..10b8ee4 --- /dev/null +++ b/vendor/symfony/polyfill-php80/README.md @@ -0,0 +1,24 @@ +Symfony Polyfill / Php80 +======================== + +This component provides features added to PHP 8.0 core: + +- `Stringable` interface +- [`fdiv`](https://php.net/fdiv) +- `ValueError` class +- `UnhandledMatchError` class +- `FILTER_VALIDATE_BOOL` constant +- [`get_debug_type`](https://php.net/get_debug_type) +- [`preg_last_error_msg`](https://php.net/preg_last_error_msg) +- [`str_contains`](https://php.net/str_contains) +- [`str_starts_with`](https://php.net/str_starts_with) +- [`str_ends_with`](https://php.net/str_ends_with) +- [`get_resource_id`](https://php.net/get_resource_id) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php b/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php new file mode 100644 index 0000000..7ea6d27 --- /dev/null +++ b/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php @@ -0,0 +1,22 @@ +flags = $flags; + } +} diff --git a/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php b/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php new file mode 100644 index 0000000..77e037c --- /dev/null +++ b/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php @@ -0,0 +1,11 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php80 as p; + +if (\PHP_VERSION_ID >= 80000) { + return; +} + +if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { + define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); +} + +if (!function_exists('fdiv')) { + function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } +} +if (!function_exists('preg_last_error_msg')) { + function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } +} +if (!function_exists('str_contains')) { + function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_starts_with')) { + function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_ends_with')) { + function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('get_debug_type')) { + function get_debug_type($value): string { return p\Php80::get_debug_type($value); } +} +if (!function_exists('get_resource_id')) { + function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } +} diff --git a/vendor/symfony/polyfill-php80/composer.json b/vendor/symfony/polyfill-php80/composer.json new file mode 100644 index 0000000..5fe679d --- /dev/null +++ b/vendor/symfony/polyfill-php80/composer.json @@ -0,0 +1,40 @@ +{ + "name": "symfony/polyfill-php80", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/polyfill-php81/LICENSE b/vendor/symfony/polyfill-php81/LICENSE new file mode 100644 index 0000000..efb17f9 --- /dev/null +++ b/vendor/symfony/polyfill-php81/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-php81/Php81.php b/vendor/symfony/polyfill-php81/Php81.php new file mode 100644 index 0000000..f0507b7 --- /dev/null +++ b/vendor/symfony/polyfill-php81/Php81.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php81; + +/** + * @author Nicolas Grekas + * + * @internal + */ +final class Php81 +{ + public static function array_is_list(array $array): bool + { + if ([] === $array || $array === array_values($array)) { + return true; + } + + $nextKey = -1; + + foreach ($array as $k => $v) { + if ($k !== ++$nextKey) { + return false; + } + } + + return true; + } +} diff --git a/vendor/symfony/polyfill-php81/README.md b/vendor/symfony/polyfill-php81/README.md new file mode 100644 index 0000000..5ef61be --- /dev/null +++ b/vendor/symfony/polyfill-php81/README.md @@ -0,0 +1,16 @@ +Symfony Polyfill / Php81 +======================== + +This component provides features added to PHP 8.1 core: + +- [`array_is_list`](https://php.net/array_is_list) +- [`MYSQLI_REFRESH_REPLICA`](https://www.php.net/manual/en/mysqli.constants.php#constantmysqli-refresh-replica) constant +- [`ReturnTypeWillChange`](https://wiki.php.net/rfc/internal_method_return_types) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php b/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php new file mode 100644 index 0000000..f4cad34 --- /dev/null +++ b/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php @@ -0,0 +1,11 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php81 as p; + +if (\PHP_VERSION_ID >= 80100) { + return; +} + +if (defined('MYSQLI_REFRESH_SLAVE') && !defined('MYSQLI_REFRESH_REPLICA')) { + define('MYSQLI_REFRESH_REPLICA', 64); +} + +if (!function_exists('array_is_list')) { + function array_is_list(array $array): bool { return p\Php81::array_is_list($array); } +} + +if (!function_exists('enum_exists')) { + function enum_exists(string $enum, bool $autoload = true): bool { return $autoload && class_exists($enum) && false; } +} diff --git a/vendor/symfony/polyfill-php81/composer.json b/vendor/symfony/polyfill-php81/composer.json new file mode 100644 index 0000000..c39ccf4 --- /dev/null +++ b/vendor/symfony/polyfill-php81/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/polyfill-php81", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php81\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/service-contracts/.gitignore b/vendor/symfony/service-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/service-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/service-contracts/Attribute/Required.php b/vendor/symfony/service-contracts/Attribute/Required.php new file mode 100644 index 0000000..9df8511 --- /dev/null +++ b/vendor/symfony/service-contracts/Attribute/Required.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Attribute; + +/** + * A required dependency. + * + * This attribute indicates that a property holds a required dependency. The annotated property or method should be + * considered during the instantiation process of the containing class. + * + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] +final class Required +{ +} diff --git a/vendor/symfony/service-contracts/CHANGELOG.md b/vendor/symfony/service-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/vendor/symfony/service-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/vendor/symfony/service-contracts/LICENSE b/vendor/symfony/service-contracts/LICENSE new file mode 100644 index 0000000..2358414 --- /dev/null +++ b/vendor/symfony/service-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/service-contracts/README.md b/vendor/symfony/service-contracts/README.md new file mode 100644 index 0000000..41e054a --- /dev/null +++ b/vendor/symfony/service-contracts/README.md @@ -0,0 +1,9 @@ +Symfony Service Contracts +========================= + +A set of abstractions extracted out of the Symfony components. + +Can be used to build on semantics that the Symfony components proved useful - and +that already have battle tested implementations. + +See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/vendor/symfony/service-contracts/ResetInterface.php b/vendor/symfony/service-contracts/ResetInterface.php new file mode 100644 index 0000000..1af1075 --- /dev/null +++ b/vendor/symfony/service-contracts/ResetInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * Provides a way to reset an object to its initial state. + * + * When calling the "reset()" method on an object, it should be put back to its + * initial state. This usually means clearing any internal buffers and forwarding + * the call to internal dependencies. All properties of the object should be put + * back to the same state it had when it was first ready to use. + * + * This method could be called, for example, to recycle objects that are used as + * services, so that they can be used to handle several requests in the same + * process loop (note that we advise making your services stateless instead of + * implementing this interface when possible.) + */ +interface ResetInterface +{ + public function reset(); +} diff --git a/vendor/symfony/service-contracts/ServiceLocatorTrait.php b/vendor/symfony/service-contracts/ServiceLocatorTrait.php new file mode 100644 index 0000000..74dfa43 --- /dev/null +++ b/vendor/symfony/service-contracts/ServiceLocatorTrait.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(ContainerExceptionInterface::class); +class_exists(NotFoundExceptionInterface::class); + +/** + * A trait to help implement ServiceProviderInterface. + * + * @author Robin Chalas + * @author Nicolas Grekas + */ +trait ServiceLocatorTrait +{ + private $factories; + private $loading = []; + private $providedTypes; + + /** + * @param callable[] $factories + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function has(string $id) + { + return isset($this->factories[$id]); + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get(string $id) + { + if (!isset($this->factories[$id])) { + throw $this->createNotFoundException($id); + } + + if (isset($this->loading[$id])) { + $ids = array_values($this->loading); + $ids = \array_slice($this->loading, array_search($id, $ids)); + $ids[] = $id; + + throw $this->createCircularReferenceException($id, $ids); + } + + $this->loading[$id] = $id; + try { + return $this->factories[$id]($this); + } finally { + unset($this->loading[$id]); + } + } + + /** + * {@inheritdoc} + */ + public function getProvidedServices(): array + { + if (null === $this->providedTypes) { + $this->providedTypes = []; + + foreach ($this->factories as $name => $factory) { + if (!\is_callable($factory)) { + $this->providedTypes[$name] = '?'; + } else { + $type = (new \ReflectionFunction($factory))->getReturnType(); + + $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?'; + } + } + } + + return $this->providedTypes; + } + + private function createNotFoundException(string $id): NotFoundExceptionInterface + { + if (!$alternatives = array_keys($this->factories)) { + $message = 'is empty...'; + } else { + $last = array_pop($alternatives); + if ($alternatives) { + $message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); + } else { + $message = sprintf('only knows about the "%s" service.', $last); + } + } + + if ($this->loading) { + $message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); + } else { + $message = sprintf('Service "%s" not found: the current service locator %s', $id, $message); + } + + return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface { + }; + } + + private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface + { + return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { + }; + } +} diff --git a/vendor/symfony/service-contracts/ServiceProviderInterface.php b/vendor/symfony/service-contracts/ServiceProviderInterface.php new file mode 100644 index 0000000..c60ad0b --- /dev/null +++ b/vendor/symfony/service-contracts/ServiceProviderInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; + +/** + * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container. + * + * @author Nicolas Grekas + * @author Mateusz Sip + */ +interface ServiceProviderInterface extends ContainerInterface +{ + /** + * Returns an associative array of service types keyed by the identifiers provided by the current container. + * + * Examples: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface + * * ['foo' => '?'] means the container provides service name "foo" of unspecified type + * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null + * + * @return string[] The provided service types, keyed by service names + */ + public function getProvidedServices(): array; +} diff --git a/vendor/symfony/service-contracts/ServiceSubscriberInterface.php b/vendor/symfony/service-contracts/ServiceSubscriberInterface.php new file mode 100644 index 0000000..098ab90 --- /dev/null +++ b/vendor/symfony/service-contracts/ServiceSubscriberInterface.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. + * + * The getSubscribedServices method returns an array of service types required by such instances, + * optionally keyed by the service names used internally. Service types that start with an interrogation + * mark "?" are optional, while the other ones are mandatory service dependencies. + * + * The injected service locators SHOULD NOT allow access to any other services not specified by the method. + * + * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. + * This interface does not dictate any injection method for these service locators, although constructor + * injection is recommended. + * + * @author Nicolas Grekas + */ +interface ServiceSubscriberInterface +{ + /** + * Returns an array of service types required by such instances, optionally keyed by the service names used internally. + * + * For mandatory dependencies: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name + * internally to fetch a service which must implement Psr\Log\LoggerInterface. + * * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name + * internally to fetch an iterable of Psr\Log\LoggerInterface instances. + * * ['Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface'] + * + * otherwise: + * + * * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency + * * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency + * * ['?Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface'] + * + * @return string[] The required service types, optionally keyed by service names + */ + public static function getSubscribedServices(); +} diff --git a/vendor/symfony/service-contracts/ServiceSubscriberTrait.php b/vendor/symfony/service-contracts/ServiceSubscriberTrait.php new file mode 100644 index 0000000..0c08530 --- /dev/null +++ b/vendor/symfony/service-contracts/ServiceSubscriberTrait.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services from + * private method return types. Service ids are available as "ClassName::methodName". + * + * @author Kevin Bond + */ +trait ServiceSubscriberTrait +{ + /** @var ContainerInterface */ + protected $container; + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices(): array + { + static $services; + + if (null !== $services) { + return $services; + } + + $services = \is_callable(['parent', __FUNCTION__]) ? parent::getSubscribedServices() : []; + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + continue; + } + + if (self::class !== $method->getDeclaringClass()->name) { + continue; + } + + if (!($returnType = $method->getReturnType()) instanceof \ReflectionNamedType) { + continue; + } + + if ($returnType->isBuiltin()) { + continue; + } + + $services[self::class.'::'.$method->name] = '?'.$returnType->getName(); + } + + return $services; + } + + /** + * @required + * + * @return ContainerInterface|null + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + + if (\is_callable(['parent', __FUNCTION__])) { + return parent::setContainer($container); + } + + return null; + } +} diff --git a/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php b/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php new file mode 100644 index 0000000..2a1b565 --- /dev/null +++ b/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Test; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\ServiceLocatorTrait; + +abstract class ServiceLocatorTest extends TestCase +{ + /** + * @return ContainerInterface + */ + protected function getServiceLocator(array $factories) + { + return new class($factories) implements ContainerInterface { + use ServiceLocatorTrait; + }; + } + + public function testHas() + { + $locator = $this->getServiceLocator([ + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + function () { return 'dummy'; }, + ]); + + $this->assertTrue($locator->has('foo')); + $this->assertTrue($locator->has('bar')); + $this->assertFalse($locator->has('dummy')); + } + + public function testGet() + { + $locator = $this->getServiceLocator([ + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + ]); + + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame('baz', $locator->get('bar')); + } + + public function testGetDoesNotMemoize() + { + $i = 0; + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$i) { + ++$i; + + return 'bar'; + }, + ]); + + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame(2, $i); + } + + public function testThrowsOnUndefinedInternalService() + { + if (!$this->getExpectedException()) { + $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); + $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); + } + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$locator) { return $locator->get('bar'); }, + ]); + + $locator->get('foo'); + } + + public function testThrowsOnCircularReference() + { + $this->expectException(\Psr\Container\ContainerExceptionInterface::class); + $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); + $locator = $this->getServiceLocator([ + 'foo' => function () use (&$locator) { return $locator->get('bar'); }, + 'bar' => function () use (&$locator) { return $locator->get('baz'); }, + 'baz' => function () use (&$locator) { return $locator->get('bar'); }, + ]); + + $locator->get('foo'); + } +} diff --git a/vendor/symfony/service-contracts/composer.json b/vendor/symfony/service-contracts/composer.json new file mode 100644 index 0000000..ad4105c --- /dev/null +++ b/vendor/symfony/service-contracts/composer.json @@ -0,0 +1,41 @@ +{ + "name": "symfony/service-contracts", + "type": "library", + "description": "Generic abstractions related to writing services", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "autoload": { + "psr-4": { "Symfony\\Contracts\\Service\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/vendor/symfony/string/AbstractString.php b/vendor/symfony/string/AbstractString.php new file mode 100644 index 0000000..8564430 --- /dev/null +++ b/vendor/symfony/string/AbstractString.php @@ -0,0 +1,716 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; +use Symfony\Component\String\Exception\RuntimeException; + +/** + * Represents a string of abstract characters. + * + * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). + * This class is the abstract type to use as a type-hint when the logic you want to + * implement doesn't care about the exact variant it deals with. + * + * @author Nicolas Grekas + * @author Hugo Hamon + * + * @throws ExceptionInterface + */ +abstract class AbstractString implements \Stringable, \JsonSerializable +{ + public const PREG_PATTERN_ORDER = \PREG_PATTERN_ORDER; + public const PREG_SET_ORDER = \PREG_SET_ORDER; + public const PREG_OFFSET_CAPTURE = \PREG_OFFSET_CAPTURE; + public const PREG_UNMATCHED_AS_NULL = \PREG_UNMATCHED_AS_NULL; + + public const PREG_SPLIT = 0; + public const PREG_SPLIT_NO_EMPTY = \PREG_SPLIT_NO_EMPTY; + public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE; + public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE; + + protected $string = ''; + protected $ignoreCase = false; + + abstract public function __construct(string $string = ''); + + /** + * Unwraps instances of AbstractString back to strings. + * + * @return string[]|array + */ + public static function unwrap(array $values): array + { + foreach ($values as $k => $v) { + if ($v instanceof self) { + $values[$k] = $v->__toString(); + } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) { + $values[$k] = $v; + } + } + + return $values; + } + + /** + * Wraps (and normalizes) strings in instances of AbstractString. + * + * @return static[]|array + */ + public static function wrap(array $values): array + { + $i = 0; + $keys = null; + + foreach ($values as $k => $v) { + if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) { + $keys = $keys ?? array_keys($values); + $keys[$i] = $j; + } + + if (\is_string($v)) { + $values[$k] = new static($v); + } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) { + $values[$k] = $v; + } + + ++$i; + } + + return null !== $keys ? array_combine($keys, $values) : $values; + } + + /** + * @param string|string[] $needle + */ + public function after(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static + { + $str = clone $this; + $i = \PHP_INT_MAX; + + if (\is_string($needle)) { + $needle = [$needle]; + } + + foreach ($needle as $n) { + $n = (string) $n; + $j = $this->indexOf($n, $offset); + + if (null !== $j && $j < $i) { + $i = $j; + $str->string = $n; + } + } + + if (\PHP_INT_MAX === $i) { + return $str; + } + + if (!$includeNeedle) { + $i += $str->length(); + } + + return $this->slice($i); + } + + /** + * @param string|string[] $needle + */ + public function afterLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static + { + $str = clone $this; + $i = null; + + if (\is_string($needle)) { + $needle = [$needle]; + } + + foreach ($needle as $n) { + $n = (string) $n; + $j = $this->indexOfLast($n, $offset); + + if (null !== $j && $j >= $i) { + $i = $offset = $j; + $str->string = $n; + } + } + + if (null === $i) { + return $str; + } + + if (!$includeNeedle) { + $i += $str->length(); + } + + return $this->slice($i); + } + + abstract public function append(string ...$suffix): static; + + /** + * @param string|string[] $needle + */ + public function before(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static + { + $str = clone $this; + $i = \PHP_INT_MAX; + + if (\is_string($needle)) { + $needle = [$needle]; + } + + foreach ($needle as $n) { + $n = (string) $n; + $j = $this->indexOf($n, $offset); + + if (null !== $j && $j < $i) { + $i = $j; + $str->string = $n; + } + } + + if (\PHP_INT_MAX === $i) { + return $str; + } + + if ($includeNeedle) { + $i += $str->length(); + } + + return $this->slice(0, $i); + } + + /** + * @param string|string[] $needle + */ + public function beforeLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static + { + $str = clone $this; + $i = null; + + if (\is_string($needle)) { + $needle = [$needle]; + } + + foreach ($needle as $n) { + $n = (string) $n; + $j = $this->indexOfLast($n, $offset); + + if (null !== $j && $j >= $i) { + $i = $offset = $j; + $str->string = $n; + } + } + + if (null === $i) { + return $str; + } + + if ($includeNeedle) { + $i += $str->length(); + } + + return $this->slice(0, $i); + } + + /** + * @return int[] + */ + public function bytesAt(int $offset): array + { + $str = $this->slice($offset, 1); + + return '' === $str->string ? [] : array_values(unpack('C*', $str->string)); + } + + abstract public function camel(): static; + + /** + * @return static[] + */ + abstract public function chunk(int $length = 1): array; + + public function collapseWhitespace(): static + { + $str = clone $this; + $str->string = trim(preg_replace('/(?:\s{2,}+|[^\S ])/', ' ', $str->string)); + + return $str; + } + + /** + * @param string|string[] $needle + */ + public function containsAny(string|iterable $needle): bool + { + return null !== $this->indexOf($needle); + } + + /** + * @param string|string[] $suffix + */ + public function endsWith(string|iterable $suffix): bool + { + if (\is_string($suffix)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + foreach ($suffix as $s) { + if ($this->endsWith((string) $s)) { + return true; + } + } + + return false; + } + + public function ensureEnd(string $suffix): static + { + if (!$this->endsWith($suffix)) { + return $this->append($suffix); + } + + $suffix = preg_quote($suffix); + $regex = '{('.$suffix.')(?:'.$suffix.')++$}D'; + + return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1'); + } + + public function ensureStart(string $prefix): static + { + $prefix = new static($prefix); + + if (!$this->startsWith($prefix)) { + return $this->prepend($prefix); + } + + $str = clone $this; + $i = $prefixLen = $prefix->length(); + + while ($this->indexOf($prefix, $i) === $i) { + $str = $str->slice($prefixLen); + $i += $prefixLen; + } + + return $str; + } + + /** + * @param string|string[] $string + */ + public function equalsTo(string|iterable $string): bool + { + if (\is_string($string)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + foreach ($string as $s) { + if ($this->equalsTo((string) $s)) { + return true; + } + } + + return false; + } + + abstract public function folded(): static; + + public function ignoreCase(): static + { + $str = clone $this; + $str->ignoreCase = true; + + return $str; + } + + /** + * @param string|string[] $needle + */ + public function indexOf(string|iterable $needle, int $offset = 0): ?int + { + if (\is_string($needle)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + $i = \PHP_INT_MAX; + + foreach ($needle as $n) { + $j = $this->indexOf((string) $n, $offset); + + if (null !== $j && $j < $i) { + $i = $j; + } + } + + return \PHP_INT_MAX === $i ? null : $i; + } + + /** + * @param string|string[] $needle + */ + public function indexOfLast(string|iterable $needle, int $offset = 0): ?int + { + if (\is_string($needle)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + $i = null; + + foreach ($needle as $n) { + $j = $this->indexOfLast((string) $n, $offset); + + if (null !== $j && $j >= $i) { + $i = $offset = $j; + } + } + + return $i; + } + + public function isEmpty(): bool + { + return '' === $this->string; + } + + abstract public function join(array $strings, string $lastGlue = null): static; + + public function jsonSerialize(): string + { + return $this->string; + } + + abstract public function length(): int; + + abstract public function lower(): static; + + /** + * Matches the string using a regular expression. + * + * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression. + * + * @return array All matches in a multi-dimensional array ordered according to flags + */ + abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array; + + abstract public function padBoth(int $length, string $padStr = ' '): static; + + abstract public function padEnd(int $length, string $padStr = ' '): static; + + abstract public function padStart(int $length, string $padStr = ' '): static; + + abstract public function prepend(string ...$prefix): static; + + public function repeat(int $multiplier): static + { + if (0 > $multiplier) { + throw new InvalidArgumentException(sprintf('Multiplier must be positive, %d given.', $multiplier)); + } + + $str = clone $this; + $str->string = str_repeat($str->string, $multiplier); + + return $str; + } + + abstract public function replace(string $from, string $to): static; + + abstract public function replaceMatches(string $fromRegexp, string|callable $to): static; + + abstract public function reverse(): static; + + abstract public function slice(int $start = 0, int $length = null): static; + + abstract public function snake(): static; + + abstract public function splice(string $replacement, int $start = 0, int $length = null): static; + + /** + * @return static[] + */ + public function split(string $delimiter, int $limit = null, int $flags = null): array + { + if (null === $flags) { + throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.'); + } + + if ($this->ignoreCase) { + $delimiter .= 'i'; + } + + set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); + + try { + if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) { + $lastError = preg_last_error(); + + foreach (get_defined_constants(true)['pcre'] as $k => $v) { + if ($lastError === $v && '_ERROR' === substr($k, -6)) { + throw new RuntimeException('Splitting failed with '.$k.'.'); + } + } + + throw new RuntimeException('Splitting failed with unknown error code.'); + } + } finally { + restore_error_handler(); + } + + $str = clone $this; + + if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) { + foreach ($chunks as &$chunk) { + $str->string = $chunk[0]; + $chunk[0] = clone $str; + } + } else { + foreach ($chunks as &$chunk) { + $str->string = $chunk; + $chunk = clone $str; + } + } + + return $chunks; + } + + /** + * @param string|string[] $prefix + */ + public function startsWith(string|iterable $prefix): bool + { + if (\is_string($prefix)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + foreach ($prefix as $prefix) { + if ($this->startsWith((string) $prefix)) { + return true; + } + } + + return false; + } + + abstract public function title(bool $allWords = false): static; + + public function toByteString(string $toEncoding = null): ByteString + { + $b = new ByteString(); + + $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding; + + if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') { + $b->string = $this->string; + + return $b; + } + + set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); + + try { + try { + $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8'); + } catch (InvalidArgumentException $e) { + if (!\function_exists('iconv')) { + throw $e; + } + + $b->string = iconv('UTF-8', $toEncoding, $this->string); + } + } finally { + restore_error_handler(); + } + + return $b; + } + + public function toCodePointString(): CodePointString + { + return new CodePointString($this->string); + } + + public function toString(): string + { + return $this->string; + } + + public function toUnicodeString(): UnicodeString + { + return new UnicodeString($this->string); + } + + abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; + + abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; + + /** + * @param string|string[] $prefix + */ + public function trimPrefix($prefix): static + { + if (\is_array($prefix) || $prefix instanceof \Traversable) { + foreach ($prefix as $s) { + $t = $this->trimPrefix($s); + + if ($t->string !== $this->string) { + return $t; + } + } + + return clone $this; + } + + $str = clone $this; + + if ($prefix instanceof self) { + $prefix = $prefix->string; + } else { + $prefix = (string) $prefix; + } + + if ('' !== $prefix && \strlen($this->string) >= \strlen($prefix) && 0 === substr_compare($this->string, $prefix, 0, \strlen($prefix), $this->ignoreCase)) { + $str->string = substr($this->string, \strlen($prefix)); + } + + return $str; + } + + abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; + + /** + * @param string|string[] $suffix + */ + public function trimSuffix($suffix): static + { + if (\is_array($suffix) || $suffix instanceof \Traversable) { + foreach ($suffix as $s) { + $t = $this->trimSuffix($s); + + if ($t->string !== $this->string) { + return $t; + } + } + + return clone $this; + } + + $str = clone $this; + + if ($suffix instanceof self) { + $suffix = $suffix->string; + } else { + $suffix = (string) $suffix; + } + + if ('' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase)) { + $str->string = substr($this->string, 0, -\strlen($suffix)); + } + + return $str; + } + + public function truncate(int $length, string $ellipsis = '', bool $cut = true): static + { + $stringLength = $this->length(); + + if ($stringLength <= $length) { + return clone $this; + } + + $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0; + + if ($length < $ellipsisLength) { + $ellipsisLength = 0; + } + + if (!$cut) { + if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) { + return clone $this; + } + + $length += $ellipsisLength; + } + + $str = $this->slice(0, $length - $ellipsisLength); + + return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str; + } + + abstract public function upper(): static; + + /** + * Returns the printable length on a terminal. + */ + abstract public function width(bool $ignoreAnsiDecoration = true): int; + + public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): static + { + $lines = '' !== $break ? $this->split($break) : [clone $this]; + $chars = []; + $mask = ''; + + if (1 === \count($lines) && '' === $lines[0]->string) { + return $lines[0]; + } + + foreach ($lines as $i => $line) { + if ($i) { + $chars[] = $break; + $mask .= '#'; + } + + foreach ($line->chunk() as $char) { + $chars[] = $char->string; + $mask .= ' ' === $char->string ? ' ' : '?'; + } + } + + $string = ''; + $j = 0; + $b = $i = -1; + $mask = wordwrap($mask, $width, '#', $cut); + + while (false !== $b = strpos($mask, '#', $b + 1)) { + for (++$i; $i < $b; ++$i) { + $string .= $chars[$j]; + unset($chars[$j++]); + } + + if ($break === $chars[$j] || ' ' === $chars[$j]) { + unset($chars[$j++]); + } + + $string .= $break; + } + + $str = clone $this; + $str->string = $string.implode('', $chars); + + return $str; + } + + public function __sleep(): array + { + return ['string']; + } + + public function __clone() + { + $this->ignoreCase = false; + } + + public function __toString(): string + { + return $this->string; + } +} diff --git a/vendor/symfony/string/AbstractUnicodeString.php b/vendor/symfony/string/AbstractUnicodeString.php new file mode 100644 index 0000000..2d4abab --- /dev/null +++ b/vendor/symfony/string/AbstractUnicodeString.php @@ -0,0 +1,603 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; +use Symfony\Component\String\Exception\RuntimeException; + +/** + * Represents a string of abstract Unicode characters. + * + * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). + * This class is the abstract type to use as a type-hint when the logic you want to + * implement is Unicode-aware but doesn't care about code points vs grapheme clusters. + * + * @author Nicolas Grekas + * + * @throws ExceptionInterface + */ +abstract class AbstractUnicodeString extends AbstractString +{ + public const NFC = \Normalizer::NFC; + public const NFD = \Normalizer::NFD; + public const NFKC = \Normalizer::NFKC; + public const NFKD = \Normalizer::NFKD; + + // all ASCII letters sorted by typical frequency of occurrence + private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; + + // the subset of folded case mappings that is not in lower case mappings + private const FOLD_FROM = ['İ', 'µ', 'ſ', "\xCD\x85", 'ς', 'ϐ', 'ϑ', 'ϕ', 'ϖ', 'ϰ', 'ϱ', 'ϵ', 'ẛ', "\xE1\xBE\xBE", 'ß', 'İ', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'և', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ẞ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'ᾐ', 'ᾑ', 'ᾒ', 'ᾓ', 'ᾔ', 'ᾕ', 'ᾖ', 'ᾗ', 'ᾘ', 'ᾙ', 'ᾚ', 'ᾛ', 'ᾜ', 'ᾝ', 'ᾞ', 'ᾟ', 'ᾠ', 'ᾡ', 'ᾢ', 'ᾣ', 'ᾤ', 'ᾥ', 'ᾦ', 'ᾧ', 'ᾨ', 'ᾩ', 'ᾪ', 'ᾫ', 'ᾬ', 'ᾭ', 'ᾮ', 'ᾯ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'ᾼ', 'ῂ', 'ῃ', 'ῄ', 'ῆ', 'ῇ', 'ῌ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'ῢ', 'ΰ', 'ῤ', 'ῦ', 'ῧ', 'ῲ', 'ῳ', 'ῴ', 'ῶ', 'ῷ', 'ῼ', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'ſt', 'st', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ']; + private const FOLD_TO = ['i̇', 'μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', 'ṡ', 'ι', 'ss', 'i̇', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'եւ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'aʾ', 'ss', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὰι', 'αι', 'άι', 'ᾶ', 'ᾶι', 'αι', 'ὴι', 'ηι', 'ήι', 'ῆ', 'ῆι', 'ηι', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'ῢ', 'ΰ', 'ῤ', 'ῦ', 'ῧ', 'ὼι', 'ωι', 'ώι', 'ῶ', 'ῶι', 'ωι', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'st', 'st', 'մն', 'մե', 'մի', 'վն', 'մխ']; + + // the subset of upper case mappings that map one code point to many code points + private const UPPER_FROM = ['ß', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'ſt', 'st', 'և', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ', 'ʼn', 'ΐ', 'ΰ', 'ǰ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾶ', 'ῆ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'ῢ', 'ΰ', 'ῤ', 'ῦ', 'ῧ', 'ῶ']; + private const UPPER_TO = ['SS', 'FF', 'FI', 'FL', 'FFI', 'FFL', 'ST', 'ST', 'ԵՒ', 'ՄՆ', 'ՄԵ', 'ՄԻ', 'ՎՆ', 'ՄԽ', 'ʼN', 'Ϊ́', 'Ϋ́', 'J̌', 'H̱', 'T̈', 'W̊', 'Y̊', 'Aʾ', 'Υ̓', 'Υ̓̀', 'Υ̓́', 'Υ̓͂', 'Α͂', 'Η͂', 'Ϊ̀', 'Ϊ́', 'Ι͂', 'Ϊ͂', 'Ϋ̀', 'Ϋ́', 'Ρ̓', 'Υ͂', 'Ϋ͂', 'Ω͂']; + + // the subset of https://github.com/unicode-org/cldr/blob/master/common/transforms/Latin-ASCII.xml that is not in NFKD + private const TRANSLIT_FROM = ['Æ', 'Ð', 'Ø', 'Þ', 'ß', 'æ', 'ð', 'ø', 'þ', 'Đ', 'đ', 'Ħ', 'ħ', 'ı', 'ĸ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'ʼn', 'Ŋ', 'ŋ', 'Œ', 'œ', 'Ŧ', 'ŧ', 'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƈ', 'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'ƕ', 'Ɩ', 'Ɨ', 'Ƙ', 'ƙ', 'ƚ', 'Ɲ', 'ƞ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'ƫ', 'Ƭ', 'ƭ', 'Ʈ', 'Ʋ', 'Ƴ', 'ƴ', 'Ƶ', 'ƶ', 'DŽ', 'Dž', 'dž', 'Ǥ', 'ǥ', 'ȡ', 'Ȥ', 'ȥ', 'ȴ', 'ȵ', 'ȶ', 'ȷ', 'ȸ', 'ȹ', 'Ⱥ', 'Ȼ', 'ȼ', 'Ƚ', 'Ⱦ', 'ȿ', 'ɀ', 'Ƀ', 'Ʉ', 'Ɇ', 'ɇ', 'Ɉ', 'ɉ', 'Ɍ', 'ɍ', 'Ɏ', 'ɏ', 'ɓ', 'ɕ', 'ɖ', 'ɗ', 'ɛ', 'ɟ', 'ɠ', 'ɡ', 'ɢ', 'ɦ', 'ɧ', 'ɨ', 'ɪ', 'ɫ', 'ɬ', 'ɭ', 'ɱ', 'ɲ', 'ɳ', 'ɴ', 'ɶ', 'ɼ', 'ɽ', 'ɾ', 'ʀ', 'ʂ', 'ʈ', 'ʉ', 'ʋ', 'ʏ', 'ʐ', 'ʑ', 'ʙ', 'ʛ', 'ʜ', 'ʝ', 'ʟ', 'ʠ', 'ʣ', 'ʥ', 'ʦ', 'ʪ', 'ʫ', 'ᴀ', 'ᴁ', 'ᴃ', 'ᴄ', 'ᴅ', 'ᴆ', 'ᴇ', 'ᴊ', 'ᴋ', 'ᴌ', 'ᴍ', 'ᴏ', 'ᴘ', 'ᴛ', 'ᴜ', 'ᴠ', 'ᴡ', 'ᴢ', 'ᵫ', 'ᵬ', 'ᵭ', 'ᵮ', 'ᵯ', 'ᵰ', 'ᵱ', 'ᵲ', 'ᵳ', 'ᵴ', 'ᵵ', 'ᵶ', 'ᵺ', 'ᵻ', 'ᵽ', 'ᵾ', 'ᶀ', 'ᶁ', 'ᶂ', 'ᶃ', 'ᶄ', 'ᶅ', 'ᶆ', 'ᶇ', 'ᶈ', 'ᶉ', 'ᶊ', 'ᶌ', 'ᶍ', 'ᶎ', 'ᶏ', 'ᶑ', 'ᶒ', 'ᶓ', 'ᶖ', 'ᶙ', 'ẚ', 'ẜ', 'ẝ', 'ẞ', 'Ỻ', 'ỻ', 'Ỽ', 'ỽ', 'Ỿ', 'ỿ', '©', '®', '₠', '₢', '₣', '₤', '₧', '₺', '₹', 'ℌ', '℞', '㎧', '㎮', '㏆', '㏗', '㏞', '㏟', '¼', '½', '¾', '⅓', '⅔', '⅕', '⅖', '⅗', '⅘', '⅙', '⅚', '⅛', '⅜', '⅝', '⅞', '⅟', '〇', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '′', '″', '〝', '〞', '«', '»', '‹', '›', '‐', '‑', '‒', '–', '—', '―', '︱', '︲', '﹘', '‖', '⁄', '⁅', '⁆', '⁎', '、', '。', '〈', '〉', '《', '》', '〔', '〕', '〘', '〙', '〚', '〛', '︑', '︒', '︹', '︺', '︽', '︾', '︿', '﹀', '﹑', '﹝', '﹞', '⦅', '⦆', '。', '、', '×', '÷', '−', '∕', '∖', '∣', '∥', '≪', '≫', '⦅', '⦆']; + private const TRANSLIT_TO = ['AE', 'D', 'O', 'TH', 'ss', 'ae', 'd', 'o', 'th', 'D', 'd', 'H', 'h', 'i', 'q', 'L', 'l', 'L', 'l', '\'n', 'N', 'n', 'OE', 'oe', 'T', 't', 'b', 'B', 'B', 'b', 'C', 'c', 'D', 'D', 'D', 'd', 'E', 'F', 'f', 'G', 'hv', 'I', 'I', 'K', 'k', 'l', 'N', 'n', 'OI', 'oi', 'P', 'p', 't', 'T', 't', 'T', 'V', 'Y', 'y', 'Z', 'z', 'DZ', 'Dz', 'dz', 'G', 'g', 'd', 'Z', 'z', 'l', 'n', 't', 'j', 'db', 'qp', 'A', 'C', 'c', 'L', 'T', 's', 'z', 'B', 'U', 'E', 'e', 'J', 'j', 'R', 'r', 'Y', 'y', 'b', 'c', 'd', 'd', 'e', 'j', 'g', 'g', 'G', 'h', 'h', 'i', 'I', 'l', 'l', 'l', 'm', 'n', 'n', 'N', 'OE', 'r', 'r', 'r', 'R', 's', 't', 'u', 'v', 'Y', 'z', 'z', 'B', 'G', 'H', 'j', 'L', 'q', 'dz', 'dz', 'ts', 'ls', 'lz', 'A', 'AE', 'B', 'C', 'D', 'D', 'E', 'J', 'K', 'L', 'M', 'O', 'P', 'T', 'U', 'V', 'W', 'Z', 'ue', 'b', 'd', 'f', 'm', 'n', 'p', 'r', 'r', 's', 't', 'z', 'th', 'I', 'p', 'U', 'b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 'v', 'x', 'z', 'a', 'd', 'e', 'e', 'i', 'u', 'a', 's', 's', 'SS', 'LL', 'll', 'V', 'v', 'Y', 'y', '(C)', '(R)', 'CE', 'Cr', 'Fr.', 'L.', 'Pts', 'TL', 'Rs', 'x', 'Rx', 'm/s', 'rad/s', 'C/kg', 'pH', 'V/m', 'A/m', ' 1/4', ' 1/2', ' 3/4', ' 1/3', ' 2/3', ' 1/5', ' 2/5', ' 3/5', ' 4/5', ' 1/6', ' 5/6', ' 1/8', ' 3/8', ' 5/8', ' 7/8', ' 1/', '0', '\'', '\'', ',', '\'', '"', '"', ',,', '"', '\'', '"', '"', '"', '<<', '>>', '<', '>', '-', '-', '-', '-', '-', '-', '-', '-', '-', '||', '/', '[', ']', '*', ',', '.', '<', '>', '<<', '>>', '[', ']', '[', ']', '[', ']', ',', '.', '[', ']', '<<', '>>', '<', '>', ',', '[', ']', '((', '))', '.', ',', '*', '/', '-', '/', '\\', '|', '||', '<<', '>>', '((', '))']; + + private static $transliterators = []; + private static $tableZero; + private static $tableWide; + + public static function fromCodePoints(int ...$codes): static + { + $string = ''; + + foreach ($codes as $code) { + if (0x80 > $code %= 0x200000) { + $string .= \chr($code); + } elseif (0x800 > $code) { + $string .= \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $string .= \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } else { + $string .= \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } + } + + return new static($string); + } + + /** + * Generic UTF-8 to ASCII transliteration. + * + * Install the intl extension for best results. + * + * @param string[]|\Transliterator[]|\Closure[] $rules See "*-Latin" rules from Transliterator::listIDs() + */ + public function ascii(array $rules = []): self + { + $str = clone $this; + $s = $str->string; + $str->string = ''; + + array_unshift($rules, 'nfd'); + $rules[] = 'latin-ascii'; + + if (\function_exists('transliterator_transliterate')) { + $rules[] = 'any-latin/bgn'; + } + + $rules[] = 'nfkd'; + $rules[] = '[:nonspacing mark:] remove'; + + while (\strlen($s) - 1 > $i = strspn($s, self::ASCII)) { + if (0 < --$i) { + $str->string .= substr($s, 0, $i); + $s = substr($s, $i); + } + + if (!$rule = array_shift($rules)) { + $rules = []; // An empty rule interrupts the next ones + } + + if ($rule instanceof \Transliterator) { + $s = $rule->transliterate($s); + } elseif ($rule instanceof \Closure) { + $s = $rule($s); + } elseif ($rule) { + if ('nfd' === $rule = strtolower($rule)) { + normalizer_is_normalized($s, self::NFD) ?: $s = normalizer_normalize($s, self::NFD); + } elseif ('nfkd' === $rule) { + normalizer_is_normalized($s, self::NFKD) ?: $s = normalizer_normalize($s, self::NFKD); + } elseif ('[:nonspacing mark:] remove' === $rule) { + $s = preg_replace('/\p{Mn}++/u', '', $s); + } elseif ('latin-ascii' === $rule) { + $s = str_replace(self::TRANSLIT_FROM, self::TRANSLIT_TO, $s); + } elseif ('de-ascii' === $rule) { + $s = preg_replace("/([AUO])\u{0308}(?=\p{Ll})/u", '$1e', $s); + $s = str_replace(["a\u{0308}", "o\u{0308}", "u\u{0308}", "A\u{0308}", "O\u{0308}", "U\u{0308}"], ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], $s); + } elseif (\function_exists('transliterator_transliterate')) { + if (null === $transliterator = self::$transliterators[$rule] ?? self::$transliterators[$rule] = \Transliterator::create($rule)) { + if ('any-latin/bgn' === $rule) { + $rule = 'any-latin'; + $transliterator = self::$transliterators[$rule] ?? self::$transliterators[$rule] = \Transliterator::create($rule); + } + + if (null === $transliterator) { + throw new InvalidArgumentException(sprintf('Unknown transliteration rule "%s".', $rule)); + } + + self::$transliterators['any-latin/bgn'] = $transliterator; + } + + $s = $transliterator->transliterate($s); + } + } elseif (!\function_exists('iconv')) { + $s = preg_replace('/[^\x00-\x7F]/u', '?', $s); + } else { + $s = @preg_replace_callback('/[^\x00-\x7F]/u', static function ($c) { + $c = (string) iconv('UTF-8', 'ASCII//TRANSLIT', $c[0]); + + if ('' === $c && '' === iconv('UTF-8', 'ASCII//TRANSLIT', '²')) { + throw new \LogicException(sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class)); + } + + return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : ('' !== $c ? $c : '?'); + }, $s); + } + } + + $str->string .= $s; + + return $str; + } + + public function camel(): static + { + $str = clone $this; + $str->string = str_replace(' ', '', preg_replace_callback('/\b./u', static function ($m) use (&$i) { + return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); + }, preg_replace('/[^\pL0-9]++/u', ' ', $this->string))); + + return $str; + } + + /** + * @return int[] + */ + public function codePointsAt(int $offset): array + { + $str = $this->slice($offset, 1); + + if ('' === $str->string) { + return []; + } + + $codePoints = []; + + foreach (preg_split('//u', $str->string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { + $codePoints[] = mb_ord($c, 'UTF-8'); + } + + return $codePoints; + } + + public function folded(bool $compat = true): static + { + $str = clone $this; + + if (!$compat || !\defined('Normalizer::NFKC_CF')) { + $str->string = normalizer_normalize($str->string, $compat ? \Normalizer::NFKC : \Normalizer::NFC); + $str->string = mb_strtolower(str_replace(self::FOLD_FROM, self::FOLD_TO, $this->string), 'UTF-8'); + } else { + $str->string = normalizer_normalize($str->string, \Normalizer::NFKC_CF); + } + + return $str; + } + + public function join(array $strings, string $lastGlue = null): static + { + $str = clone $this; + + $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; + $str->string = implode($this->string, $strings).$tail; + + if (!preg_match('//u', $str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function lower(): static + { + $str = clone $this; + $str->string = mb_strtolower(str_replace('İ', 'i̇', $str->string), 'UTF-8'); + + return $str; + } + + public function match(string $regexp, int $flags = 0, int $offset = 0): array + { + $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; + + if ($this->ignoreCase) { + $regexp .= 'i'; + } + + set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); + + try { + if (false === $match($regexp.'u', $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { + $lastError = preg_last_error(); + + foreach (get_defined_constants(true)['pcre'] as $k => $v) { + if ($lastError === $v && '_ERROR' === substr($k, -6)) { + throw new RuntimeException('Matching failed with '.$k.'.'); + } + } + + throw new RuntimeException('Matching failed with unknown error code.'); + } + } finally { + restore_error_handler(); + } + + return $matches; + } + + public function normalize(int $form = self::NFC): static + { + if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD])) { + throw new InvalidArgumentException('Unsupported normalization form.'); + } + + $str = clone $this; + normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); + + return $str; + } + + public function padBoth(int $length, string $padStr = ' '): static + { + if ('' === $padStr || !preg_match('//u', $padStr)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $pad = clone $this; + $pad->string = $padStr; + + return $this->pad($length, $pad, \STR_PAD_BOTH); + } + + public function padEnd(int $length, string $padStr = ' '): static + { + if ('' === $padStr || !preg_match('//u', $padStr)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $pad = clone $this; + $pad->string = $padStr; + + return $this->pad($length, $pad, \STR_PAD_RIGHT); + } + + public function padStart(int $length, string $padStr = ' '): static + { + if ('' === $padStr || !preg_match('//u', $padStr)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $pad = clone $this; + $pad->string = $padStr; + + return $this->pad($length, $pad, \STR_PAD_LEFT); + } + + public function replaceMatches(string $fromRegexp, string|callable $to): static + { + if ($this->ignoreCase) { + $fromRegexp .= 'i'; + } + + if (\is_array($to) || $to instanceof \Closure) { + $replace = 'preg_replace_callback'; + $to = static function (array $m) use ($to): string { + $to = $to($m); + + if ('' !== $to && (!\is_string($to) || !preg_match('//u', $to))) { + throw new InvalidArgumentException('Replace callback must return a valid UTF-8 string.'); + } + + return $to; + }; + } elseif ('' !== $to && !preg_match('//u', $to)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } else { + $replace = 'preg_replace'; + } + + set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); + + try { + if (null === $string = $replace($fromRegexp.'u', $to, $this->string)) { + $lastError = preg_last_error(); + + foreach (get_defined_constants(true)['pcre'] as $k => $v) { + if ($lastError === $v && '_ERROR' === substr($k, -6)) { + throw new RuntimeException('Matching failed with '.$k.'.'); + } + } + + throw new RuntimeException('Matching failed with unknown error code.'); + } + } finally { + restore_error_handler(); + } + + $str = clone $this; + $str->string = $string; + + return $str; + } + + public function reverse(): static + { + $str = clone $this; + $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY))); + + return $str; + } + + public function snake(): static + { + $str = $this->camel()->title(); + $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8'); + + return $str; + } + + public function title(bool $allWords = false): static + { + $str = clone $this; + + $limit = $allWords ? -1 : 1; + + $str->string = preg_replace_callback('/\b./u', static function (array $m): string { + return mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); + }, $str->string, $limit); + + return $str; + } + + public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static + { + if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { + throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } + $chars = preg_quote($chars); + + $str = clone $this; + $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string); + + return $str; + } + + public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static + { + if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { + throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } + $chars = preg_quote($chars); + + $str = clone $this; + $str->string = preg_replace("{[$chars]++$}uD", '', $str->string); + + return $str; + } + + public function trimPrefix($prefix): static + { + if (!$this->ignoreCase) { + return parent::trimPrefix($prefix); + } + + $str = clone $this; + + if ($prefix instanceof \Traversable) { + $prefix = iterator_to_array($prefix, false); + } elseif ($prefix instanceof parent) { + $prefix = $prefix->string; + } + + $prefix = implode('|', array_map('preg_quote', (array) $prefix)); + $str->string = preg_replace("{^(?:$prefix)}iuD", '', $this->string); + + return $str; + } + + public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static + { + if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { + throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } + $chars = preg_quote($chars); + + $str = clone $this; + $str->string = preg_replace("{^[$chars]++}uD", '', $str->string); + + return $str; + } + + public function trimSuffix($suffix): static + { + if (!$this->ignoreCase) { + return parent::trimSuffix($suffix); + } + + $str = clone $this; + + if ($suffix instanceof \Traversable) { + $suffix = iterator_to_array($suffix, false); + } elseif ($suffix instanceof parent) { + $suffix = $suffix->string; + } + + $suffix = implode('|', array_map('preg_quote', (array) $suffix)); + $str->string = preg_replace("{(?:$suffix)$}iuD", '', $this->string); + + return $str; + } + + public function upper(): static + { + $str = clone $this; + $str->string = mb_strtoupper($str->string, 'UTF-8'); + + return $str; + } + + public function width(bool $ignoreAnsiDecoration = true): int + { + $width = 0; + $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string); + + if (false !== strpos($s, "\r")) { + $s = str_replace(["\r\n", "\r"], "\n", $s); + } + + if (!$ignoreAnsiDecoration) { + $s = preg_replace('/[\p{Cc}\x7F]++/u', '', $s); + } + + foreach (explode("\n", $s) as $s) { + if ($ignoreAnsiDecoration) { + $s = preg_replace('/(?:\x1B(?: + \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [0x40-\x7E] + | [P\]X^_] .*? \x1B\\\\ + | [\x41-\x7E] + )|[\p{Cc}\x7F]++)/xu', '', $s); + } + + // Non printable characters have been dropped, so wcswidth cannot logically return -1. + $width += $this->wcswidth($s); + } + + return $width; + } + + private function pad(int $len, self $pad, int $type): static + { + $sLen = $this->length(); + + if ($len <= $sLen) { + return clone $this; + } + + $padLen = $pad->length(); + $freeLen = $len - $sLen; + $len = $freeLen % $padLen; + + switch ($type) { + case \STR_PAD_RIGHT: + return $this->append(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); + + case \STR_PAD_LEFT: + return $this->prepend(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); + + case \STR_PAD_BOTH: + $freeLen /= 2; + + $rightLen = ceil($freeLen); + $len = $rightLen % $padLen; + $str = $this->append(str_repeat($pad->string, intdiv($rightLen, $padLen)).($len ? $pad->slice(0, $len) : '')); + + $leftLen = floor($freeLen); + $len = $leftLen % $padLen; + + return $str->prepend(str_repeat($pad->string, intdiv($leftLen, $padLen)).($len ? $pad->slice(0, $len) : '')); + + default: + throw new InvalidArgumentException('Invalid padding type.'); + } + } + + /** + * Based on https://github.com/jquast/wcwidth, a Python implementation of https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c. + */ + private function wcswidth(string $string): int + { + $width = 0; + + foreach (preg_split('//u', $string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { + $codePoint = mb_ord($c, 'UTF-8'); + + if (0 === $codePoint // NULL + || 0x034F === $codePoint // COMBINING GRAPHEME JOINER + || (0x200B <= $codePoint && 0x200F >= $codePoint) // ZERO WIDTH SPACE to RIGHT-TO-LEFT MARK + || 0x2028 === $codePoint // LINE SEPARATOR + || 0x2029 === $codePoint // PARAGRAPH SEPARATOR + || (0x202A <= $codePoint && 0x202E >= $codePoint) // LEFT-TO-RIGHT EMBEDDING to RIGHT-TO-LEFT OVERRIDE + || (0x2060 <= $codePoint && 0x2063 >= $codePoint) // WORD JOINER to INVISIBLE SEPARATOR + ) { + continue; + } + + // Non printable characters + if (32 > $codePoint // C0 control characters + || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL + ) { + return -1; + } + + if (null === self::$tableZero) { + self::$tableZero = require __DIR__.'/Resources/data/wcswidth_table_zero.php'; + } + + if ($codePoint >= self::$tableZero[0][0] && $codePoint <= self::$tableZero[$ubound = \count(self::$tableZero) - 1][1]) { + $lbound = 0; + while ($ubound >= $lbound) { + $mid = floor(($lbound + $ubound) / 2); + + if ($codePoint > self::$tableZero[$mid][1]) { + $lbound = $mid + 1; + } elseif ($codePoint < self::$tableZero[$mid][0]) { + $ubound = $mid - 1; + } else { + continue 2; + } + } + } + + if (null === self::$tableWide) { + self::$tableWide = require __DIR__.'/Resources/data/wcswidth_table_wide.php'; + } + + if ($codePoint >= self::$tableWide[0][0] && $codePoint <= self::$tableWide[$ubound = \count(self::$tableWide) - 1][1]) { + $lbound = 0; + while ($ubound >= $lbound) { + $mid = floor(($lbound + $ubound) / 2); + + if ($codePoint > self::$tableWide[$mid][1]) { + $lbound = $mid + 1; + } elseif ($codePoint < self::$tableWide[$mid][0]) { + $ubound = $mid - 1; + } else { + $width += 2; + + continue 2; + } + } + } + + ++$width; + } + + return $width; + } +} diff --git a/vendor/symfony/string/ByteString.php b/vendor/symfony/string/ByteString.php new file mode 100644 index 0000000..b3649b6 --- /dev/null +++ b/vendor/symfony/string/ByteString.php @@ -0,0 +1,490 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; +use Symfony\Component\String\Exception\RuntimeException; + +/** + * Represents a binary-safe string of bytes. + * + * @author Nicolas Grekas + * @author Hugo Hamon + * + * @throws ExceptionInterface + */ +class ByteString extends AbstractString +{ + private const ALPHABET_ALPHANUMERIC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + + public function __construct(string $string = '') + { + $this->string = $string; + } + + /* + * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03) + * + * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16 + * + * Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE). + * + * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/) + */ + + public static function fromRandom(int $length = 16, string $alphabet = null): self + { + if ($length <= 0) { + throw new InvalidArgumentException(sprintf('A strictly positive length is expected, "%d" given.', $length)); + } + + $alphabet = $alphabet ?? self::ALPHABET_ALPHANUMERIC; + $alphabetSize = \strlen($alphabet); + $bits = (int) ceil(log($alphabetSize, 2.0)); + if ($bits <= 0 || $bits > 56) { + throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.'); + } + + $ret = ''; + while ($length > 0) { + $urandomLength = (int) ceil(2 * $length * $bits / 8.0); + $data = random_bytes($urandomLength); + $unpackedData = 0; + $unpackedBits = 0; + for ($i = 0; $i < $urandomLength && $length > 0; ++$i) { + // Unpack 8 bits + $unpackedData = ($unpackedData << 8) | \ord($data[$i]); + $unpackedBits += 8; + + // While we have enough bits to select a character from the alphabet, keep + // consuming the random data + for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) { + $index = ($unpackedData & ((1 << $bits) - 1)); + $unpackedData >>= $bits; + // Unfortunately, the alphabet size is not necessarily a power of two. + // Worst case, it is 2^k + 1, which means we need (k+1) bits and we + // have around a 50% chance of missing as k gets larger + if ($index < $alphabetSize) { + $ret .= $alphabet[$index]; + --$length; + } + } + } + } + + return new static($ret); + } + + public function bytesAt(int $offset): array + { + $str = $this->string[$offset] ?? ''; + + return '' === $str ? [] : [\ord($str)]; + } + + public function append(string ...$suffix): static + { + $str = clone $this; + $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); + + return $str; + } + + public function camel(): static + { + $str = clone $this; + $str->string = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string)))); + + return $str; + } + + public function chunk(int $length = 1): array + { + if (1 > $length) { + throw new InvalidArgumentException('The chunk length must be greater than zero.'); + } + + if ('' === $this->string) { + return []; + } + + $str = clone $this; + $chunks = []; + + foreach (str_split($this->string, $length) as $chunk) { + $str->string = $chunk; + $chunks[] = clone $str; + } + + return $chunks; + } + + public function endsWith(string|iterable|AbstractString $suffix): bool + { + if ($suffix instanceof AbstractString) { + $suffix = $suffix->string; + } elseif (!\is_string($suffix)) { + return parent::endsWith($suffix); + } + + return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase); + } + + public function equalsTo(string|iterable|AbstractString $string): bool + { + if ($string instanceof AbstractString) { + $string = $string->string; + } elseif (!\is_string($string)) { + return parent::equalsTo($string); + } + + if ('' !== $string && $this->ignoreCase) { + return 0 === strcasecmp($string, $this->string); + } + + return $string === $this->string; + } + + public function folded(): static + { + $str = clone $this; + $str->string = strtolower($str->string); + + return $str; + } + + public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOf($needle, $offset); + } + + if ('' === $needle) { + return null; + } + + $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset); + + return false === $i ? null : $i; + } + + public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOfLast($needle, $offset); + } + + if ('' === $needle) { + return null; + } + + $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset); + + return false === $i ? null : $i; + } + + public function isUtf8(): bool + { + return '' === $this->string || preg_match('//u', $this->string); + } + + public function join(array $strings, string $lastGlue = null): static + { + $str = clone $this; + + $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; + $str->string = implode($this->string, $strings).$tail; + + return $str; + } + + public function length(): int + { + return \strlen($this->string); + } + + public function lower(): static + { + $str = clone $this; + $str->string = strtolower($str->string); + + return $str; + } + + public function match(string $regexp, int $flags = 0, int $offset = 0): array + { + $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; + + if ($this->ignoreCase) { + $regexp .= 'i'; + } + + set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); + + try { + if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { + $lastError = preg_last_error(); + + foreach (get_defined_constants(true)['pcre'] as $k => $v) { + if ($lastError === $v && '_ERROR' === substr($k, -6)) { + throw new RuntimeException('Matching failed with '.$k.'.'); + } + } + + throw new RuntimeException('Matching failed with unknown error code.'); + } + } finally { + restore_error_handler(); + } + + return $matches; + } + + public function padBoth(int $length, string $padStr = ' '): static + { + $str = clone $this; + $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH); + + return $str; + } + + public function padEnd(int $length, string $padStr = ' '): static + { + $str = clone $this; + $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT); + + return $str; + } + + public function padStart(int $length, string $padStr = ' '): static + { + $str = clone $this; + $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT); + + return $str; + } + + public function prepend(string ...$prefix): static + { + $str = clone $this; + $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string; + + return $str; + } + + public function replace(string $from, string $to): static + { + $str = clone $this; + + if ('' !== $from) { + $str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string); + } + + return $str; + } + + public function replaceMatches(string $fromRegexp, string|callable $to): static + { + if ($this->ignoreCase) { + $fromRegexp .= 'i'; + } + + $replace = \is_array($to) || $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace'; + + set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); + + try { + if (null === $string = $replace($fromRegexp, $to, $this->string)) { + $lastError = preg_last_error(); + + foreach (get_defined_constants(true)['pcre'] as $k => $v) { + if ($lastError === $v && '_ERROR' === substr($k, -6)) { + throw new RuntimeException('Matching failed with '.$k.'.'); + } + } + + throw new RuntimeException('Matching failed with unknown error code.'); + } + } finally { + restore_error_handler(); + } + + $str = clone $this; + $str->string = $string; + + return $str; + } + + public function reverse(): static + { + $str = clone $this; + $str->string = strrev($str->string); + + return $str; + } + + public function slice(int $start = 0, int $length = null): static + { + $str = clone $this; + $str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX); + + return $str; + } + + public function snake(): static + { + $str = $this->camel()->title(); + $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); + + return $str; + } + + public function splice(string $replacement, int $start = 0, int $length = null): static + { + $str = clone $this; + $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); + + return $str; + } + + public function split(string $delimiter, int $limit = null, int $flags = null): array + { + if (1 > $limit = $limit ?? \PHP_INT_MAX) { + throw new InvalidArgumentException('Split limit must be a positive integer.'); + } + + if ('' === $delimiter) { + throw new InvalidArgumentException('Split delimiter is empty.'); + } + + if (null !== $flags) { + return parent::split($delimiter, $limit, $flags); + } + + $str = clone $this; + $chunks = $this->ignoreCase + ? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit) + : explode($delimiter, $this->string, $limit); + + foreach ($chunks as &$chunk) { + $str->string = $chunk; + $chunk = clone $str; + } + + return $chunks; + } + + public function startsWith(string|iterable|AbstractString $prefix): bool + { + if ($prefix instanceof AbstractString) { + $prefix = $prefix->string; + } elseif (!\is_string($prefix)) { + return parent::startsWith($prefix); + } + + return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix))); + } + + public function title(bool $allWords = false): static + { + $str = clone $this; + $str->string = $allWords ? ucwords($str->string) : ucfirst($str->string); + + return $str; + } + + public function toUnicodeString(string $fromEncoding = null): UnicodeString + { + return new UnicodeString($this->toCodePointString($fromEncoding)->string); + } + + public function toCodePointString(string $fromEncoding = null): CodePointString + { + $u = new CodePointString(); + + if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) { + $u->string = $this->string; + + return $u; + } + + set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); + + try { + try { + $validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true); + } catch (InvalidArgumentException $e) { + if (!\function_exists('iconv')) { + throw $e; + } + + $u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string); + + return $u; + } + } finally { + restore_error_handler(); + } + + if (!$validEncoding) { + throw new InvalidArgumentException(sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252')); + } + + $u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252'); + + return $u; + } + + public function trim(string $chars = " \t\n\r\0\x0B\x0C"): static + { + $str = clone $this; + $str->string = trim($str->string, $chars); + + return $str; + } + + public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): static + { + $str = clone $this; + $str->string = rtrim($str->string, $chars); + + return $str; + } + + public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): static + { + $str = clone $this; + $str->string = ltrim($str->string, $chars); + + return $str; + } + + public function upper(): static + { + $str = clone $this; + $str->string = strtoupper($str->string); + + return $str; + } + + public function width(bool $ignoreAnsiDecoration = true): int + { + $string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string); + + return (new CodePointString($string))->width($ignoreAnsiDecoration); + } +} diff --git a/vendor/symfony/string/CHANGELOG.md b/vendor/symfony/string/CHANGELOG.md new file mode 100644 index 0000000..53af364 --- /dev/null +++ b/vendor/symfony/string/CHANGELOG.md @@ -0,0 +1,35 @@ +CHANGELOG +========= + +5.4 +--- + + * Add `trimSuffix()` and `trimPrefix()` methods + +5.3 +--- + + * Made `AsciiSlugger` fallback to parent locale's symbolsMap + +5.2.0 +----- + + * added a `FrenchInflector` class + +5.1.0 +----- + + * added the `AbstractString::reverse()` method + * made `AbstractString::width()` follow POSIX.1-2001 + * added `LazyString` which provides memoizing stringable objects + * The component is not marked as `@experimental` anymore + * added the `s()` helper method to get either an `UnicodeString` or `ByteString` instance, + depending of the input string UTF-8 compliancy + * added `$cut` parameter to `Symfony\Component\String\AbstractString::truncate()` + * added `AbstractString::containsAny()` + * allow passing a string of custom characters to `ByteString::fromRandom()` + +5.0.0 +----- + + * added the component as experimental diff --git a/vendor/symfony/string/CodePointString.php b/vendor/symfony/string/CodePointString.php new file mode 100644 index 0000000..926ff79 --- /dev/null +++ b/vendor/symfony/string/CodePointString.php @@ -0,0 +1,260 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; + +/** + * Represents a string of Unicode code points encoded as UTF-8. + * + * @author Nicolas Grekas + * @author Hugo Hamon + * + * @throws ExceptionInterface + */ +class CodePointString extends AbstractUnicodeString +{ + public function __construct(string $string = '') + { + if ('' !== $string && !preg_match('//u', $string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $this->string = $string; + } + + public function append(string ...$suffix): static + { + $str = clone $this; + $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); + + if (!preg_match('//u', $str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function chunk(int $length = 1): array + { + if (1 > $length) { + throw new InvalidArgumentException('The chunk length must be greater than zero.'); + } + + if ('' === $this->string) { + return []; + } + + $rx = '/('; + while (65535 < $length) { + $rx .= '.{65535}'; + $length -= 65535; + } + $rx .= '.{'.$length.'})/us'; + + $str = clone $this; + $chunks = []; + + foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { + $str->string = $chunk; + $chunks[] = clone $str; + } + + return $chunks; + } + + public function codePointsAt(int $offset): array + { + $str = $offset ? $this->slice($offset, 1) : $this; + + return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')]; + } + + public function endsWith(string|iterable|AbstractString $suffix): bool + { + if ($suffix instanceof AbstractString) { + $suffix = $suffix->string; + } elseif (!\is_string($suffix)) { + return parent::endsWith($suffix); + } + + if ('' === $suffix || !preg_match('//u', $suffix)) { + return false; + } + + if ($this->ignoreCase) { + return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string); + } + + return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix)); + } + + public function equalsTo(string|iterable|AbstractString $string): bool + { + if ($string instanceof AbstractString) { + $string = $string->string; + } elseif (!\is_string($string)) { + return parent::equalsTo($string); + } + + if ('' !== $string && $this->ignoreCase) { + return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); + } + + return $string === $this->string; + } + + public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOf($needle, $offset); + } + + if ('' === $needle) { + return null; + } + + $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8'); + + return false === $i ? null : $i; + } + + public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOfLast($needle, $offset); + } + + if ('' === $needle) { + return null; + } + + $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8'); + + return false === $i ? null : $i; + } + + public function length(): int + { + return mb_strlen($this->string, 'UTF-8'); + } + + public function prepend(string ...$prefix): static + { + $str = clone $this; + $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; + + if (!preg_match('//u', $str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function replace(string $from, string $to): static + { + $str = clone $this; + + if ('' === $from || !preg_match('//u', $from)) { + return $str; + } + + if ('' !== $to && !preg_match('//u', $to)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + if ($this->ignoreCase) { + $str->string = implode($to, preg_split('{'.preg_quote($from).'}iuD', $this->string)); + } else { + $str->string = str_replace($from, $to, $this->string); + } + + return $str; + } + + public function slice(int $start = 0, int $length = null): static + { + $str = clone $this; + $str->string = mb_substr($this->string, $start, $length, 'UTF-8'); + + return $str; + } + + public function splice(string $replacement, int $start = 0, int $length = null): static + { + if (!preg_match('//u', $replacement)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $str = clone $this; + $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0; + $length = $length ? \strlen(mb_substr($this->string, $start, $length, 'UTF-8')) : $length; + $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); + + return $str; + } + + public function split(string $delimiter, int $limit = null, int $flags = null): array + { + if (1 > $limit = $limit ?? \PHP_INT_MAX) { + throw new InvalidArgumentException('Split limit must be a positive integer.'); + } + + if ('' === $delimiter) { + throw new InvalidArgumentException('Split delimiter is empty.'); + } + + if (null !== $flags) { + return parent::split($delimiter.'u', $limit, $flags); + } + + if (!preg_match('//u', $delimiter)) { + throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); + } + + $str = clone $this; + $chunks = $this->ignoreCase + ? preg_split('{'.preg_quote($delimiter).'}iuD', $this->string, $limit) + : explode($delimiter, $this->string, $limit); + + foreach ($chunks as &$chunk) { + $str->string = $chunk; + $chunk = clone $str; + } + + return $chunks; + } + + public function startsWith(string|iterable|AbstractString $prefix): bool + { + if ($prefix instanceof AbstractString) { + $prefix = $prefix->string; + } elseif (!\is_string($prefix)) { + return parent::startsWith($prefix); + } + + if ('' === $prefix || !preg_match('//u', $prefix)) { + return false; + } + + if ($this->ignoreCase) { + return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8'); + } + + return 0 === strncmp($this->string, $prefix, \strlen($prefix)); + } +} diff --git a/vendor/symfony/string/Exception/ExceptionInterface.php b/vendor/symfony/string/Exception/ExceptionInterface.php new file mode 100644 index 0000000..3619786 --- /dev/null +++ b/vendor/symfony/string/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Exception; + +interface ExceptionInterface extends \Throwable +{ +} diff --git a/vendor/symfony/string/Exception/InvalidArgumentException.php b/vendor/symfony/string/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..6aa586b --- /dev/null +++ b/vendor/symfony/string/Exception/InvalidArgumentException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Exception; + +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/string/Exception/RuntimeException.php b/vendor/symfony/string/Exception/RuntimeException.php new file mode 100644 index 0000000..77cb091 --- /dev/null +++ b/vendor/symfony/string/Exception/RuntimeException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Exception; + +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/string/Inflector/EnglishInflector.php b/vendor/symfony/string/Inflector/EnglishInflector.php new file mode 100644 index 0000000..9f2fac6 --- /dev/null +++ b/vendor/symfony/string/Inflector/EnglishInflector.php @@ -0,0 +1,511 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Inflector; + +final class EnglishInflector implements InflectorInterface +{ + /** + * Map English plural to singular suffixes. + * + * @see http://english-zone.com/spelling/plurals.html + */ + private const PLURAL_MAP = [ + // First entry: plural suffix, reversed + // Second entry: length of plural suffix + // Third entry: Whether the suffix may succeed a vocal + // Fourth entry: Whether the suffix may succeed a consonant + // Fifth entry: singular suffix, normal + + // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) + ['a', 1, true, true, ['on', 'um']], + + // nebulae (nebula) + ['ea', 2, true, true, 'a'], + + // services (service) + ['secivres', 8, true, true, 'service'], + + // mice (mouse), lice (louse) + ['eci', 3, false, true, 'ouse'], + + // geese (goose) + ['esee', 4, false, true, 'oose'], + + // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) + ['i', 1, true, true, 'us'], + + // men (man), women (woman) + ['nem', 3, true, true, 'man'], + + // children (child) + ['nerdlihc', 8, true, true, 'child'], + + // oxen (ox) + ['nexo', 4, false, false, 'ox'], + + // indices (index), appendices (appendix), prices (price) + ['seci', 4, false, true, ['ex', 'ix', 'ice']], + + // selfies (selfie) + ['seifles', 7, true, true, 'selfie'], + + // zombies (zombie) + ['seibmoz', 7, true, true, 'zombie'], + + // movies (movie) + ['seivom', 6, true, true, 'movie'], + + // conspectuses (conspectus), prospectuses (prospectus) + ['sesutcep', 8, true, true, 'pectus'], + + // feet (foot) + ['teef', 4, true, true, 'foot'], + + // geese (goose) + ['eseeg', 5, true, true, 'goose'], + + // teeth (tooth) + ['hteet', 5, true, true, 'tooth'], + + // news (news) + ['swen', 4, true, true, 'news'], + + // series (series) + ['seires', 6, true, true, 'series'], + + // babies (baby) + ['sei', 3, false, true, 'y'], + + // accesses (access), addresses (address), kisses (kiss) + ['sess', 4, true, false, 'ss'], + + // analyses (analysis), ellipses (ellipsis), fungi (fungus), + // neuroses (neurosis), theses (thesis), emphases (emphasis), + // oases (oasis), crises (crisis), houses (house), bases (base), + // atlases (atlas) + ['ses', 3, true, true, ['s', 'se', 'sis']], + + // objectives (objective), alternative (alternatives) + ['sevit', 5, true, true, 'tive'], + + // drives (drive) + ['sevird', 6, false, true, 'drive'], + + // lives (life), wives (wife) + ['sevi', 4, false, true, 'ife'], + + // moves (move) + ['sevom', 5, true, true, 'move'], + + // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff) + ['sev', 3, true, true, ['f', 've', 'ff']], + + // axes (axis), axes (ax), axes (axe) + ['sexa', 4, false, false, ['ax', 'axe', 'axis']], + + // indexes (index), matrixes (matrix) + ['sex', 3, true, false, 'x'], + + // quizzes (quiz) + ['sezz', 4, true, false, 'z'], + + // bureaus (bureau) + ['suae', 4, false, true, 'eau'], + + // fees (fee), trees (tree), employees (employee) + ['see', 3, true, true, 'ee'], + + // edges (edge) + ['segd', 4, true, true, 'dge'], + + // roses (rose), garages (garage), cassettes (cassette), + // waltzes (waltz), heroes (hero), bushes (bush), arches (arch), + // shoes (shoe) + ['se', 2, true, true, ['', 'e']], + + // tags (tag) + ['s', 1, true, true, ''], + + // chateaux (chateau) + ['xuae', 4, false, true, 'eau'], + + // people (person) + ['elpoep', 6, true, true, 'person'], + ]; + + /** + * Map English singular to plural suffixes. + * + * @see http://english-zone.com/spelling/plurals.html + */ + private const SINGULAR_MAP = [ + // First entry: singular suffix, reversed + // Second entry: length of singular suffix + // Third entry: Whether the suffix may succeed a vocal + // Fourth entry: Whether the suffix may succeed a consonant + // Fifth entry: plural suffix, normal + + // criterion (criteria) + ['airetirc', 8, false, false, 'criterion'], + + // nebulae (nebula) + ['aluben', 6, false, false, 'nebulae'], + + // children (child) + ['dlihc', 5, true, true, 'children'], + + // prices (price) + ['eci', 3, false, true, 'ices'], + + // services (service) + ['ecivres', 7, true, true, 'services'], + + // lives (life), wives (wife) + ['efi', 3, false, true, 'ives'], + + // selfies (selfie) + ['eifles', 6, true, true, 'selfies'], + + // movies (movie) + ['eivom', 5, true, true, 'movies'], + + // lice (louse) + ['esuol', 5, false, true, 'lice'], + + // mice (mouse) + ['esuom', 5, false, true, 'mice'], + + // geese (goose) + ['esoo', 4, false, true, 'eese'], + + // houses (house), bases (base) + ['es', 2, true, true, 'ses'], + + // geese (goose) + ['esoog', 5, true, true, 'geese'], + + // caves (cave) + ['ev', 2, true, true, 'ves'], + + // drives (drive) + ['evird', 5, false, true, 'drives'], + + // objectives (objective), alternative (alternatives) + ['evit', 4, true, true, 'tives'], + + // moves (move) + ['evom', 4, true, true, 'moves'], + + // staves (staff) + ['ffats', 5, true, true, 'staves'], + + // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) + ['ff', 2, true, true, 'ffs'], + + // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) + ['f', 1, true, true, ['fs', 'ves']], + + // arches (arch) + ['hc', 2, true, true, 'ches'], + + // bushes (bush) + ['hs', 2, true, true, 'shes'], + + // teeth (tooth) + ['htoot', 5, true, true, 'teeth'], + + // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) + ['mu', 2, true, true, 'a'], + + // men (man), women (woman) + ['nam', 3, true, true, 'men'], + + // people (person) + ['nosrep', 6, true, true, ['persons', 'people']], + + // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) + ['noi', 3, true, true, 'ions'], + + // coupon (coupons) + ['nop', 3, true, true, 'pons'], + + // seasons (season), treasons (treason), poisons (poison), lessons (lesson) + ['nos', 3, true, true, 'sons'], + + // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) + ['no', 2, true, true, 'a'], + + // echoes (echo) + ['ohce', 4, true, true, 'echoes'], + + // heroes (hero) + ['oreh', 4, true, true, 'heroes'], + + // atlases (atlas) + ['salta', 5, true, true, 'atlases'], + + // irises (iris) + ['siri', 4, true, true, 'irises'], + + // analyses (analysis), ellipses (ellipsis), neuroses (neurosis) + // theses (thesis), emphases (emphasis), oases (oasis), + // crises (crisis) + ['sis', 3, true, true, 'ses'], + + // accesses (access), addresses (address), kisses (kiss) + ['ss', 2, true, false, 'sses'], + + // syllabi (syllabus) + ['suballys', 8, true, true, 'syllabi'], + + // buses (bus) + ['sub', 3, true, true, 'buses'], + + // circuses (circus) + ['suc', 3, true, true, 'cuses'], + + // conspectuses (conspectus), prospectuses (prospectus) + ['sutcep', 6, true, true, 'pectuses'], + + // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) + ['su', 2, true, true, 'i'], + + // news (news) + ['swen', 4, true, true, 'news'], + + // feet (foot) + ['toof', 4, true, true, 'feet'], + + // chateaux (chateau), bureaus (bureau) + ['uae', 3, false, true, ['eaus', 'eaux']], + + // oxen (ox) + ['xo', 2, false, false, 'oxen'], + + // hoaxes (hoax) + ['xaoh', 4, true, false, 'hoaxes'], + + // indices (index) + ['xedni', 5, false, true, ['indicies', 'indexes']], + + // boxes (box) + ['xo', 2, false, true, 'oxes'], + + // indexes (index), matrixes (matrix) + ['x', 1, true, false, ['cies', 'xes']], + + // appendices (appendix) + ['xi', 2, false, true, 'ices'], + + // babies (baby) + ['y', 1, false, true, 'ies'], + + // quizzes (quiz) + ['ziuq', 4, true, false, 'quizzes'], + + // waltzes (waltz) + ['z', 1, true, true, 'zes'], + ]; + + /** + * A list of words which should not be inflected, reversed. + */ + private const UNINFLECTED = [ + '', + + // data + 'atad', + + // deer + 'reed', + + // feedback + 'kcabdeef', + + // fish + 'hsif', + + // info + 'ofni', + + // moose + 'esoom', + + // series + 'seires', + + // sheep + 'peehs', + + // species + 'seiceps', + ]; + + /** + * {@inheritdoc} + */ + public function singularize(string $plural): array + { + $pluralRev = strrev($plural); + $lowerPluralRev = strtolower($pluralRev); + $pluralLength = \strlen($lowerPluralRev); + + // Check if the word is one which is not inflected, return early if so + if (\in_array($lowerPluralRev, self::UNINFLECTED, true)) { + return [$plural]; + } + + // The outer loop iterates over the entries of the plural table + // The inner loop $j iterates over the characters of the plural suffix + // in the plural table to compare them with the characters of the actual + // given plural suffix + foreach (self::PLURAL_MAP as $map) { + $suffix = $map[0]; + $suffixLength = $map[1]; + $j = 0; + + // Compare characters in the plural table and of the suffix of the + // given plural one by one + while ($suffix[$j] === $lowerPluralRev[$j]) { + // Let $j point to the next character + ++$j; + + // Successfully compared the last character + // Add an entry with the singular suffix to the singular array + if ($j === $suffixLength) { + // Is there any character preceding the suffix in the plural string? + if ($j < $pluralLength) { + $nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]); + + if (!$map[2] && $nextIsVocal) { + // suffix may not succeed a vocal but next char is one + break; + } + + if (!$map[3] && !$nextIsVocal) { + // suffix may not succeed a consonant but next char is one + break; + } + } + + $newBase = substr($plural, 0, $pluralLength - $suffixLength); + $newSuffix = $map[4]; + + // Check whether the first character in the plural suffix + // is uppercased. If yes, uppercase the first character in + // the singular suffix too + $firstUpper = ctype_upper($pluralRev[$j - 1]); + + if (\is_array($newSuffix)) { + $singulars = []; + + foreach ($newSuffix as $newSuffixEntry) { + $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); + } + + return $singulars; + } + + return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; + } + + // Suffix is longer than word + if ($j === $pluralLength) { + break; + } + } + } + + // Assume that plural and singular is identical + return [$plural]; + } + + /** + * {@inheritdoc} + */ + public function pluralize(string $singular): array + { + $singularRev = strrev($singular); + $lowerSingularRev = strtolower($singularRev); + $singularLength = \strlen($lowerSingularRev); + + // Check if the word is one which is not inflected, return early if so + if (\in_array($lowerSingularRev, self::UNINFLECTED, true)) { + return [$singular]; + } + + // The outer loop iterates over the entries of the singular table + // The inner loop $j iterates over the characters of the singular suffix + // in the singular table to compare them with the characters of the actual + // given singular suffix + foreach (self::SINGULAR_MAP as $map) { + $suffix = $map[0]; + $suffixLength = $map[1]; + $j = 0; + + // Compare characters in the singular table and of the suffix of the + // given plural one by one + + while ($suffix[$j] === $lowerSingularRev[$j]) { + // Let $j point to the next character + ++$j; + + // Successfully compared the last character + // Add an entry with the plural suffix to the plural array + if ($j === $suffixLength) { + // Is there any character preceding the suffix in the plural string? + if ($j < $singularLength) { + $nextIsVocal = false !== strpos('aeiou', $lowerSingularRev[$j]); + + if (!$map[2] && $nextIsVocal) { + // suffix may not succeed a vocal but next char is one + break; + } + + if (!$map[3] && !$nextIsVocal) { + // suffix may not succeed a consonant but next char is one + break; + } + } + + $newBase = substr($singular, 0, $singularLength - $suffixLength); + $newSuffix = $map[4]; + + // Check whether the first character in the singular suffix + // is uppercased. If yes, uppercase the first character in + // the singular suffix too + $firstUpper = ctype_upper($singularRev[$j - 1]); + + if (\is_array($newSuffix)) { + $plurals = []; + + foreach ($newSuffix as $newSuffixEntry) { + $plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); + } + + return $plurals; + } + + return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; + } + + // Suffix is longer than word + if ($j === $singularLength) { + break; + } + } + } + + // Assume that plural is singular with a trailing `s` + return [$singular.'s']; + } +} diff --git a/vendor/symfony/string/Inflector/FrenchInflector.php b/vendor/symfony/string/Inflector/FrenchInflector.php new file mode 100644 index 0000000..42f6125 --- /dev/null +++ b/vendor/symfony/string/Inflector/FrenchInflector.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Inflector; + +/** + * French inflector. + * + * This class does only inflect nouns; not adjectives nor composed words like "soixante-dix". + */ +final class FrenchInflector implements InflectorInterface +{ + /** + * A list of all rules for pluralise. + * + * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php + */ + private const PLURALIZE_REGEXP = [ + // First entry: regexp + // Second entry: replacement + + // Words finishing with "s", "x" or "z" are invariables + // Les mots finissant par "s", "x" ou "z" sont invariables + ['/(s|x|z)$/i', '\1'], + + // Words finishing with "eau" are pluralized with a "x" + // Les mots finissant par "eau" prennent tous un "x" au pluriel + ['/(eau)$/i', '\1x'], + + // Words finishing with "au" are pluralized with a "x" excepted "landau" + // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" + ['/^(landau)$/i', '\1s'], + ['/(au)$/i', '\1x'], + + // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" + // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" + ['/^(pneu|bleu|émeu)$/i', '\1s'], + ['/(eu)$/i', '\1x'], + + // Words finishing with "al" are pluralized with a "aux" excepted + // Les mots finissant en "al" se terminent en "aux" sauf + ['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'], + ['/al$/i', '\1aux'], + + // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux + ['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'], + + // Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel + ['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'], + + // Invariable words + ['/^(cinquante|soixante|mille)$/i', '\1'], + + // French titles + ['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'], + ['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'], + ]; + + /** + * A list of all rules for singularize. + */ + private const SINGULARIZE_REGEXP = [ + // First entry: regexp + // Second entry: replacement + + // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux + ['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'], + + // Words finishing with "eau" are pluralized with a "x" + // Les mots finissant par "eau" prennent tous un "x" au pluriel + ['/(eau)x$/i', '\1'], + + // Words finishing with "al" are pluralized with a "aux" expected + // Les mots finissant en "al" se terminent en "aux" sauf + ['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'], + + // Words finishing with "au" are pluralized with a "x" excepted "landau" + // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" + ['/(au)x$/i', '\1'], + + // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" + // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" + ['/(eu)x$/i', '\1'], + + // Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou + // Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou + ['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'], + + // French titles + ['/^mes(dame|demoiselle)s$/', 'ma\1'], + ['/^Mes(dame|demoiselle)s$/', 'Ma\1'], + ['/^mes(sieur|seigneur)s$/', 'mon\1'], + ['/^Mes(sieur|seigneur)s$/', 'Mon\1'], + + //Default rule + ['/s$/i', ''], + ]; + + /** + * A list of words which should not be inflected. + * This list is only used by singularize. + */ + private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i'; + + /** + * {@inheritdoc} + */ + public function singularize(string $plural): array + { + if ($this->isInflectedWord($plural)) { + return [$plural]; + } + + foreach (self::SINGULARIZE_REGEXP as $rule) { + [$regexp, $replace] = $rule; + + if (1 === preg_match($regexp, $plural)) { + return [preg_replace($regexp, $replace, $plural)]; + } + } + + return [$plural]; + } + + /** + * {@inheritdoc} + */ + public function pluralize(string $singular): array + { + if ($this->isInflectedWord($singular)) { + return [$singular]; + } + + foreach (self::PLURALIZE_REGEXP as $rule) { + [$regexp, $replace] = $rule; + + if (1 === preg_match($regexp, $singular)) { + return [preg_replace($regexp, $replace, $singular)]; + } + } + + return [$singular.'s']; + } + + private function isInflectedWord(string $word): bool + { + return 1 === preg_match(self::UNINFLECTED, $word); + } +} diff --git a/vendor/symfony/string/Inflector/InflectorInterface.php b/vendor/symfony/string/Inflector/InflectorInterface.php new file mode 100644 index 0000000..67f2834 --- /dev/null +++ b/vendor/symfony/string/Inflector/InflectorInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Inflector; + +interface InflectorInterface +{ + /** + * Returns the singular forms of a string. + * + * If the method can't determine the form with certainty, several possible singulars are returned. + * + * @return string[] + */ + public function singularize(string $plural): array; + + /** + * Returns the plural forms of a string. + * + * If the method can't determine the form with certainty, several possible plurals are returned. + * + * @return string[] + */ + public function pluralize(string $singular): array; +} diff --git a/vendor/symfony/string/LICENSE b/vendor/symfony/string/LICENSE new file mode 100644 index 0000000..383e7a5 --- /dev/null +++ b/vendor/symfony/string/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019-2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/string/LazyString.php b/vendor/symfony/string/LazyString.php new file mode 100644 index 0000000..15b8d72 --- /dev/null +++ b/vendor/symfony/string/LazyString.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +/** + * A string whose value is computed lazily by a callback. + * + * @author Nicolas Grekas + */ +class LazyString implements \Stringable, \JsonSerializable +{ + private \Closure|string $value; + + /** + * @param callable|array $callback A callable or a [Closure, method] lazy-callable + */ + public static function fromCallable(callable|array $callback, mixed ...$arguments): static + { + if (\is_array($callback) && !\is_callable($callback) && !(($callback[0] ?? null) instanceof \Closure || 2 < \count($callback))) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']')); + } + + $lazyString = new static(); + $lazyString->value = static function () use (&$callback, &$arguments, &$value): string { + if (null !== $arguments) { + if (!\is_callable($callback)) { + $callback[0] = $callback[0](); + $callback[1] = $callback[1] ?? '__invoke'; + } + $value = $callback(...$arguments); + $callback = self::getPrettyName($callback); + $arguments = null; + } + + return $value ?? ''; + }; + + return $lazyString; + } + + public static function fromStringable(string|int|float|bool|\Stringable $value): static + { + if (\is_object($value)) { + return static::fromCallable([$value, '__toString']); + } + + $lazyString = new static(); + $lazyString->value = (string) $value; + + return $lazyString; + } + + /** + * Tells whether the provided value can be cast to string. + */ + final public static function isStringable(mixed $value): bool + { + return \is_string($value) || $value instanceof \Stringable || is_scalar($value); + } + + /** + * Casts scalars and stringable objects to strings. + * + * @throws \TypeError When the provided value is not stringable + */ + final public static function resolve(\Stringable|string|int|float|bool $value): string + { + return $value; + } + + public function __toString(): string + { + if (\is_string($this->value)) { + return $this->value; + } + + try { + return $this->value = ($this->value)(); + } catch (\Throwable $e) { + if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) { + $type = explode(', ', $e->getMessage()); + $type = substr(array_pop($type), 0, -\strlen(' returned')); + $r = new \ReflectionFunction($this->value); + $callback = $r->getStaticVariables()['callback']; + + $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type)); + } + + throw $e; + } + } + + public function __sleep(): array + { + $this->__toString(); + + return ['value']; + } + + public function jsonSerialize(): string + { + return $this->__toString(); + } + + private function __construct() + { + } + + private static function getPrettyName(callable $callback): string + { + if (\is_string($callback)) { + return $callback; + } + + if (\is_array($callback)) { + $class = \is_object($callback[0]) ? get_debug_type($callback[0]) : $callback[0]; + $method = $callback[1]; + } elseif ($callback instanceof \Closure) { + $r = new \ReflectionFunction($callback); + + if (false !== strpos($r->name, '{closure}') || !$class = $r->getClosureScopeClass()) { + return $r->name; + } + + $class = $class->name; + $method = $r->name; + } else { + $class = get_debug_type($callback); + $method = '__invoke'; + } + + return $class.'::'.$method; + } +} diff --git a/vendor/symfony/string/README.md b/vendor/symfony/string/README.md new file mode 100644 index 0000000..9c7e1e1 --- /dev/null +++ b/vendor/symfony/string/README.md @@ -0,0 +1,14 @@ +String Component +================ + +The String component provides an object-oriented API to strings and deals +with bytes, UTF-8 code points and grapheme clusters in a unified way. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/string.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/string/Resources/data/wcswidth_table_wide.php b/vendor/symfony/string/Resources/data/wcswidth_table_wide.php new file mode 100644 index 0000000..43c802d --- /dev/null +++ b/vendor/symfony/string/Resources/data/wcswidth_table_wide.php @@ -0,0 +1,1135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +if (!\function_exists(u::class)) { + function u(?string $string = ''): UnicodeString + { + return new UnicodeString($string ?? ''); + } +} + +if (!\function_exists(b::class)) { + function b(?string $string = ''): ByteString + { + return new ByteString($string ?? ''); + } +} + +if (!\function_exists(s::class)) { + /** + * @return UnicodeString|ByteString + */ + function s(?string $string = ''): AbstractString + { + $string = $string ?? ''; + + return preg_match('//u', $string) ? new UnicodeString($string) : new ByteString($string); + } +} diff --git a/vendor/symfony/string/Slugger/AsciiSlugger.php b/vendor/symfony/string/Slugger/AsciiSlugger.php new file mode 100644 index 0000000..548a6b9 --- /dev/null +++ b/vendor/symfony/string/Slugger/AsciiSlugger.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Slugger; + +use Symfony\Component\String\AbstractUnicodeString; +use Symfony\Component\String\UnicodeString; +use Symfony\Contracts\Translation\LocaleAwareInterface; + +if (!interface_exists(LocaleAwareInterface::class)) { + throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".'); +} + +/** + * @author Titouan Galopin + */ +class AsciiSlugger implements SluggerInterface, LocaleAwareInterface +{ + private const LOCALE_TO_TRANSLITERATOR_ID = [ + 'am' => 'Amharic-Latin', + 'ar' => 'Arabic-Latin', + 'az' => 'Azerbaijani-Latin', + 'be' => 'Belarusian-Latin', + 'bg' => 'Bulgarian-Latin', + 'bn' => 'Bengali-Latin', + 'de' => 'de-ASCII', + 'el' => 'Greek-Latin', + 'fa' => 'Persian-Latin', + 'he' => 'Hebrew-Latin', + 'hy' => 'Armenian-Latin', + 'ka' => 'Georgian-Latin', + 'kk' => 'Kazakh-Latin', + 'ky' => 'Kirghiz-Latin', + 'ko' => 'Korean-Latin', + 'mk' => 'Macedonian-Latin', + 'mn' => 'Mongolian-Latin', + 'or' => 'Oriya-Latin', + 'ps' => 'Pashto-Latin', + 'ru' => 'Russian-Latin', + 'sr' => 'Serbian-Latin', + 'sr_Cyrl' => 'Serbian-Latin', + 'th' => 'Thai-Latin', + 'tk' => 'Turkmen-Latin', + 'uk' => 'Ukrainian-Latin', + 'uz' => 'Uzbek-Latin', + 'zh' => 'Han-Latin', + ]; + + private ?string $defaultLocale; + private \Closure|array $symbolsMap = [ + 'en' => ['@' => 'at', '&' => 'and'], + ]; + + /** + * Cache of transliterators per locale. + * + * @var \Transliterator[] + */ + private array $transliterators = []; + + public function __construct(string $defaultLocale = null, array|\Closure $symbolsMap = null) + { + $this->defaultLocale = $defaultLocale; + $this->symbolsMap = $symbolsMap ?? $this->symbolsMap; + } + + /** + * {@inheritdoc} + */ + public function setLocale(string $locale) + { + $this->defaultLocale = $locale; + } + + /** + * {@inheritdoc} + */ + public function getLocale(): string + { + return $this->defaultLocale; + } + + /** + * {@inheritdoc} + */ + public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString + { + $locale = $locale ?? $this->defaultLocale; + + $transliterator = []; + if ($locale && ('de' === $locale || 0 === strpos($locale, 'de_'))) { + // Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl) + $transliterator = ['de-ASCII']; + } elseif (\function_exists('transliterator_transliterate') && $locale) { + $transliterator = (array) $this->createTransliterator($locale); + } + + if ($this->symbolsMap instanceof \Closure) { + // If the symbols map is passed as a closure, there is no need to fallback to the parent locale + // as the closure can just provide substitutions for all locales of interest. + $symbolsMap = $this->symbolsMap; + array_unshift($transliterator, static function ($s) use ($symbolsMap, $locale) { + return $symbolsMap($s, $locale); + }); + } + + $unicodeString = (new UnicodeString($string))->ascii($transliterator); + + if (\is_array($this->symbolsMap)) { + $map = null; + if (isset($this->symbolsMap[$locale])) { + $map = $this->symbolsMap[$locale]; + } else { + $parent = self::getParentLocale($locale); + if ($parent && isset($this->symbolsMap[$parent])) { + $map = $this->symbolsMap[$parent]; + } + } + if ($map) { + foreach ($map as $char => $replace) { + $unicodeString = $unicodeString->replace($char, ' '.$replace.' '); + } + } + } + + return $unicodeString + ->replaceMatches('/[^A-Za-z0-9]++/', $separator) + ->trim($separator) + ; + } + + private function createTransliterator(string $locale): ?\Transliterator + { + if (\array_key_exists($locale, $this->transliterators)) { + return $this->transliterators[$locale]; + } + + // Exact locale supported, cache and return + if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) { + return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); + } + + // Locale not supported and no parent, fallback to any-latin + if (!$parent = self::getParentLocale($locale)) { + return $this->transliterators[$locale] = null; + } + + // Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales + if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) { + $transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); + } + + return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null; + } + + private static function getParentLocale(?string $locale): ?string + { + if (!$locale) { + return null; + } + if (false === $str = strrchr($locale, '_')) { + // no parent locale + return null; + } + + return substr($locale, 0, -\strlen($str)); + } +} diff --git a/vendor/symfony/string/Slugger/SluggerInterface.php b/vendor/symfony/string/Slugger/SluggerInterface.php new file mode 100644 index 0000000..c679ed9 --- /dev/null +++ b/vendor/symfony/string/Slugger/SluggerInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Slugger; + +use Symfony\Component\String\AbstractUnicodeString; + +/** + * Creates a URL-friendly slug from a given string. + * + * @author Titouan Galopin + */ +interface SluggerInterface +{ + /** + * Creates a slug for the given string and locale, using appropriate transliteration when needed. + */ + public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString; +} diff --git a/vendor/symfony/string/UnicodeString.php b/vendor/symfony/string/UnicodeString.php new file mode 100644 index 0000000..70cf4c5 --- /dev/null +++ b/vendor/symfony/string/UnicodeString.php @@ -0,0 +1,358 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; + +/** + * Represents a string of Unicode grapheme clusters encoded as UTF-8. + * + * A letter followed by combining characters (accents typically) form what Unicode defines + * as a grapheme cluster: a character as humans mean it in written texts. This class knows + * about the concept and won't split a letter apart from its combining accents. It also + * ensures all string comparisons happen on their canonically-composed representation, + * ignoring e.g. the order in which accents are listed when a letter has many of them. + * + * @see https://unicode.org/reports/tr15/ + * + * @author Nicolas Grekas + * @author Hugo Hamon + * + * @throws ExceptionInterface + */ +class UnicodeString extends AbstractUnicodeString +{ + public function __construct(string $string = '') + { + $this->string = normalizer_is_normalized($string) ? $string : normalizer_normalize($string); + + if (false === $this->string) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + } + + public function append(string ...$suffix): static + { + $str = clone $this; + $str->string = $this->string.(1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix)); + normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); + + if (false === $str->string) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function chunk(int $length = 1): array + { + if (1 > $length) { + throw new InvalidArgumentException('The chunk length must be greater than zero.'); + } + + if ('' === $this->string) { + return []; + } + + $rx = '/('; + while (65535 < $length) { + $rx .= '\X{65535}'; + $length -= 65535; + } + $rx .= '\X{'.$length.'})/u'; + + $str = clone $this; + $chunks = []; + + foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { + $str->string = $chunk; + $chunks[] = clone $str; + } + + return $chunks; + } + + public function endsWith(string|iterable|AbstractString $suffix): bool + { + if ($suffix instanceof AbstractString) { + $suffix = $suffix->string; + } elseif (!\is_string($suffix)) { + return parent::endsWith($suffix); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($suffix, $form) ?: $suffix = normalizer_normalize($suffix, $form); + + if ('' === $suffix || false === $suffix) { + return false; + } + + if ($this->ignoreCase) { + return 0 === mb_stripos(grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)), $suffix, 0, 'UTF-8'); + } + + return $suffix === grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)); + } + + public function equalsTo(string|iterable|AbstractString $string): bool + { + if ($string instanceof AbstractString) { + $string = $string->string; + } elseif (!\is_string($string)) { + return parent::equalsTo($string); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($string, $form) ?: $string = normalizer_normalize($string, $form); + + if ('' !== $string && false !== $string && $this->ignoreCase) { + return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); + } + + return $string === $this->string; + } + + public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOf($needle, $offset); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); + + if ('' === $needle || false === $needle) { + return null; + } + + try { + $i = $this->ignoreCase ? grapheme_stripos($this->string, $needle, $offset) : grapheme_strpos($this->string, $needle, $offset); + } catch (\ValueError $e) { + return null; + } + + return false === $i ? null : $i; + } + + public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOfLast($needle, $offset); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); + + if ('' === $needle || false === $needle) { + return null; + } + + $string = $this->string; + + if (0 > $offset) { + // workaround https://bugs.php.net/74264 + if (0 > $offset += grapheme_strlen($needle)) { + $string = grapheme_substr($string, 0, $offset); + } + $offset = 0; + } + + $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset); + + return false === $i ? null : $i; + } + + public function join(array $strings, string $lastGlue = null): static + { + $str = parent::join($strings, $lastGlue); + normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); + + return $str; + } + + public function length(): int + { + return grapheme_strlen($this->string); + } + + public function normalize(int $form = self::NFC): static + { + $str = clone $this; + + if (\in_array($form, [self::NFC, self::NFKC], true)) { + normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); + } elseif (!\in_array($form, [self::NFD, self::NFKD], true)) { + throw new InvalidArgumentException('Unsupported normalization form.'); + } elseif (!normalizer_is_normalized($str->string, $form)) { + $str->string = normalizer_normalize($str->string, $form); + $str->ignoreCase = null; + } + + return $str; + } + + public function prepend(string ...$prefix): static + { + $str = clone $this; + $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; + normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); + + if (false === $str->string) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function replace(string $from, string $to): static + { + $str = clone $this; + normalizer_is_normalized($from) ?: $from = normalizer_normalize($from); + + if ('' !== $from && false !== $from) { + $tail = $str->string; + $result = ''; + $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; + + while ('' !== $tail && false !== $i = $indexOf($tail, $from)) { + $slice = grapheme_substr($tail, 0, $i); + $result .= $slice.$to; + $tail = substr($tail, \strlen($slice) + \strlen($from)); + } + + $str->string = $result.$tail; + normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); + + if (false === $str->string) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + } + + return $str; + } + + public function replaceMatches(string $fromRegexp, string|callable $to): static + { + $str = parent::replaceMatches($fromRegexp, $to); + normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); + + return $str; + } + + public function slice(int $start = 0, int $length = null): static + { + $str = clone $this; + + $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647); + + return $str; + } + + public function splice(string $replacement, int $start = 0, int $length = null): static + { + $str = clone $this; + + $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0; + $length = $length ? \strlen(grapheme_substr($this->string, $start, $length ?? 2147483647)) : $length; + $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647); + normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); + + if (false === $str->string) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function split(string $delimiter, int $limit = null, int $flags = null): array + { + if (1 > $limit = $limit ?? 2147483647) { + throw new InvalidArgumentException('Split limit must be a positive integer.'); + } + + if ('' === $delimiter) { + throw new InvalidArgumentException('Split delimiter is empty.'); + } + + if (null !== $flags) { + return parent::split($delimiter.'u', $limit, $flags); + } + + normalizer_is_normalized($delimiter) ?: $delimiter = normalizer_normalize($delimiter); + + if (false === $delimiter) { + throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); + } + + $str = clone $this; + $tail = $this->string; + $chunks = []; + $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; + + while (1 < $limit && false !== $i = $indexOf($tail, $delimiter)) { + $str->string = grapheme_substr($tail, 0, $i); + $chunks[] = clone $str; + $tail = substr($tail, \strlen($str->string) + \strlen($delimiter)); + --$limit; + } + + $str->string = $tail; + $chunks[] = clone $str; + + return $chunks; + } + + public function startsWith(string|iterable|AbstractString $prefix): bool + { + if ($prefix instanceof AbstractString) { + $prefix = $prefix->string; + } elseif (!\is_string($prefix)) { + return parent::startsWith($prefix); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($prefix, $form) ?: $prefix = normalizer_normalize($prefix, $form); + + if ('' === $prefix || false === $prefix) { + return false; + } + + if ($this->ignoreCase) { + return 0 === mb_stripos(grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES), $prefix, 0, 'UTF-8'); + } + + return $prefix === grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES); + } + + public function __wakeup() + { + if (!\is_string($this->string)) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); + } + + public function __clone() + { + if (null === $this->ignoreCase) { + normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); + } + + $this->ignoreCase = false; + } +} diff --git a/vendor/symfony/string/composer.json b/vendor/symfony/string/composer.json new file mode 100644 index 0000000..187323f --- /dev/null +++ b/vendor/symfony/string/composer.json @@ -0,0 +1,42 @@ +{ + "name": "symfony/string", + "type": "library", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "keywords": ["string", "utf8", "utf-8", "grapheme", "i18n", "unicode"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\String\\": "" }, + "files": [ "Resources/functions.php" ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +}