diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b9ab33e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..23d0f79 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: sebastian-meyer +custom: "https://paypal.me/sebastianmeyer" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..33f1a33 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + assignees: + - "sebastian-meyer" + labels: [ ] + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + assignees: + - "sebastian-meyer" + labels: [ ] diff --git a/.github/workflows/phpmd.yml b/.github/workflows/phpmd.yml new file mode 100644 index 0000000..d9baa5c --- /dev/null +++ b/.github/workflows/phpmd.yml @@ -0,0 +1,40 @@ +name: PHP Mess Detector + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + PHPMD: + name: PHPMD Scanner + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + + - name: Setup Environment + uses: shivammathur/setup-php@v2 + with: + php-version: "8.0" + coverage: none + tools: phpmd + + - name: Run PHPMD + run: phpmd . sarif codesize --reportfile phpmd-results.sarif + continue-on-error: true + + - name: Upload Analysis Results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: phpmd-results.sarif + wait-for-processing: true diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..a42088b --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,35 @@ +name: PHP Static Analyzer + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + PHPStan: + name: PHPStan Scanner + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + + - name: Setup Environment + uses: php-actions/composer@v6 + with: + command: update + php_version: "8.0" + + - name: Run PHPStan + uses: php-actions/phpstan@v3 + with: + path: src/ + configuration: phpstan.dist.neon diff --git a/.gitignore b/.gitignore index a67d42b..723e47a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -composer.phar +/.vscode/ +/config/config.yml +/data/ +/var/ /vendor/ - -# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# composer.lock +.php-cs-fixer.php +phpstan.neon diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..8286899 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,38 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace PhpCsFixer; + +/** + * Configuration for PHP-CS-Fixer. + * @see https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/doc/config.rst + * + * @return ConfigInterface + */ +return (new Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@PSR12' => true, + ]) + ->setFinder( + (new Finder())->in(__DIR__) + ); diff --git a/bin/cli b/bin/cli new file mode 100644 index 0000000..8571e92 --- /dev/null +++ b/bin/cli @@ -0,0 +1,55 @@ +#!/usr/bin/env php + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2; + +use Doctrine\ORM\Tools\Console\ConsoleRunner; +use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider; +use Exception; +use OCC\OaiPmh2\Console\AddRecordCommand; +use OCC\OaiPmh2\Console\BulkUpdateCommand; +use OCC\OaiPmh2\Console\DeleteRecordCommand; +use OCC\OaiPmh2\Console\PruneResumptionTokensCommand; +use OCC\OaiPmh2\Console\UpdateFormatsCommand; + +require __DIR__ . '/../vendor/autoload.php'; + +$commands = [ + new AddRecordCommand(), + new BulkUpdateCommand(), + new DeleteRecordCommand(), + new PruneResumptionTokensCommand(), + new UpdateFormatsCommand() +]; + +try { + ConsoleRunner::run( + new SingleManagerProvider( + Database::getInstance()->getEntityManager() + ), + $commands + ); +} catch (Exception $exception) { + echo '[ERROR] Exception ' . $exception->getCode() . ' thrown:' . PHP_EOL; + echo $exception->getMessage() . PHP_EOL; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..14743c6 --- /dev/null +++ b/composer.json @@ -0,0 +1,86 @@ +{ + "name": "opencultureconsulting/oai-pmh2", + "description": "This is a stand-alone OAI-PMH 2.0 data provider. It serves records in any XML metadata format from a database, supports deleted records, resumption tokens and sets.", + "type": "project", + "keywords": [ + "oai", + "oaipmh", + "oaipmh2", + "oai-pmh", + "oai-pmh2", + "code4lib" + ], + "homepage": "https://github.com/opencultureconsulting/oai-pmh2", + "readme": "README.md", + "license": ["GPL-3.0-or-later"], + "authors": [ + { + "name": "Sebastian Meyer", + "email": "sebastian.meyer@opencultureconsulting.com", + "homepage": "https://www.opencultureconsulting.com", + "role": "maintainer" + } + ], + "support": { + "issues": "https://github.com/opencultureconsulting/oai-pmh2/issues", + "source": "https://github.com/opencultureconsulting/oai-pmh2", + "docs": "https://github.com/opencultureconsulting/oai-pmh2/blob/main/README.md" + }, + "require": { + "php": "^8.1", + "ext-dom": "*", + "ext-libxml": "*", + "ext-sqlite3": "*", + "doctrine/dbal": "^3.7", + "doctrine/orm": "^2.17", + "opencultureconsulting/basics": "^1.0", + "opencultureconsulting/psr15": "^1.0", + "symfony/cache": "^6.4", + "symfony/console": "^6.4", + "symfony/filesystem": "^6.4", + "symfony/serializer":"^6.4", + "symfony/validator": "^6.4", + "symfony/yaml": "^6.4" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-strict-rules": "^1.5", + "friendsofphp/php-cs-fixer": "^3.45" + }, + "autoload": { + "psr-4": { + "OCC\\OaiPmh2\\": "src/" + } + }, + "scripts": { + "pre-install-cmd": [ + "@php -r \"!is_dir('./data') && mkdir('./data', 0775);\"", + "@php -r \"!file_exists('./config/config.yml') && copy('./config/config.dist.yml', './config/config.yml');\"" + ], + "post-install-cmd": [ + "@doctrine:clear-cache --quiet", + "@php bin/cli orm:generate-proxies --quiet" + ], + "post-create-project-cmd": [ + "@doctrine:initialize-database --quiet" + ], + "doctrine:clear-cache": [ + "@php bin/cli orm:clear-cache:metadata --flush", + "@php bin/cli orm:clear-cache:query --flush", + "@php bin/cli orm:clear-cache:result --flush" + ], + "doctrine:initialize-database": [ + "@php bin/cli orm:schema-tool:update --complete --force", + "@oai:update-formats --quiet" + ], + "oai:update-formats": [ + "@php bin/cli oai:formats:update" + ] + }, + "scripts-descriptions": { + "doctrine:clear-cache": "Clears the Doctrine/ORM metadata, query and result caches", + "doctrine:generate-proxies": "Generates the Doctrine/ORM entity proxies", + "doctrine:initialize-database": "Initializes a new database (NOT RECOMMENDED IN PRODUCTION!)", + "oai:update-formats": "Updates supported metadata formats from configuration" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..c263e1e --- /dev/null +++ b/composer.lock @@ -0,0 +1,4242 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "816dd79b521706bbb444b1dbf37d384c", + "packages": [ + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/collections", + "version": "2.1.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "72328a11443a0de79967104ad36ba7b30bded134" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/72328a11443a0de79967104ad36ba7b30bded134", + "reference": "72328a11443a0de79967104ad36ba7b30bded134", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^5.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.1.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2023-10-03T09:22:33+00:00" + }, + { + "name": "doctrine/common", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced", + "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^2.0 || ^3.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0 || ^10.0", + "doctrine/collections": "^1", + "phpstan/phpstan": "^1.4.1", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^6.1", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/common/issues", + "source": "https://github.com/doctrine/common/tree/3.4.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "type": "tidelift" + } + ], + "time": "2022-10-09T11:47:59+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.7.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "0ac3c270590e54910715e9a1a044cc368df282b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/0ac3c270590e54910715e9a1a044cc368df282b2", + "reference": "0ac3c270590e54910715e9a1a044cc368df282b2", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "1.10.42", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "9.6.13", + "psalm/plugin-phpunit": "0.18.4", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^5.4|^6.0", + "symfony/console": "^4.4|^5.4|^6.0", + "vimeo/psalm": "4.30.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.7.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2023-11-19T08:06:58+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.2" + }, + "time": "2023-09-27T20:04:15+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/750671534e0241a7c50ea5b43f67e23eb5c96f32", + "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2022-10-12T20:59:15+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/f9301a5b2fb1216b2b08f02ba04dc45423db6bff", + "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.8" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2023-06-16T13:40:37+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "doctrine/lexer", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^4.11 || ^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-12-14T08:49:07+00:00" + }, + { + "name": "doctrine/orm", + "version": "2.17.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "393679a4795e49b0b3ac317dce84d0f8888f2b77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/393679a4795e49b0b3ac317dce84d0f8888f2b77", + "reference": "393679a4795e49b0b3ac317dce84d0f8888f2b77", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.12.1 || ^2.1.1", + "doctrine/collections": "^1.5 || ^2.1", + "doctrine/common": "^3.0.3", + "doctrine/dbal": "^2.13.1 || ^3.2", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^2", + "doctrine/persistence": "^2.4 || ^3", + "ext-ctype": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php72": "^1.23", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "doctrine/annotations": "<1.13 || >= 3.0" + }, + "require-dev": { + "doctrine/annotations": "^1.13 || ^2", + "doctrine/coding-standard": "^9.0.2 || ^12.0", + "phpbench/phpbench": "^0.16.10 || ^1.0", + "phpstan/phpstan": "~1.4.10 || 1.10.35", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", + "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "vimeo/psalm": "4.30.0 || 5.16.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0", + "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" + }, + "bin": [ + "bin/doctrine" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "lib/Doctrine/ORM" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/2.17.2" + }, + "time": "2023-12-20T21:47:52+00:00" + }, + { + "name": "doctrine/persistence", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "63fee8c33bef740db6730eb2a750cd3da6495603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/63fee8c33bef740db6730eb2a750cd3da6495603", + "reference": "63fee8c33bef740db6730eb2a750cd3da6495603", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^7.2 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/common": "<2.10" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/coding-standard": "^11", + "doctrine/common": "^3.0", + "phpstan/phpstan": "1.9.4", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "vimeo/psalm": "4.30.0 || 5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/3.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2023-05-17T18:32:04+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, + { + "name": "opencultureconsulting/basics", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/opencultureconsulting/php-basics.git", + "reference": "b20a8df00cecd426b8283a5719533255c7073658" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opencultureconsulting/php-basics/zipball/b20a8df00cecd426b8283a5719533255c7073658", + "reference": "b20a8df00cecd426b8283a5719533255c7073658", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.45", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-strict-rules": "^1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "OCC\\Basics\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "Sebastian Meyer", + "email": "sebastian.meyer@opencultureconsulting.com", + "homepage": "https://www.opencultureconsulting.com", + "role": "maintainer" + } + ], + "description": "This is a collection of generic Classes and useful Traits for PHP projects.", + "homepage": "https://github.com/opencultureconsulting/php-basics", + "keywords": [ + "ArrayAccess", + "IteratorAggregate", + "StrictList", + "StrictQueue", + "StrictStack", + "countable", + "getter", + "iterator", + "setter", + "singleton", + "throwErrorException" + ], + "support": { + "docs": "https://github.com/opencultureconsulting/php-basics/blob/main/README.md", + "issues": "https://github.com/opencultureconsulting/php-basics/issues", + "source": "https://github.com/opencultureconsulting/php-basics" + }, + "funding": [ + { + "url": "https://paypal.me/sebastianmeyer", + "type": "custom" + }, + { + "url": "https://github.com/sebastian-meyer", + "type": "github" + } + ], + "time": "2024-01-03T14:27:36+00:00" + }, + { + "name": "opencultureconsulting/psr15", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/opencultureconsulting/psr-15.git", + "reference": "f108f19d2ee3cd3773f8007d16d9317726cc5e99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opencultureconsulting/psr-15/zipball/f108f19d2ee3cd3773f8007d16d9317726cc5e99", + "reference": "f108f19d2ee3cd3773f8007d16d9317726cc5e99", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2.6", + "opencultureconsulting/basics": "^1.0", + "php": "^8.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0" + }, + "provide": { + "psr/http-server-handler-implementation": "1.0", + "psr/http-server-middleware-implementation": "1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.45", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-strict-rules": "^1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "OCC\\PSR15\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "Sebastian Meyer", + "email": "sebastian.meyer@opencultureconsulting.com", + "homepage": "https://www.opencultureconsulting.com", + "role": "maintainer" + } + ], + "description": "This is a queue-based implementation of PSR-15: HTTP Server Request Handler.", + "homepage": "https://github.com/opencultureconsulting/psr-15", + "keywords": [ + "handler", + "http", + "http-server-handler", + "http-server-middleware", + "middleware", + "psr-15", + "request" + ], + "support": { + "docs": "https://github.com/opencultureconsulting/psr-15/blob/main/README.md", + "issues": "https://github.com/opencultureconsulting/psr-15/issues", + "source": "https://github.com/opencultureconsulting/psr-15" + }, + "funding": [ + { + "url": "https://paypal.me/sebastianmeyer", + "type": "custom" + }, + { + "url": "https://github.com/sebastian-meyer", + "type": "github" + } + ], + "time": "2024-01-01T09:29:46+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "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/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://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/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "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/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/cache", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "14a75869bbb41cb35bc5d9d322473928c6f3f978" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/14a75869bbb41cb35bc5d9d322473928c6f3f978", + "reference": "14a75869bbb41cb35bc5d9d322473928c6f3f978", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.3.6|^7.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/var-dumper": "<5.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.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 extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v6.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": "2023-12-29T15:34:34+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "1d74b127da04ffa87aa940abe15446fa89653778" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1d74b127da04ffa87aa940abe15446fa89653778", + "reference": "1d74b127da04ffa87aa940abe15446fa89653778", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "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 caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.4.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": "2023-09-25T12:52:38+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0254811a143e6bc6c8deea08b589a7e68a37f625" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0254811a143e6bc6c8deea08b589a7e68a37f625", + "reference": "0254811a143e6bc6c8deea08b589a7e68a37f625", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "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/v6.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": "2023-12-10T16:15:48+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-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.4.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": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "952a8cb588c3bc6ce76f6023000fb932f16a6e59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/952a8cb588c3bc6ce76f6023000fb932f16a6e59", + "reference": "952a8cb588c3bc6ce76f6023000fb932f16a6e59", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "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.4.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": "2023-07-26T17:27:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "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.28.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": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "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.28.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": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "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.28.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": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "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.28.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": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", + "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "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.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.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": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "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.28.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": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-php80": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "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.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.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": "2023-08-16T06:22:46+00:00" + }, + { + "name": "symfony/serializer", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "f87ea9d7bfd4cf2f7b72be554607e6c96e6664af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/f87ea9d7bfd4cf2f7b72be554607e6c96e6664af", + "reference": "f87ea9d7bfd4cf2f7b72be554607e6c96e6664af", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<5.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4.24|>=6,<6.2.11", + "symfony/uid": "<5.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4.24|^6.2.11|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "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": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v6.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": "2023-12-29T15:34:34+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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/v3.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": "2023-12-26T14:02:43+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "7cb80bc10bfcdf6b5492741c0b9357dac66940bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/7cb80bc10bfcdf6b5492741c0b9357dac66940bc", + "reference": "7cb80bc10bfcdf6b5492741c0b9357dac66940bc", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "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.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "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.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": "2023-12-10T16:15:48+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "06450585bf65e978026bda220cdebca3f867fde7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/06450585bf65e978026bda220cdebca3f867fde7", + "reference": "06450585bf65e978026bda220cdebca3f867fde7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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 translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.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": "2023-12-26T14:02:43+00:00" + }, + { + "name": "symfony/validator", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "15fe2c6ed815b06b6b8636d8ba3ef9807ee1a75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/15fe2c6ed815b06b6b8636d8ba3ef9807ee1a75c", + "reference": "15fe2c6ed815b06b6b8636d8ba3ef9807ee1a75c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.13", + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13|^2", + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "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 tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v6.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": "2023-12-29T16:34:12+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "5fe9a0021b8d35e67d914716ec8de50716a68e7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/5fe9a0021b8d35e67d914716ec8de50716a68e7e", + "reference": "5fe9a0021b8d35e67d914716ec8de50716a68e7e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "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": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v6.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": "2023-12-27T08:18:35+00:00" + }, + { + "name": "symfony/yaml", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "4f9237a1bb42455d609e6687d2613dde5b41a587" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4f9237a1bb42455d609e6687d2613dde5b41a587", + "reference": "4f9237a1bb42455d609e6687d2613dde5b41a587", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.4.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": "2023-11-06T11:00:25+00:00" + } + ], + "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-10-11T07:11:09+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.45.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "c0daa33cb2533cd73f48dde1c70c2afa3e7953b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c0daa33cb2533cd73f48dde1c70c2afa3e7953b5", + "reference": "c0daa33cb2533cd73f48dde1c70c2afa3e7953b5", + "shasum": "" + }, + "require": { + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0 || ^5.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php80": "^1.28", + "symfony/polyfill-php81": "^1.28", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3 || ^2.0", + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4", + "phpunit/phpunit": "^9.6 || ^10.5.5", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.45.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2023-12-30T02:07:07+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.10.50", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", + "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2023-12-13T10:59:42+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/7a50e9662ee9f3942e4aaaf3d603653f60282542", + "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10.34" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.2" + }, + "time": "2023-10-30T14:35:06+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T10:55:06+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "e95216850555cd55e71b857eb9d6c2674124603a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e95216850555cd55e71b857eb9d6c2674124603a", + "reference": "e95216850555cd55e71b857eb9d6c2674124603a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "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 tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.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": "2023-12-27T22:16:42+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "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 dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.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": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "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": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.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": "2023-10-31T17:30:12+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "22301f0e7fdeaacc14318928612dee79be99860e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", + "reference": "22301f0e7fdeaacc14318928612dee79be99860e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "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 an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.4.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": "2023-08-08T10:16:24+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "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.28.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": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c4b1ef0bc80533d87a2e969806172f1c2a980241" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c4b1ef0bc80533d87a2e969806172f1c2a980241", + "reference": "c4b1ef0bc80533d87a2e969806172f1c2a980241", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "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": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.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": "2023-12-22T16:42:54+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2", + "reference": "fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "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 a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.4.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": "2023-02-16T10:14:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1", + "ext-dom": "*", + "ext-libxml": "*", + "ext-sqlite3": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/config/config.dist.yml b/config/config.dist.yml new file mode 100644 index 0000000..0939283 --- /dev/null +++ b/config/config.dist.yml @@ -0,0 +1,90 @@ +# +# The default configuration +# +# Copy this file as 'config.yml' and change the settings to your preferences. +# See https://www.openarchives.org/OAI/2.0/openarchivesprotocol.htm for further +# explanation. +# + +# +# A human readable name for the repository +# +repositoryName: 'OAI-PMH 2.0 Data Provider' + +# +# Email address for contacting the repository owner +# +# This has to be a valid email according to RFC 822 Address Specification. +# See https://www.w3.org/Protocols/rfc822/#z8 for further explanation. +# +adminEmail: admin@example.org + +# +# Database connection details +# +# This has to be a valid data source name (DSN) URL. The scheme is used to +# specify a driver, the user and password in the URL encode user and password +# for the connection, followed by the host and port parts. The path after the +# authority part represents the name of the database (the leading slash is +# removed so add an extra slash to specify an absolute file path for SQLite). +# The placeholder "%BASEDIR%" may be used to represent the application's base +# directory. +# Any optional query parameters are used as additional connection parameters. +# Since the scheme determines the database driver, it also specifies if the PDO +# abstraction ("mariadb", "mssql", "mysql", "oracle", "postgres", "sqlite") or +# native drivers ("ibm-db2", "mysqli", "oci8", "pgsql", "sqlite3", "sqlsrv") +# should be used to handle the connection. Make sure the corresponding PHP +# extensions are installed. +# See https://www.doctrine-project.org/projects/doctrine-dbal/en/3.7/reference/configuration.html#connecting-using-a-url +# for further explanation. +# +# %DRIVER%://[%USER%[:%PASSWORD%]@]%HOST%[:%PORT%]/%DBNAME%[?%OPTIONS%] +# +# Examples: +# database: 'mssql://oaipmh:secret@127.0.0.1/oaipmh' +# database: 'mysql://root@localhost/oai?charset=utf8mb4' +# database: 'pgsql://oaipmh:secret@localhost:5432/oai_data_provider' +# database: 'sqlite3:////home/oaipmh/database.db' +# +# Run "composer doctrine:initialize-database" after switching to a new DB to +# test the settings and initialize the database! +# +database: 'sqlite3:///%BASEDIR%/data/sqlite3.db' + +# +# Metadata formats, namespaces and schemas of your records +# +# The default is 'oai_dc' which is also required by the OAI-PMH specification, +# but technically you can provide any XML based data formats you want. Just add +# another entry with the metadata prefix as key and namespace/schema URIs as +# array values or replace the default entry (although not recommended). +# You do not have to provide every record in each metadata format, but if you +# have the same record in multiple formats, it's highly recommended to use the +# same identifier for all versions of the record. +# +# Run "composer oai:update-formats" after changing metadata prefixes to update +# the database accordingly! +# +metadataPrefix: { + oai_dc: { + namespace: 'http://www.openarchives.org/OAI/2.0/oai_dc/', + schema: 'https://www.openarchives.org/OAI/2.0/oai_dc.xsd' + } +} + +# +# Maximum number of records to return per request +# +# For larger result sets resumption tokens are provided repeatedly, which +# allow requesting more batches of records until the set is complete. +# +# [1 - 100] +# +maxRecords: 50 + +# +# Number of seconds a resumption token should be valid +# +# [300 - 86400] +# +tokenValid: 1800 # 30 minutes diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..7a9d50f --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,14 @@ +# +# Configuration for PHPStan. +# @see https://phpstan.org/config-reference +# + +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon + +parameters: + level: 9 + strictRules: + noVariableVariables: false + paths: + - src diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..28b0ea1 --- /dev/null +++ b/public/index.php @@ -0,0 +1,28 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2; + +require __DIR__ . '/../vendor/autoload.php'; + +$app = new App(); +$app->run(); diff --git a/public/resources/stylesheet.xsl b/public/resources/stylesheet.xsl new file mode 100644 index 0000000..42dc644 --- /dev/null +++ b/public/resources/stylesheet.xsl @@ -0,0 +1,520 @@ + + + + + + + + * { + font-family: "Lucida Sans Unicode", sans-serif; + } + body { + margin: 1em 2em 1em 2em; + } + h1, + h2, + h3, + h4 { + clear: left; + } + h1 { + padding-bottom: 4px; + margin-bottom: 0px; + } + h2 { + margin-bottom: 0.5em; + } + h3 { + margin-bottom: 0.3em; + font-size: medium; + } + h4 { + margin-bottom: 0.2em; + font-size: small; + } + table { + margin-top: 10px; + } + td.key { + background-color: #e0e0ff; + padding: 3px; + text-align: right; + border: 1px solid #c0c0c0; + white-space: nowrap; + vertical-align: top; + } + td.value { + vertical-align: center; + padding-left: 1em; + padding: 3px; + } + .link { + border: 1px outset #88f; + background-color: #c0c0ff; + padding: 1px 4px; + font-size: 80%; + text-decoration: none; + color: black; + } + .link:hover { + color: gray; + } + .link:active { + color: red; + border: 1px inset #88f; + background-color: #a0a0df; + } + .results { + margin-bottom: 1.5em; + } + div.quicklinks { + border-bottom: 2px solid #ccc; + border-top: 2px solid #ccc; + } + ul { + margin: 2px 0; + padding: 4px; + text-align: left; + clear: left; + } + ul li { + font-size: 80%; + display: inline; + list-style: none; + } + ol { + padding: 0; + } + ol>li { + list-style: none; + padding: 0 5px 5px; + margin: 0 0 1em; + border: 1px solid #c0c0c0; + } + p { + margin: 0; + padding: 5px; + } + p.info { + font-size: 80%; + } + .xmlSource { + font-size: 70%; + border: solid #c0c0a0 1px; + background-color: #ffffe0; + padding: 2em 2em 2em 0; + } + .xmlBlock { + padding-left: 2em; + } + .xmlTagName { + color: #800000; + font-weight: bold; + } + .xmlAttrName { + font-weight: bold; + } + .xmlAttrValue { + color: #0000c0; + } + + + + + + + + + + + + + + + + + + + + + + + OAI-PMH 2.0 Request Results + + + +

OAI-PMH 2.0 Request Results

+ + + +

You are viewing an HTML version of the XML OAI-PMH response. To see the underlying XML as it appears to any OAI-PMH harvester use your web browser's view source option or disable XSLT processing.

+

This XSL script was originally written by Christopher Gutteridge at University of Southampton for the EPrints project and was later adapted by Sebastian Meyer at Open Culture Consulting to be more generally applicable to other OAI-PMH interfaces. It is available on GitHub for free!

+ + +
+ + + + + + + + + + + + + +
Datestamp of Response
Request URL
Request Parameters + verb =
+ metadataPrefix =
+ identifier =
+ from =
+ until =
+ set =
+ resumptionToken =
+
+ + +

Error

+

The request could not be completed due to the following error.

+
+ +
+
+ +

+

The request was completed with the following results.

+
+ + + + + +
+
+
+
+ + + + + + +
Error Code
+

+
+ + + +
    +
  1. +

    Repository Identification

    + + + + + + + + + + + + + + + +
    Name
    Base URL
    Protocol Version
    Earliest Datestamp
    Deleted Record Policy
    Granularity
    Administrative Email
    +
  2. +
+
+ + + + + +

This is a list of metadata formats available for the record .

+
+ +

This is a list of metadata formats available from this repository.

+
+
+
    + +
+
+ + + +
  • +

    Metadata Format

    + + + + + + + + +
    Prefix
    Namespace
    Schema
    +
  • +
    + + + +

    This is a list of records' identifiers available for the metadata format .

    +
      + +
    + +
    + + +
  • +

    Record Header

    + + + + + + + + +
    Identifier
    Datestamp
    Deleted + + yes + no + +
    +
  • +
    + + + +

    This is a list of records available for the metadata format .

    +
      + +
    + +
    + + + +

    This is the record in the metadata format .

    +
      + +
    +
    + + + +
  • + + +
  • +
    + + +

    Record

    + + + + + + +
    Identifier
    Datestamp
    +

    This record has been deleted.

    +
    + + + + + + + +

    There are more results.

    + + + + + + + + + + +
    Cursor Position
    Total Records
    Expiration Datestamp
    Resumption Token
    +
    + + + +

    Metadata Format

    +
    + +
    +
    + + + +

    Metadata Format DublinCore

    + + +
    +
    + + + Title + + + + Author or Creator + + + + Subject and Keywords + + + + Description + + + + Publisher + + + + Other Contributor + + + + Date + + + + Resource Type + + + + Format + + + + Resource Identifier + + + + Source + + + + Language + + + + Relation + + + + + + + + + + + + + Coverage + + + + Rights Management + + + + +
    + <></> +
    +
    + + + + + ="" + + +
    diff --git a/src/App.php b/src/App.php new file mode 100644 index 0000000..09e3edc --- /dev/null +++ b/src/App.php @@ -0,0 +1,59 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2; + +use OCC\OaiPmh2\Middleware\Dispatcher; +use OCC\PSR15\QueueRequestHandler; + +/** + * Main application of the OAI-PMH 2.0 Data Provider. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class App +{ + /** + * The PSR-15 Server Request Handler. + */ + protected QueueRequestHandler $requestHandler; + + /** + * Instantiate application. + */ + public function __construct() + { + $this->requestHandler = new QueueRequestHandler([new Dispatcher()]); + } + + /** + * Run the application. + * + * @return void + */ + public function run(): void + { + $this->requestHandler->handle(); + $this->requestHandler->respond(); + } +} diff --git a/src/Configuration.php b/src/Configuration.php new file mode 100644 index 0000000..e8ec706 --- /dev/null +++ b/src/Configuration.php @@ -0,0 +1,178 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2; + +use OCC\Basics\Traits\Singleton; +use Symfony\Component\Filesystem\Exception\FileNotFoundException; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Exception\ValidationFailedException; +use Symfony\Component\Validator\Validation; +use Symfony\Component\Yaml\Yaml; + +/** + * Reads, validates and provides configuration settings. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + * + * @property-read string $repositoryName + * @property-read string $adminEmail + * @property-read string $database + * @property-read array $metadataPrefix + * @property-read int $maxRecords + * @property-read int $tokenValid + * + * @template TKey of string + * @template TValue + */ +class Configuration +{ + use Singleton; + + /** + * Fully qualified path to the configuration file. + * + * @var string + */ + protected const CONFIG_FILE = __DIR__ . '/../config/config.yml'; + + /** + * The configuration settings. + * + * @var array + */ + protected readonly array $settings; + + /** + * Get constraints for configuration array. + * + * @return Assert\Collection The collection of constraints + */ + protected function getValidationConstraints(): Assert\Collection + { + return new Assert\Collection([ + 'repositoryName' => [ + new Assert\Type('string'), + new Assert\NotBlank() + ], + 'adminEmail' => [ + new Assert\Type('string'), + new Assert\Email(['mode' => 'html5']), + new Assert\NotBlank() + ], + 'database' => [ + new Assert\Type('string'), + new Assert\NotBlank() + ], + 'metadataPrefix' => [ + new Assert\Type('array'), + new Assert\All([ + new Assert\Collection([ + 'schema' => [ + new Assert\Type('string'), + new Assert\Url(), + new Assert\NotBlank() + ], + 'namespace' => [ + new Assert\Type('string'), + new Assert\Url(), + new Assert\NotBlank() + ] + ]) + ]) + ], + 'maxRecords' => [ + new Assert\Type('int'), + new Assert\Range([ + 'min' => 1, + 'max' => 100 + ]) + ], + 'tokenValid' => [ + new Assert\Type('int'), + new Assert\Range([ + 'min' => 300, + 'max' => 86400 + ]) + ] + ]); + } + + /** + * Read and validate configuration file. + * + * @return array The configuration array + * + * @throws FileNotFoundException|ValidationFailedException + */ + protected function loadConfigFile(): array + { + $configPath = Path::canonicalize(self::CONFIG_FILE); + if (!is_readable($configPath)) { + throw new FileNotFoundException( + sprintf( + 'Configuration file "%s" not found or not readable.', + $configPath + ), + 500, + null, + $configPath + ); + } + $config = Yaml::parseFile($configPath); + $validator = Validation::createValidator(); + $violations = $validator->validate($config, $this->getValidationConstraints()); + if ($violations->count() > 0) { + throw new ValidationFailedException(null, $violations); + } + /** @var array $config */ + return $config; + } + + /** + * Load and validate configuration settings from YAML file. + * + * @throws FileNotFoundException|ValidationFailedException + */ + private function __construct() + { + try { + $this->settings = $this->loadConfigFile(); + } catch (FileNotFoundException|ValidationFailedException $exception) { + throw $exception; + } + } + + /** + * Magic getter for $this->settings. + * + * @param TKey $name The setting to retrieve + * + * @return TValue|null The setting or NULL + */ + public function __get(string $name): mixed + { + return $this->settings[$name] ?? null; + } +} diff --git a/src/Console/AddRecordCommand.php b/src/Console/AddRecordCommand.php new file mode 100644 index 0000000..9a4f275 --- /dev/null +++ b/src/Console/AddRecordCommand.php @@ -0,0 +1,46 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Console; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Add or update a record in the database. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[AsCommand( + name: 'oai:records:add', + description: 'Add or update a record in the database' +)] +class AddRecordCommand extends Command +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + return Command::SUCCESS; + } +} diff --git a/src/Console/BulkUpdateCommand.php b/src/Console/BulkUpdateCommand.php new file mode 100644 index 0000000..d36fe22 --- /dev/null +++ b/src/Console/BulkUpdateCommand.php @@ -0,0 +1,48 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Console; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Update records in database from CSV file. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[AsCommand( + name: 'oai:records:bulk-update', + description: 'Update records in database from CSV file' +)] +class BulkUpdateCommand extends Command +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + // https://symfony.com/doc/current/console/input.html + // https://symfony.com/doc/current/components/serializer.html#the-csvencoder + return Command::SUCCESS; + } +} diff --git a/src/Console/DeleteRecordCommand.php b/src/Console/DeleteRecordCommand.php new file mode 100644 index 0000000..f438c1b --- /dev/null +++ b/src/Console/DeleteRecordCommand.php @@ -0,0 +1,46 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Console; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Delete a record from database. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[AsCommand( + name: 'oai:records:delete', + description: 'Delete a record from database' +)] +class DeleteRecordCommand extends Command +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + return Command::SUCCESS; + } +} diff --git a/src/Console/PruneResumptionTokensCommand.php b/src/Console/PruneResumptionTokensCommand.php new file mode 100644 index 0000000..ea86359 --- /dev/null +++ b/src/Console/PruneResumptionTokensCommand.php @@ -0,0 +1,56 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Console; + +use OCC\OaiPmh2\Database; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Prune expired resumption tokens from database. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[AsCommand( + name: 'oai:tokens:prune', + description: 'Prune expired resumption tokens from database' +)] +class PruneResumptionTokensCommand extends Command +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $expired = Database::getInstance()->pruneResumptionTokens(); + $output->writeln([ + '', + sprintf( + ' [OK] %d resumption tokens are expired and were successfully deleted. ', + $expired + ), + '' + ]); + return Command::SUCCESS; + } +} diff --git a/src/Console/UpdateFormatsCommand.php b/src/Console/UpdateFormatsCommand.php new file mode 100644 index 0000000..26a1e23 --- /dev/null +++ b/src/Console/UpdateFormatsCommand.php @@ -0,0 +1,135 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Console; + +use OCC\OaiPmh2\Configuration; +use OCC\OaiPmh2\Database; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Synchronize metadata formats in database with configuration. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[AsCommand( + name: 'oai:formats:update', + description: 'Update metadata formats in database from configuration' +)] +class UpdateFormatsCommand extends Command +{ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $formats = Configuration::getInstance()->metadataPrefix; + $inDatabase = Database::getInstance()->getMetadataFormats()->getQueryResult(); + $added = 0; + $deleted = 0; + foreach ($formats as $prefix => $format) { + if (in_array($prefix, array_keys($inDatabase), true)) { + if ( + $format['namespace'] === $inDatabase[$prefix]->getNamespace() + and $format['schema'] === $inDatabase[$prefix]->getSchema() + ) { + continue; + } + } + if (Database::getInstance()->addOrUpdateMetadataFormat($prefix, $format['namespace'], $format['schema'])) { + ++$added; + $output->writeln([ + sprintf( + ' [OK] Metadata format "%s" added or updated successfully. ', + $prefix + ) + ]); + } else { + $output->writeln([ + sprintf( + ' [ERROR] Could not add or update metadata format "%s". ', + $prefix + ) + ]); + } + } + foreach (array_keys($inDatabase) as $prefix) { + if (!in_array($prefix, array_keys($formats), true)) { + if (Database::getInstance()->removeMetadataFormat($prefix)) { + ++$deleted; + $output->writeln([ + sprintf( + ' [OK] Metadata format "%s" and all associated records deleted successfully. ', + $prefix + ) + ]); + } else { + $output->writeln([ + sprintf( + ' [ERROR] Could not delete metadata format "%s". ', + $prefix + ) + ]); + } + } + } + /** @var Application */ + $app = $this->getApplication(); + $app->doRun( + new ArrayInput([ + 'command' => 'orm:clear-cache:result', + '--flush' => true + ]), + new NullOutput() + ); + $currentFormats = array_keys(Database::getInstance()->getMetadataFormats()->getQueryResult()); + if (count($currentFormats) > 0) { + $output->writeln( + [ + '', + ' The following metadata formats are currently supported: ', + ' ======================================================= ', + '', + ' "' . implode('", "', $currentFormats) . '" ', + '' + ], + 1 | 16 + ); + } else { + $output->writeln( + [ + '', + ' [INFO] There are currently no metadata formats supported. ', + ' Please add a metadata prefix to config/config.yml and run ', + ' command "php bin/cli oai:formats:update" again! ', + '' + ], + 1 | 16 + ); + } + return Command::SUCCESS; + } +} diff --git a/src/Database.php b/src/Database.php new file mode 100644 index 0000000..643de54 --- /dev/null +++ b/src/Database.php @@ -0,0 +1,441 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2; + +use DateTime; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\AbstractAsset; +use Doctrine\DBAL\Tools\DsnParser; +use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\Configuration as DoctrineConfiguration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\Proxy\ProxyFactory; +use Doctrine\ORM\Tools\Pagination\Paginator; +use Exception; +use OCC\Basics\Traits\Singleton; +use OCC\OaiPmh2\Database\Format; +use OCC\OaiPmh2\Database\Record; +use OCC\OaiPmh2\Database\Result; +use OCC\OaiPmh2\Database\Set; +use OCC\OaiPmh2\Database\Token; +use Symfony\Component\Cache\Adapter\PhpFilesAdapter; +use Symfony\Component\Filesystem\Path; + +/** + * Handles all database shenanigans. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + * + * @template Formats of array + * @template Records of array + * @template Sets of array + */ +class Database +{ + use Singleton; + + protected const DB_TABLES = [ + 'formats', + 'records', + 'records_sets', + 'sets', + 'tokens' + ]; + + /** + * This holds the Doctrine entity manager. + */ + protected EntityManager $entityManager; + + /** + * Add or update metadata format. + * + * @param string $prefix The metadata prefix + * @param string $namespace The namespace URI + * @param string $schema The schema URL + * + * @return bool Whether the format was inserted/updated successfully + */ + public function addOrUpdateMetadataFormat(string $prefix, string $namespace, string $schema): bool + { + $inDatabase = $this->getMetadataFormats()->getQueryResult(); + if (in_array($prefix, array_keys($inDatabase), true)) { + try { + $dql = $this->entityManager->createQueryBuilder(); + $dql->update(Format::class, 'format') + ->set('format.namespace', ':namepsace') + ->set('format.xmlSchema', ':schema') + ->where($dql->expr()->eq('format.prefix', ':prefix')) + ->setParameter('prefix', $prefix) + ->setParameter('namespace', $namespace) + ->setParameter('schema', $schema); + $query = $dql->getQuery(); + $query->execute(); + return true; + } catch (Exception) { + return false; + } + } else { + try { + $format = new Format($prefix, $namespace, $schema); + $this->entityManager->persist($format); + $this->entityManager->flush(); + return true; + } catch (Exception) { + return false; + } + } + } + + /** + * Get the earliest datestamp of any record. + * + * @return string The earliest datestamp + */ + public function getEarliestDatestamp(): string + { + $timestamp = '0000-00-00T00:00:00Z'; + $dql = $this->entityManager->createQueryBuilder(); + $dql->select('record') + ->from(Record::class, 'record') + ->orderBy('record.lastChanged', 'ASC') + ->setMaxResults(1); + $query = $dql->getQuery(); + $query->enableResultCache(); + /** @var ?array */ + $result = $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY); + if (isset($result)) { + $timestamp = $result['lastChanged']->format('Y-m-d\TH:i:s\Z'); + } + return $timestamp; + } + + /** + * Get the Doctrine entity manager. + * + * @return EntityManager The entity manager instance + */ + public function getEntityManager(): EntityManager + { + return $this->entityManager; + } + + /** + * Get all metadata prefixes. + * + * @param ?string $identifier Optional record identifier + * + * @return Result The metadata prefixes + */ + public function getMetadataFormats(?string $identifier = null): Result + { + $dql = $this->entityManager->createQueryBuilder(); + $dql->select('format') + ->from(Format::class, 'format', 'format.prefix'); + if (isset($identifier)) { + $dql->innerJoin( + 'format.records', + 'records', + 'WITH', + $dql->expr()->andX( + $dql->expr()->eq('records.identifier', ':identifier'), + $dql->expr()->neq('records.data', '') + ) + ) + ->setParameter('identifier', $identifier); + } + $query = $dql->getQuery(); + $query->enableResultCache(); + /** @var Formats */ + $queryResult = $query->getResult(); + return new Result($queryResult); + } + + /** + * Get a single record. + * + * @param string $identifier The record identifier + * @param string $metadataPrefix The metadata prefix + * + * @return ?Record The record or NULL on failure + */ + public function getRecord(string $identifier, string $metadataPrefix): ?Record + { + $dql = $this->entityManager->createQueryBuilder(); + $dql->select('record') + ->from(Record::class, 'record') + ->where($dql->expr()->eq('record.identifier', ':identifier')) + ->andWhere($dql->expr()->eq('record.format', ':format')) + ->setParameter('identifier', $identifier) + ->setParameter('format', $metadataPrefix) + ->setMaxResults(1); + $query = $dql->getQuery(); + /** @var ?Record */ + return $query->getOneOrNullResult(); + } + + /** + * Get list of records. + * + * @param string $verb The currently requested verb ('ListIdentifiers' or 'ListRecords') + * @param string $metadataPrefix The metadata prefix + * @param int $counter Counter for split result sets + * @param ?string $from The "from" datestamp + * @param ?string $until The "until" datestamp + * @param ?string $set The set spec + * + * @return Result The records and possibly a resumtion token + */ + public function getRecords( + string $verb, + string $metadataPrefix, + int $counter = 0, + ?string $from = null, + ?string $until = null, + ?string $set = null + ): Result + { + $maxRecords = Configuration::getInstance()->maxRecords; + $cursor = $counter * $maxRecords; + + $dql = $this->entityManager->createQueryBuilder(); + $dql->select('record') + ->from(Record::class, 'record', 'record.identifier') + ->where($dql->expr()->eq('record.format', ':metadataPrefix')) + ->setParameter('metadataPrefix', $metadataPrefix) + ->setFirstResult($cursor) + ->setMaxResults($maxRecords); + if (isset($from)) { + $dql->andWhere($dql->expr()->gte('record.lastChanged', ':from')); + $dql->setParameter('from', new DateTime($from)); + } + if (isset($until)) { + $dql->andWhere($dql->expr()->lte('record.lastChanged', ':until')); + $dql->setParameter('until', new DateTime($until)); + } + if (isset($set)) { + $dql->andWhere($dql->expr()->in('record.sets', ':set')); + $dql->setParameter('set', $set); + } + $query = $dql->getQuery(); + /** @var Records */ + $queryResult = $query->getResult(); + $result = new Result($queryResult); + $paginator = new Paginator($query, true); + if (count($paginator) > ($cursor + count($result))) { + $token = new Token($verb, [ + 'counter' => $counter + 1, + 'completeListSize' => count($paginator), + 'metadataPrefix' => $metadataPrefix, + 'from' => $from, + 'until' => $until, + 'set' => $set + ]); + $this->entityManager->persist($token); + $this->entityManager->flush(); + $result->setResumptionToken($token); + } + return $result; + } + + /** + * Get resumption token. + * + * @param string $token The token + * @param string $verb The current verb to validate token + * + * @return ?Token The resumption token or NULL if invalid + */ + public function getResumptionToken(string $token, string $verb): ?Token + { + $dql = $this->entityManager->createQueryBuilder(); + $dql->select('token') + ->from(Token::class, 'token') + ->where($dql->expr()->gte('token.validUntil', ':now')) + ->andWhere($dql->expr()->eq('token.token', ':token')) + ->andWhere($dql->expr()->eq('token.verb', ':verb')) + ->setParameter('now', new DateTime()) + ->setParameter('token', $token) + ->setParameter('verb', $verb) + ->setMaxResults(1); + $query = $dql->getQuery(); + /** @var ?Token */ + return $query->getOneOrNullResult(); + } + + /** + * Get all sets. + * + * @param int $counter Counter for split result sets + * + * @return Result The sets and possibly a resumption token + */ + public function getSets($counter = 0): Result + { + $result = []; + $maxRecords = Configuration::getInstance()->maxRecords; + $cursor = $counter * $maxRecords; + + $dql = $this->entityManager->createQueryBuilder(); + $dql->select('sets') + ->from(Set::class, 'sets', 'sets.spec') + ->setFirstResult($cursor) + ->setMaxResults($maxRecords); + $query = $dql->getQuery(); + $query->enableResultCache(); + /** @var Sets */ + $resultQuery = $query->getResult(); + $result = new Result($resultQuery); + $paginator = new Paginator($query, false); + if (count($paginator) > ($cursor + count($result))) { + $token = new Token('ListSets', [ + 'counter' => $counter + 1, + 'completeListSize' => count($paginator) + ]); + $this->entityManager->persist($token); + $this->entityManager->flush(); + $result->setResumptionToken($token); + } + return $result; + } + + /** + * Check if a record identifier exists. + * + * @param string $identifier The record identifier + * + * @return bool Whether the identifier exists + */ + public function idDoesExist(string $identifier): bool + { + $dql = $this->entityManager->createQueryBuilder(); + $dql->select('COUNT(record.identifier)') + ->from(Record::class, 'record') + ->where($dql->expr()->eq('record.identifier', ':identifier')) + ->setParameter('identifier', $identifier) + ->setMaxResults(1); + $query = $dql->getQuery(); + return (bool) $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR); + } + + /** + * Prune expired resumption tokens. + * + * @return int The number of deleted tokens + */ + public function pruneResumptionTokens(): int + { + $dql = $this->entityManager->createQueryBuilder(); + $dql->delete(Token::class, 'token') + ->where($dql->expr()->lt('token.validUntil', ':now')) + ->setParameter('now', new DateTime()); + $query = $dql->getQuery(); + /** @var int */ + return $query->execute(); + } + + /** + * Remove metadata format and all associated records. + * + * @param string $prefix The metadata prefix + * + * @return bool TRUE on success or FALSE on failure + */ + public function removeMetadataFormat(string $prefix): bool + { + $dql = $this->entityManager->createQueryBuilder(); + $dql->delete(Format::class, 'format') + ->where($dql->expr()->eq('format.prefix', ':prefix')) + ->setParameter('prefix', $prefix); + $query = $dql->getQuery(); + try { + $query->execute(); + return true; + } catch (Exception) { + return false; + } + } + + /** + * This is a singleton class, thus the constructor is private. + * + * Usage: Get an instance of this class by calling Database::getInstance() + */ + private function __construct() + { + $configuration = new DoctrineConfiguration(); + $configuration->setAutoGenerateProxyClasses( + ProxyFactory::AUTOGENERATE_NEVER + ); + $configuration->setMetadataCache( + new PhpFilesAdapter( + 'Metadata', + 0, + __DIR__ . '/../var/cache' + ) + ); + $configuration->setMetadataDriverImpl( + new AttributeDriver([__DIR__ . '/Database']) + ); + $configuration->setProxyDir(__DIR__ . '/../var/generated'); + $configuration->setProxyNamespace('OCC\OaiPmh2\Proxy'); + $configuration->setQueryCache( + new PhpFilesAdapter( + 'Query', + 0, + __DIR__ . '/../var/cache' + ) + ); + $configuration->setResultCache( + new PhpFilesAdapter( + 'Result', + 0, + __DIR__ . '/../var/cache' + ) + ); + $configuration->setSchemaAssetsFilter( + static function(string|AbstractAsset $assetName): bool { + if ($assetName instanceof AbstractAsset) { + $assetName = $assetName->getName(); + } + return in_array($assetName, self::DB_TABLES, true); + } + ); + + $baseDir = Path::canonicalize(__DIR__ . '/../'); + $dsn = str_replace('%BASEDIR%', $baseDir, Configuration::getInstance()->database); + $parser = new DsnParser([ + 'mariadb' => 'pdo_mysql', + 'mssql' => 'pdo_sqlsrv', + 'mysql' => 'pdo_mysql', + 'oracle' => 'pdo_oci', + 'postgres' => 'pdo_pgsql', + 'sqlite' => 'pdo_sqlite' + ]); + $connection = DriverManager::getConnection($parser->parse($dsn), $configuration); + + $this->entityManager = new EntityManager($connection, $configuration); + } +} diff --git a/src/Database/Format.php b/src/Database/Format.php new file mode 100644 index 0000000..41a806c --- /dev/null +++ b/src/Database/Format.php @@ -0,0 +1,174 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Database; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Exception\ValidationFailedException; +use Symfony\Component\Validator\Validation; + +/** + * Doctrine/ORM Entity for formats. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[ORM\Entity] +#[ORM\Table(name: 'formats')] +class Format +{ + /** + * The unique metadata prefix. + */ + #[ORM\Id] + #[ORM\Column(type: 'string')] + private string $prefix; + + /** + * The format's namespace URI. + */ + #[ORM\Column(type: 'string')] + private string $namespace; + + /** + * The format's schema URL. + */ + #[ORM\Column(type: 'string')] + private string $xmlSchema; + + /** + * Collection of associated records. + * + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Record::class, mappedBy: 'format')] + private Collection $records; + + /** + * Update bi-directional association with records. + * + * @param Record $record The record to add to this format + * + * @return void + */ + public function addRecord(Record $record): void + { + if (!$this->records->contains($record)) { + $this->records->add($record); + } + } + + /** + * Get the format's namespace URI. + * + * @return string The namespace URI + */ + public function getNamespace(): string + { + return $this->namespace; + } + + /** + * Get the metadata prefix for this format. + * + * @return string The metadata prefix + */ + public function getPrefix(): string + { + return $this->prefix; + } + + /** + * Get a collection of associated records. + * + * @return Collection The associated records + */ + public function getRecords(): Collection + { + return $this->records; + } + + /** + * Get the format's schema URL. + * + * @return string The schema URL + */ + public function getSchema(): string + { + return $this->xmlSchema; + } + + /** + * Update bi-directional association with records. + * + * @param Record $record The record to remove from this metadata prefix + * + * @return void + */ + public function removeRecord(Record $record): void + { + $this->records->removeElement($record); + } + + /** + * Validate namespace and schema URLs. + * + * @param string $url The namespace or schema URL + * + * @return string The validated URL + * + * @throws ValidationFailedException + */ + protected function validate(string $url): string + { + $validator = Validation::createValidator(); + $violations = $validator->validate($url, new Assert\Url()); + if ($violations->count() > 0) { + throw new ValidationFailedException(null, $violations); + } + return $url; + } + + /** + * Get new entity of format. + * + * @param string $prefix The metadata prefix + * @param string $namespace The format's namespace URI + * @param string $schema The format's schema URL + * + * @throws ValidationFailedException + */ + public function __construct(string $prefix, string $namespace, string $schema) + { + try { + $this->prefix = $prefix; + $this->namespace = $this->validate($namespace); + $this->xmlSchema = $this->validate($schema); + $this->records = new ArrayCollection(); + } catch (ValidationFailedException $exception) { + throw $exception; + } + } +} diff --git a/src/Database/Record.php b/src/Database/Record.php new file mode 100644 index 0000000..1d6af91 --- /dev/null +++ b/src/Database/Record.php @@ -0,0 +1,263 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Database; + +use DateTime; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Exception\ValidationFailedException; +use Symfony\Component\Validator\Validation; + +/** + * Doctrine/ORM Entity for records. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[ORM\Entity] +#[ORM\Table(name: 'records')] +class Record +{ + /** + * The record identifier. + */ + #[ORM\Id] + #[ORM\Column(type: 'string')] + private string $identifier; + + /** + * The associated format. + */ + #[ORM\Id] + #[ORM\ManyToOne(targetEntity: Format::class, inversedBy: 'records')] + #[ORM\JoinColumn(name: 'format', referencedColumnName: 'prefix')] + private Format $format; + + /** + * The date and time of last change. + */ + #[ORM\Column(name: 'last_changed', type: 'datetime')] + private DateTime $lastChanged; + + /** + * The record's content. + */ + #[ORM\Column(type: 'text')] + private string $content = ''; + + /** + * Collection of associated sets. + * + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Set::class, inversedBy: 'records', indexBy: 'spec')] + #[ORM\JoinTable(name: 'records_sets')] + #[ORM\JoinColumn(name: 'record_identifier', referencedColumnName: 'identifier')] + #[ORM\JoinColumn(name: 'record_format', referencedColumnName: 'format')] + #[ORM\InverseJoinColumn(name: 'set_spec', referencedColumnName: 'spec')] + private Collection $sets; + + /** + * Get the record's content. + * + * @return string The record's content + */ + public function getContent(): string + { + return $this->content; + } + + /** + * Get the record identifier. + * + * @return string The record identifier + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * Update bi-directional association with format. + * + * @param Format $format The metadata prefix + * + * @return void + */ + private function addFormat(Format $format): void + { + $this->format = $format; + $format->addRecord($this); + } + + /** + * Associate the record with a set. + * + * @param Set $set The set + * + * @return void + */ + public function addSet(Set $set): void + { + if (!$this->sets->contains($set)) { + $this->sets->add($set); + $set->addRecord($this); + } + } + + /** + * Get the associated format. + * + * @return Format The associated format + */ + public function getFormat(): Format + { + return $this->format; + } + + /** + * Get the date and time of last change. + * + * @return DateTime The datetime of last change + */ + public function getLastChanged(): DateTime + { + return $this->lastChanged; + } + + /** + * Get a collection of associated sets. + * + * @return Collection The associated sets + */ + public function getSets(): Collection + { + return $this->sets; + } + + /** + * Remove record from set. + * + * @param Set $set The set + * + * @return void + */ + public function removeSet(Set $set): void + { + if ($this->sets->contains($set)) { + $this->sets->removeElement($set); + $set->removeRecord($this); + } + } + + /** + * Set record's content. + * + * @param string $data The record's content + * @param bool $validate Should the input be validated? + * + * @return void + * + * @throws ValidationFailedException + */ + public function setContent(string $data, bool $validate = true): void + { + $data = trim($data); + if ($validate && $data !== '') { + try { + $data = $this->validate($data); + } catch (ValidationFailedException $exception) { + throw $exception; + } + } + $this->content = $data; + } + + /** + * Set date and time of last change. + * + * @param ?DateTime $dateTime The datetime of last change or NULL for "NOW" + * + * @return void + */ + public function setLastChanged(?DateTime $dateTime = null): void + { + if (!isset($dateTime)) { + $dateTime = new DateTime(); + } + $this->lastChanged = $dateTime; + } + + /** + * Validate XML content. + * + * @param string $xml The XML string + * + * @return string The validated XML string + * + * @throws ValidationFailedException + */ + protected function validate(string $xml): string + { + $validator = Validation::createValidator(); + $violations = $validator->validate($xml, new Assert\Type('string')); + if ($violations->count() > 0) { + throw new ValidationFailedException(null, $violations); + } + return $xml; + } + + /** + * Get new entity of record. + * + * @param string $identifier The record identifier + * @param Format $format The format + * @param string $data The record's content + * + * @throws ValidationFailedException + */ + public function __construct(string $identifier, Format $format, string $data = '') + { + try { + $this->identifier = $identifier; + $this->addFormat($format); + $this->setContent($data); + $this->setLastChanged(); + $this->sets = new ArrayCollection(); + } catch (ValidationFailedException $exception) { + throw $exception; + } + } + + /** + * Get the record's content. + * + * @return string The record's content + */ + public function __toString(): string + { + return $this->content; + } +} diff --git a/src/Database/Result.php b/src/Database/Result.php new file mode 100644 index 0000000..dfddbe8 --- /dev/null +++ b/src/Database/Result.php @@ -0,0 +1,97 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Database; + +use Countable; +use Iterator; +use OCC\Basics\InterfaceTraits\Countable as CountableTrait; +use OCC\Basics\InterfaceTraits\Iterator as IteratorTrait; + +/** + * A database result set with optional resumption token. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + * + * @template QueryResult of array + * @implements Iterator + */ +class Result implements Countable, Iterator +{ + use CountableTrait; + use IteratorTrait; + + /** + * This holds the Doctrine result set. + * + * @var QueryResult + */ + private array $data = []; + + /** + * This holds the optional resumption token. + */ + protected ?Token $resumptionToken; + + /** + * Get the query result. + * + * @return QueryResult The result set + */ + public function getQueryResult(): array + { + return $this->data; + } + + /** + * Get the resumption token. + * + * @return ?Token The resumption token or NULL if not applicable + */ + public function getResumptionToken(): ?Token + { + return $this->resumptionToken; + } + + /** + * Set the resumption token. + * + * @param Token $token The resumption token + * + * @return void + */ + public function setResumptionToken(Token $token): void + { + $this->resumptionToken = $token; + } + + /** + * Create new result set. + * + * @param QueryResult $queryResult The Doctrine result set + */ + public function __construct(array $queryResult) + { + $this->data = $queryResult; + } +} diff --git a/src/Database/Set.php b/src/Database/Set.php new file mode 100644 index 0000000..6126082 --- /dev/null +++ b/src/Database/Set.php @@ -0,0 +1,158 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Database; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; + +/** + * Doctrine/ORM Entity for sets. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[ORM\Entity] +#[ORM\Table(name: 'sets')] +class Set +{ + /** + * The unique set spec. + */ + #[ORM\Id] + #[ORM\Column(type: 'string')] + private string $spec; + + /** + * The name of the set. + */ + #[ORM\Column(type: 'string')] + private string $name; + + /** + * A description of the set. + */ + #[ORM\Column(type: 'text')] + private string $description = ''; + + /** + * Collection of associated records. + * + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Record::class, mappedBy: 'sets')] + private Collection $records; + + /** + * Update bi-directional association with records. + * + * @param Record $record The record to add to this set + * + * @return void + */ + public function addRecord(Record $record): void + { + if (!$this->records->contains($record)) { + $this->records->add($record); + } + } + + /** + * Get the description of this set. + * + * @return string The set description + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * Get the name of this set. + * + * @return string The set name + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get the set spec. + * + * @return string The set spec + */ + public function getSpec(): string + { + return $this->spec; + } + + /** + * Get a collection of associated records. + * + * @return Collection The associated records + */ + public function getRecords(): Collection + { + return $this->records; + } + + /** + * Update bi-directional association with records. + * + * @param Record $record The record to remove from this set + * + * @return void + */ + public function removeRecord(Record $record): void + { + $this->records->removeElement($record); + } + + /** + * Set the description for this set. + * + * @param string $description The description + * + * @return void + */ + public function setDescription(string $description): void + { + $this->description = $description; + } + + /** + * Get new entity of set. + * + * @param string $spec The set spec + * @param string $name The name of the set + * @param string $description The description of the set + */ + public function __construct(string $spec, string $name, string $description = '') + { + $this->spec = $spec; + $this->name = $name; + $this->setDescription($description); + $this->records = new ArrayCollection(); + } +} diff --git a/src/Database/Token.php b/src/Database/Token.php new file mode 100644 index 0000000..94bfe15 --- /dev/null +++ b/src/Database/Token.php @@ -0,0 +1,121 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Database; + +use DateInterval; +use DateTime; +use Doctrine\ORM\Mapping as ORM; +use OCC\OaiPmh2\Configuration; + +/** + * Doctrine/ORM Entity for resumption tokens. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +#[ORM\Entity] +#[ORM\Table(name: 'tokens')] +class Token +{ + /** + * The resumption token. + */ + #[ORM\Id] + #[ORM\Column(type: 'string')] + private string $token; + + /** + * The verb for which the token is issued. + */ + #[ORM\Column(type: 'string')] + private string $verb; + + /** + * The query parameters as serialized array. + */ + #[ORM\Column(type: 'string')] + private string $parameters; + + /** + * The date and time of validity. + */ + #[ORM\Column(name: 'valid_until', type: 'datetime')] + private DateTime $validUntil; + + /** + * Get the resumption token. + * + * @return string The resumption token + */ + public function getToken(): string + { + return $this->token; + } + + /** + * Get the query parameters. + * + * @return array The query parameters + */ + public function getParameters(): array + { + /** @var array */ + return unserialize($this->parameters); + } + + /** + * Get the date and time of validity. + * + * @return DateTime The datetime of validity + */ + public function getValidUntil(): DateTime + { + return $this->validUntil; + } + + /** + * Get the verb for which the token was issued. + * + * @return string The verb + */ + public function getVerb(): string + { + return $this->verb; + } + + /** + * Get new entity of resumption token. + * + * @param string $verb The verb for which the token is issued + * @param array $parameters The query parameters + */ + public function __construct(string $verb, array $parameters) + { + $this->token = substr(md5(microtime()), 0, 8); + $this->verb = $verb; + $this->parameters = serialize($parameters); + $validity = new DateTime(); + $validity->add(new DateInterval('PT' . Configuration::getInstance()->tokenValid . 'S')); + $this->validUntil = $validity; + } +} diff --git a/src/Document.php b/src/Document.php new file mode 100644 index 0000000..7278d8a --- /dev/null +++ b/src/Document.php @@ -0,0 +1,181 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2; + +use DOMDocument; +use DOMElement; +use DOMException; +use DOMNode; +use GuzzleHttp\Psr7\Uri; +use Psr\Http\Message\ServerRequestInterface; + +/** + * An OAI-PMH XML response object. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class Document +{ + /** + * This holds the DOMDocument of the OAI-PMH XML response. + */ + protected DOMDocument $dom; + + /** + * This holds the root node of the OAI-PMH XML response. + */ + protected DOMElement $rootNode; + + /** + * Create a new XML element. + * + * @param string $localName The local name for the element + * @param string $value The optional value for the element + * @param bool $appendToRoot Append the new element to the root node? + * + * @return DOMElement The newly created element + */ + public function createElement(string $localName, string $value = '', bool $appendToRoot = false): DOMElement + { + $node = $this->dom->createElement( + $localName, + htmlspecialchars($value, ENT_XML1, 'UTF-8') + ); + if ($appendToRoot) { + $this->rootNode->appendChild($node); + } + return $node; + } + + /** + * Import XML data into response document. + * + * @param string $data The XML data + * + * @return DOMNode The imported XML node + * + * @throws DOMException + */ + public function importData(string $data): DOMNode + { + $document = new DOMDocument('1.0', 'UTF-8'); + $document->preserveWhiteSpace = false; + if ($document->loadXML($data) === true) { + /** @var DOMElement */ + $rootNode = $document->documentElement; + $node = $this->dom->importNode($rootNode, true); + return $node; + } else { + throw new DOMException( + 'Could not import the XML data. Most likely it is not well-formed.', + 500 + ); + } + } + + /** + * Create an OAI-PMH XML response. + * + * @param ServerRequestInterface $serverRequest The PSR-7 HTTP Server Request + */ + public function __construct(ServerRequestInterface $serverRequest) + { + $uri = $serverRequest->getUri(); + + // Create XML document. + $this->dom = new DOMDocument('1.0', 'UTF-8'); + $this->dom->preserveWhiteSpace = false; + + // Add processing instructions. + $basePath = $uri->getPath(); + if (str_ends_with($basePath, 'index.php')) { + $basePath = pathinfo($basePath, PATHINFO_DIRNAME); + } + $stylesheet = Uri::composeComponents( + $uri->getScheme(), + $uri->getAuthority(), + rtrim($basePath, '/') . '/resources/stylesheet.xsl', + null, + null + ); + $xslt = $this->dom->createProcessingInstruction( + 'xml-stylesheet', + sprintf( + 'type="text/xsl" href="%s"', + $stylesheet + ) + ); + $this->dom->appendChild($xslt); + + // Add root element "OAI-PMH". + $root = $this->dom->createElement('OAI-PMH'); + $this->dom->appendChild($root); + $root->setAttribute( + 'xmlns', + 'http://www.openarchives.org/OAI/2.0/' + ); + $root->setAttribute( + 'xmlns:xsi', + 'http://www.w3.org/2001/XMLSchema-instance' + ); + $root->setAttribute( + 'xsi:schemaLocation', + 'http://www.openarchives.org/OAI/2.0/ https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd' + ); + + // Add element "responseDate". + $responseDate = $this->dom->createElement('responseDate', gmdate('Y-m-d\TH:i:s\Z')); + $root->appendChild($responseDate); + + // Add element "request". + $baseUrl = Uri::composeComponents( + $uri->getScheme(), + $uri->getAuthority(), + $uri->getPath(), + null, + null + ); + $request = $this->dom->createElement('request', $baseUrl); + $root->appendChild($request); + foreach ($serverRequest->getAttributes() as $param => $value) { + $request->setAttribute( + $param, + htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8') + ); + } + + $this->rootNode = $root; + } + + /** + * Serialize the OAI-PMH XML response. + * + * @return string The XML output + */ + public function __toString(): string + { + $this->dom->formatOutput = true; + return (string) $this->dom->saveXML(); + } +} diff --git a/src/Middleware.php b/src/Middleware.php new file mode 100644 index 0000000..5e5602e --- /dev/null +++ b/src/Middleware.php @@ -0,0 +1,80 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2; + +use GuzzleHttp\Psr7\Utils; +use OCC\OaiPmh2\Middleware\ErrorHandler; +use OCC\PSR15\AbstractMiddleware; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Base class for all OAI-PMH requests. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +abstract class Middleware extends AbstractMiddleware +{ + /** + * This holds the prepared response document. + */ + protected Document $preparedResponse; + + /** + * Prepare response document. + * + * @param ServerRequestInterface $request The pre-processed request + * + * @return void + */ + abstract protected function prepareResponse(ServerRequestInterface $request): void; + + /** + * Process an incoming server request. + * + * @param ServerRequestInterface $request The incoming server request + * + * @return ServerRequestInterface The processed server request + */ + protected function processRequest(ServerRequestInterface $request): ServerRequestInterface + { + $this->prepareResponse($request); + return $request; + } + + /** + * Process an incoming response before. + * + * @param ResponseInterface $response The incoming response + * + * @return ResponseInterface The processed response + */ + protected function processResponse(ResponseInterface $response): ResponseInterface + { + if (!ErrorHandler::getInstance()->hasErrors() && isset($this->preparedResponse)) { + $response = $response->withBody(Utils::streamFor((string) $this->preparedResponse)); + } + return $response; + } +} diff --git a/src/Middleware/Dispatcher.php b/src/Middleware/Dispatcher.php new file mode 100644 index 0000000..8f0db37 --- /dev/null +++ b/src/Middleware/Dispatcher.php @@ -0,0 +1,176 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Middleware; + +use OCC\OaiPmh2\Middleware; +use OCC\PSR15\AbstractMiddleware; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Validate and dispatch a OAI-PMH server request. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class Dispatcher extends AbstractMiddleware +{ + /** + * List of defined OAI-PMH parameters. + */ + protected const OAI_PARAMS = [ + 'verb', + 'identifier', + 'metadataPrefix', + 'from', + 'until', + 'set', + 'resumptionToken' + ]; + + /** + * Get server request populated with request attributes. + * + * @param ServerRequestInterface $request The GET or POST request + * + * @return ServerRequestInterface The same request with parsed attributes + */ + protected function getRequestWithAttributes(ServerRequestInterface $request): ServerRequestInterface + { + $arguments = []; + if ($request->getMethod() === 'GET') { + $arguments = $request->getQueryParams(); + } elseif ($request->getMethod() === 'POST') { + if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') { + $arguments = (array) $request->getParsedBody(); + } + } + if ($this->validateArguments($arguments)) { + foreach ($arguments as $param => $value) { + $request = $request->withAttribute($param, $value); + } + } + return $request; + } + + /** + * Dispatch the OAI-PMH request. + * + * @param ServerRequestInterface $request The request to dispatch + * + * @return ServerRequestInterface The processed server request + */ + protected function processRequest(ServerRequestInterface $request): ServerRequestInterface + { + $request = $this->getRequestWithAttributes($request); + if (!ErrorHandler::getInstance()->hasErrors()) { + /** @var Middleware $middleware */ + $middleware = __NAMESPACE__ . '\\' . $request->getAttribute('verb'); + $this->requestHandler->queue->enqueue(new $middleware()); + } + $this->requestHandler->queue->enqueue(ErrorHandler::getInstance()); + return $request; + } + + /** + * Finalize the OAI-PMH response. + * + * @param ResponseInterface $response The response to finalize + * + * @return ResponseInterface The final response + */ + protected function processResponse(ResponseInterface $response): ResponseInterface + { + // TODO: Add support for content compression + // https://openarchives.org/OAI/openarchivesprotocol.html#ResponseCompression + return $response->withHeader('Content-Type', 'text/xml'); + } + + /** + * Validate the request parameters. + * @see https://openarchives.org/OAI/openarchivesprotocol.html#ProtocolMessages + * + * @param string[] $arguments The request parameters + * + * @return bool Whether the parameters are syntactically valid + */ + protected function validateArguments(array $arguments): bool + { + if ( + count(array_diff(array_keys($arguments), self::OAI_PARAMS)) !== 0 + or !isset($arguments['verb']) + ) { + ErrorHandler::getInstance()->withError('badArgument'); + } else { + switch ($arguments['verb']) { + case 'GetRecord': + if ( + count($arguments) !== 3 + or !isset($arguments['identifier']) + or !isset($arguments['metadataPrefix']) + ) { + ErrorHandler::getInstance()->withError('badArgument'); + } + break; + case 'Identify': + if (count($arguments) !== 1) { + ErrorHandler::getInstance()->withError('badArgument'); + } + break; + case 'ListIdentifiers': + case 'ListRecords': + if ( + isset($arguments['metadataPrefix']) + xor isset($arguments['resumptionToken']) + ) { + if ( + (isset($arguments['resumptionToken']) && count($arguments) !== 2) + or isset($arguments['identifier']) + ) { + ErrorHandler::getInstance()->withError('badArgument'); + } + } else { + ErrorHandler::getInstance()->withError('badArgument'); + } + break; + case 'ListMetadataFormats': + if (count($arguments) !== 1) { + if (!isset($arguments['identifier']) || count($arguments) !== 2) { + ErrorHandler::getInstance()->withError('badArgument'); + } + } + break; + case 'ListSets': + if (count($arguments) !== 1) { + if (!isset($arguments['resumptionToken']) || count($arguments) !== 2) { + ErrorHandler::getInstance()->withError('badArgument'); + } + } + break; + default: + ErrorHandler::getInstance()->withError('badVerb'); + } + } + return !ErrorHandler::getInstance()->hasErrors(); + } +} diff --git a/src/Middleware/ErrorHandler.php b/src/Middleware/ErrorHandler.php new file mode 100644 index 0000000..2c76f57 --- /dev/null +++ b/src/Middleware/ErrorHandler.php @@ -0,0 +1,138 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Middleware; + +use DomainException; +use GuzzleHttp\Psr7\Utils; +use OCC\Basics\Traits\Singleton; +use OCC\OaiPmh2\Document; +use OCC\PSR15\AbstractMiddleware; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; + +/** + * Handles OAI-PMH errors. + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class ErrorHandler extends AbstractMiddleware +{ + use Singleton; + + /** + * List of defined OAI-PMH errors. + * @see https://openarchives.org/OAI/openarchivesprotocol.html#ErrorConditions + */ + protected const OAI_ERRORS = [ + 'badArgument' => 'The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax.', + 'badResumptionToken' => 'The value of the resumptionToken argument is invalid or expired.', + 'badVerb' => 'Value of the verb argument is not a legal OAI-PMH verb, the verb argument is missing, or the verb argument is repeated.', + 'cannotDisseminateFormat' => 'The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository.', + 'idDoesNotExist' => 'The value of the identifier argument is unknown or illegal in this repository.', + 'noRecordsMatch' => 'The combination of the values of the from, until, set and metadataPrefix arguments results in an empty list.', + 'noMetadataFormats' => 'There are no metadata formats available for the specified item.', + 'noSetHierarchy' => 'The repository does not support sets.' + ]; + + /** + * The current error codes. + * + * @var string[] $errors + */ + protected array $errors = []; + + /** + * Prepare the response body. + * + * @return StreamInterface The response body stream + */ + protected function getResponseBody(): StreamInterface + { + $document = new Document($this->requestHandler->request); + foreach (array_unique($this->errors) as $errorCode) { + $error = $document->createElement('error', self::OAI_ERRORS[$errorCode], true); + $error->setAttribute('code', $errorCode); + } + return Utils::streamFor((string) $document); + } + + /** + * Check if currently there are errors to handle. + * + * @return bool Whether the error handler has any errors registered + */ + public function hasErrors(): bool + { + return (bool) count($this->errors); + } + + /** + * Generate an error response if errors occured. + * + * @param ResponseInterface $response The incoming response + * + * @return ResponseInterface The error response + */ + protected function processResponse(ResponseInterface $response): ResponseInterface + { + if ($this->hasErrors()) { + $response = $response->withBody($this->getResponseBody()); + } + return $response; + } + + /** + * Delegate an OAI-PMH error to the error handler. + * + * @param string $errorCode The error code to handle + * + * @return ErrorHandler The ErrorHandler instance + * + * @throws DomainException + */ + public function withError(string $errorCode): ErrorHandler + { + if (in_array($errorCode, array_keys(self::OAI_ERRORS), true)) { + $this->errors[] = $errorCode; + } else { + throw new DomainException( + sprintf( + 'Valid OAI-PMH error code expected, "%s" given.', + $errorCode + ), + 500 + ); + } + return $this; + } + + /** + * This is a singleton class, thus the constructor is private. + * + * Usage: Get an instance by calling ErrorHandler::getInstance() + */ + private function __construct() + { + } +} diff --git a/src/Middleware/GetRecord.php b/src/Middleware/GetRecord.php new file mode 100644 index 0000000..3d08597 --- /dev/null +++ b/src/Middleware/GetRecord.php @@ -0,0 +1,93 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Middleware; + +use OCC\OaiPmh2\Database; +use OCC\OaiPmh2\Document; +use OCC\OaiPmh2\Middleware; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Process the "GetRecord" request. + * @see https://www.openarchives.org/OAI/openarchivesprotocol.html#GetRecord + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class GetRecord extends Middleware +{ + /** + * Prepare the response body for verb "GetRecord". + * + * @param ServerRequestInterface $request The incoming request + * + * @return void + */ + protected function prepareResponse(ServerRequestInterface $request): void + { + $params = $request->getAttributes(); + $oaiRecord = Database::getInstance()->getRecord($params['identifier'], $params['metadataPrefix']); + + if (!isset($oaiRecord)) { + if (Database::getInstance()->idDoesExist($params['identifier'])) { + ErrorHandler::getInstance()->withError('cannotDisseminateFormat'); + } else { + ErrorHandler::getInstance()->withError('idDoesNotExist'); + } + return; + } + + $document = new Document($request); + $getRecord = $document->createElement('GetRecord', '', true); + + $record = $document->createElement('record'); + $getRecord->appendChild($record); + + $header = $document->createElement('header'); + if ($oaiRecord->getContent() === '') { + $header->setAttribute('status', 'deleted'); + } + $record->appendChild($header); + + $identifier = $document->createElement('identifier', $oaiRecord->getIdentifier()); + $header->appendChild($identifier); + + $datestamp = $document->createElement('datestamp', $oaiRecord->getLastChanged()->format('Y-m-d\TH:i:s\Z')); + $header->appendChild($datestamp); + + foreach ($oaiRecord->getSets() as $set) { + $setSpec = $document->createElement('setSpec', $set->getName()); + $header->appendChild($setSpec); + } + + if ($oaiRecord->getContent() !== '') { + $metadata = $document->createElement('metadata'); + $record->appendChild($metadata); + + $data = $document->importData($oaiRecord->getContent()); + $metadata->appendChild($data); + } + + $this->preparedResponse = $document; + } +} diff --git a/src/Middleware/Identify.php b/src/Middleware/Identify.php new file mode 100644 index 0000000..ffab658 --- /dev/null +++ b/src/Middleware/Identify.php @@ -0,0 +1,90 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Middleware; + +use GuzzleHttp\Psr7\Uri; +use OCC\OaiPmh2\Configuration; +use OCC\OaiPmh2\Database; +use OCC\OaiPmh2\Document; +use OCC\OaiPmh2\Middleware; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Process the "Identify" request. + * @see https://www.openarchives.org/OAI/openarchivesprotocol.html#Identify + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class Identify extends Middleware +{ + /** + * Prepare the response body for verb "Identify". + * + * @param ServerRequestInterface $request The incoming request + * + * @return void + */ + protected function prepareResponse(ServerRequestInterface $request): void + { + $document = new Document($request); + $identify = $document->createElement('Identify', '', true); + + $name = Configuration::getInstance()->repositoryName; + $repositoryName = $document->createElement('repositoryName', $name); + $identify->appendChild($repositoryName); + + $uri = Uri::composeComponents( + $request->getUri()->getScheme(), + $request->getUri()->getAuthority(), + $request->getUri()->getPath(), + null, + null + ); + $baseURL = $document->createElement('baseURL', $uri); + $identify->appendChild($baseURL); + + $protocolVersion = $document->createElement('protocolVersion', '2.0'); + $identify->appendChild($protocolVersion); + + $email = Configuration::getInstance()->adminEmail; + $adminEmail = $document->createElement('adminEmail', $email); + $identify->appendChild($adminEmail); + + $datestamp = Database::getInstance()->getEarliestDatestamp(); + $earliestDatestamp = $document->createElement('earliestDatestamp', $datestamp); + $identify->appendChild($earliestDatestamp); + + $deletedRecord = $document->createElement('deletedRecord', 'transient'); + $identify->appendChild($deletedRecord); + + $granularity = $document->createElement('granularity', 'YYYY-MM-DDThh:mm:ssZ'); + $identify->appendChild($granularity); + + // TODO: Add support for content compression + // $compression = $document->createElement('compression', '...'); + // $identify->appendChild($compression); + + $this->preparedResponse = $document; + } +} diff --git a/src/Middleware/ListIdentifiers.php b/src/Middleware/ListIdentifiers.php new file mode 100644 index 0000000..a3133d6 --- /dev/null +++ b/src/Middleware/ListIdentifiers.php @@ -0,0 +1,156 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Middleware; + +use OCC\OaiPmh2\Configuration; +use OCC\OaiPmh2\Database; +use OCC\OaiPmh2\Database\Record; +use OCC\OaiPmh2\Document; +use OCC\OaiPmh2\Middleware; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Process the "ListIdentifiers" request. + * @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListIdentifiers + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class ListIdentifiers extends Middleware +{ + /** + * Prepare the response body for verb "ListIdentifiers" and "ListRecords". + * + * @param ServerRequestInterface $request The incoming request + * + * @return void + */ + protected function prepareResponse(ServerRequestInterface $request): void + { + $counter = 0; + $completeListSize = 0; + $maxRecords = Configuration::getInstance()->maxRecords; + + $params = $request->getAttributes(); + $verb = $params['verb']; + $metadataPrefix = $params['metadataPrefix'] ?? ''; + $from = $params['from'] ?? null; + $until = $params['until'] ?? null; + $set = $params['set'] ?? null; + $resumptionToken = $params['resumptionToken'] ?? null; + + if (isset($resumptionToken)) { + $oldToken = Database::getInstance()->getResumptionToken($resumptionToken, $verb); + if (!isset($oldToken)) { + ErrorHandler::getInstance()->withError('badResumptionToken'); + return; + } else { + foreach ($oldToken->getParameters() as $key => $value) { + $$key = $value; + } + } + } + $prefixes = Database::getInstance()->getMetadataFormats(); + if (!in_array($metadataPrefix, array_keys($prefixes->getQueryResult()), true)) { + ErrorHandler::getInstance()->withError('cannotDisseminateFormat'); + return; + } + if (isset($set)) { + $sets = Database::getInstance()->getSets(); + if (!in_array($set, array_keys($sets->getQueryResult()), true)) { + ErrorHandler::getInstance()->withError('noSetHierarchy'); + return; + } + } + + $records = Database::getInstance()->getRecords($verb, $metadataPrefix, $counter, $from, $until, $set); + if (count($records) === 0) { + ErrorHandler::getInstance()->withError('noRecordsMatch'); + return; + } elseif ($records->getResumptionToken() !== null) { + $newToken = $records->getResumptionToken(); + $completeListSize = $newToken->getParameters()['completeListSize']; + } + + $document = new Document($request); + $list = $document->createElement($verb, '', true); + + /** @var Record $oaiRecord */ + foreach ($records as $oaiRecord) { + if ($verb === 'ListIdentifiers') { + $baseNode = $list; + } else { + $record = $document->createElement('record'); + $list->appendChild($record); + $baseNode = $record; + } + + $header = $document->createElement('header'); + if ($oaiRecord->getContent() === '') { + $header->setAttribute('status', 'deleted'); + } + $baseNode->appendChild($header); + + $identifier = $document->createElement('identifier', $oaiRecord->getIdentifier()); + $header->appendChild($identifier); + + $datestamp = $document->createElement('datestamp', $oaiRecord->getLastChanged()->format('Y-m-d\TH:i:s\Z')); + $header->appendChild($datestamp); + + foreach ($oaiRecord->getSets() as $oaiSet) { + $setSpec = $document->createElement('setSpec', $oaiSet->getName()); + $header->appendChild($setSpec); + } + + if ($verb === 'ListRecords' && $oaiRecord->getContent() !== '') { + $metadata = $document->createElement('metadata'); + $baseNode->appendChild($metadata); + + $data = $document->importData($oaiRecord->getContent()); + $metadata->appendChild($data); + } + } + + if (isset($oldToken) || isset($newToken)) { + $resumptionToken = $document->createElement('resumptionToken'); + $list->appendChild($resumptionToken); + if (isset($newToken)) { + $resumptionToken->nodeValue = $newToken->getToken(); + $resumptionToken->setAttribute( + 'expirationDate', + $newToken->getValidUntil()->format('Y-m-d\TH:i:s\Z') + ); + } + $resumptionToken->setAttribute( + 'completeListSize', + (string) $completeListSize + ); + $resumptionToken->setAttribute( + 'cursor', + (string) ($counter * $maxRecords) + ); + } + + $this->preparedResponse = $document; + } +} diff --git a/src/Middleware/ListMetadataFormats.php b/src/Middleware/ListMetadataFormats.php new file mode 100644 index 0000000..b1288c9 --- /dev/null +++ b/src/Middleware/ListMetadataFormats.php @@ -0,0 +1,82 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Middleware; + +use OCC\OaiPmh2\Database; +use OCC\OaiPmh2\Database\Format; +use OCC\OaiPmh2\Document; +use OCC\OaiPmh2\Middleware; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Process the "ListMetadataFormats" request. + * @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListMetadataFormats + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class ListMetadataFormats extends Middleware +{ + /** + * Prepare the response body for verb "ListMetadataFormats". + * + * @param ServerRequestInterface $request The incoming request + * + * @return void + */ + protected function prepareResponse(ServerRequestInterface $request): void + { + /** @var ?string */ + $identifier = $request->getAttribute('identifier'); + $formats = Database::getInstance()->getMetadataFormats($identifier); + + if (count($formats) === 0) { + if (!isset($identifier) || Database::getInstance()->idDoesExist($identifier)) { + ErrorHandler::getInstance()->withError('noMetadataFormats'); + } else { + ErrorHandler::getInstance()->withError('idDoesNotExist'); + } + return; + } + + $document = new Document($request); + $listMetadataFormats = $document->createElement('ListMetadataFormats', '', true); + + /** @var Format $oaiFormat */ + foreach ($formats as $oaiFormat) { + $metadataFormat = $document->createElement('metadataFormat'); + $listMetadataFormats->appendChild($metadataFormat); + + $metadataPrefix = $document->createElement('metadataPrefix', $oaiFormat->getPrefix()); + $metadataFormat->appendChild($metadataPrefix); + + $schema = $document->createElement('schema', $oaiFormat->getSchema()); + $metadataFormat->appendChild($schema); + + $metadataNamespace = $document->createElement('metadataNamespace', $oaiFormat->getNamespace()); + $metadataFormat->appendChild($metadataNamespace); + } + + $this->preparedResponse = $document; + } +} diff --git a/src/Middleware/ListRecords.php b/src/Middleware/ListRecords.php new file mode 100644 index 0000000..a75c875 --- /dev/null +++ b/src/Middleware/ListRecords.php @@ -0,0 +1,39 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Middleware; + +/** + * Process the "ListRecords" request. + * @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListRecords + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + */ +class ListRecords extends ListIdentifiers +{ + /** + * "ListIdentifiers" and "ListRecords" are practically identical except the + * former returns the header information only while the latter also returns + * the records' data. + */ +} diff --git a/src/Middleware/ListSets.php b/src/Middleware/ListSets.php new file mode 100644 index 0000000..9a34b6e --- /dev/null +++ b/src/Middleware/ListSets.php @@ -0,0 +1,122 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\OaiPmh2\Middleware; + +use OCC\OaiPmh2\Configuration; +use OCC\OaiPmh2\Database; +use OCC\OaiPmh2\Database\Set; +use OCC\OaiPmh2\Database\Token; +use OCC\OaiPmh2\Document; +use OCC\OaiPmh2\Middleware; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Process the "ListSets" request. + * @see https://openarchives.org/OAI/openarchivesprotocol.html#ListSets + * + * @author Sebastian Meyer + * @package opencultureconsulting/oai-pmh2 + * + * @template Sets of array + */ +class ListSets extends Middleware +{ + /** + * Prepare the response body for verb "ListSets". + * + * @param ServerRequestInterface $request The incoming request + * + * @return void + */ + protected function prepareResponse(ServerRequestInterface $request): void + { + $counter = 0; + $completeListSize = 0; + $maxRecords = Configuration::getInstance()->maxRecords; + + /** @var ?string */ + $token = $request->getAttribute('resumptionToken'); + if (isset($token)) { + $oldToken = Database::getInstance()->getResumptionToken($token, 'ListSets'); + if (!isset($oldToken)) { + ErrorHandler::getInstance()->withError('badResumptionToken'); + return; + } else { + foreach ($oldToken->getParameters() as $key => $value) { + $$key = $value; + } + } + } + + $sets = Database::getInstance()->getSets($counter); + if (count($sets) === 0) { + ErrorHandler::getInstance()->withError('noSetHierarchy'); + return; + } elseif ($sets->getResumptionToken() !== null) { + $newToken = $sets->getResumptionToken(); + $completeListSize = $newToken->getParameters()['completeListSize']; + } + + $document = new Document($request); + $list = $document->createElement('ListSets', '', true); + + /** @var Set $oaiSet */ + foreach ($sets as $oaiSet) { + $set = $document->createElement('set'); + $list->appendChild($set); + + $setSpec = $document->createElement('setSpec', $oaiSet->getSpec()); + $set->appendChild($setSpec); + + $setName = $document->createElement('setName', $oaiSet->getName()); + $set->appendChild($setName); + + if ($oaiSet->getDescription() !== '') { + $setDescription = $document->createElement('setDescription', $oaiSet->getDescription()); + $set->appendChild($setDescription); + } + } + + if (isset($oldToken) || isset($newToken)) { + $resumptionToken = $document->createElement('resumptionToken'); + $list->appendChild($resumptionToken); + if (isset($newToken)) { + $resumptionToken->nodeValue = $newToken->getToken(); + $resumptionToken->setAttribute( + 'expirationDate', + $newToken->getValidUntil()->format('Y-m-d\TH:i:s\Z') + ); + } + $resumptionToken->setAttribute( + 'completeListSize', + (string) $completeListSize + ); + $resumptionToken->setAttribute( + 'cursor', + (string) ($counter * $maxRecords) + ); + } + + $this->preparedResponse = $document; + } +}