diff --git a/.phpmd.dist.xml b/.phpmd.dist.xml
index 55da42a..59a1afd 100644
--- a/.phpmd.dist.xml
+++ b/.phpmd.dist.xml
@@ -7,20 +7,35 @@
xsi:noNamespaceSchemaLocation="
http://pmd.sf.net/ruleset_xml_schema.xsd">
- Open Culture Consulting follows PHP Mess Detector standards.
+ Open Culture Consulting follows PHP Mess Detector standards with few exceptions.
+
-
+
-
+
+
+
+
+
+
+
+
+
-
+
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 17bb0a6..b724bf4 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
-office@opencultureconsulting.com.
+.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e4b8f8f..830fc30 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,3 +1,9 @@
-Please read the excellent [GitHub Open Source Guide](https://opensource.guide/how-to-contribute/) on *How to Contribute on Open Source*.
+Please read the excellent [GitHub Open Source Guide][guide] on *How to
+Contribute on Open Source*.
-If you have any further questions just [open a new issue](https://github.com/opencultureconsulting/oai-pmh2/issues/new) and I'll be happy to assist!
+[guide]:
+
+If you have any further questions just [open a new issue][issuetracker] and
+I'll be happy to assist!
+
+[issuetracker]:
diff --git a/README.md b/README.md
index 3f4e3bd..225d037 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# oai-pmh2
\ No newline at end of file
+# oai-pmh2
diff --git a/bin/cli b/bin/cli
index e6f3dd3..782b293 100644
--- a/bin/cli
+++ b/bin/cli
@@ -49,7 +49,7 @@ $commands = [
try {
ConsoleRunner::run(
new SingleManagerProvider(
- Database::getInstance()->getEntityManager()
+ EntityManager::getInstance()
),
$commands
);
diff --git a/composer.json b/composer.json
index bf45fdc..bd961d9 100644
--- a/composer.json
+++ b/composer.json
@@ -31,10 +31,10 @@
"ext-dom": "*",
"ext-libxml": "*",
"ext-sqlite3": "*",
- "doctrine/dbal": "^3.8",
+ "doctrine/dbal": "^4.1",
"doctrine/orm": "^3.2",
- "opencultureconsulting/basics": "^1.1",
- "opencultureconsulting/psr15": "^1.0",
+ "opencultureconsulting/basics": "^2.1",
+ "opencultureconsulting/psr15": "^1.2",
"symfony/cache": "^6.4",
"symfony/console": "^6.4",
"symfony/filesystem": "^6.4",
@@ -44,11 +44,13 @@
"require-dev": {
"phpdocumentor/shim": "^3.5",
"phpmd/phpmd": "^2.15",
- "phpstan/phpstan": "^1.11",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-doctrine": "^1.5",
"phpstan/phpstan-strict-rules": "^1.6",
- "friendsofphp/php-cs-fixer": "^3.59",
+ "phpstan/phpstan-symfony": "^1.4",
+ "friendsofphp/php-cs-fixer": "^3.64",
"squizlabs/php_codesniffer": "^3.10",
- "vimeo/psalm": "^5.25"
+ "vimeo/psalm": "^5.26"
},
"autoload": {
"psr-4": {
@@ -74,7 +76,7 @@
],
"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:query",
"@php bin/cli orm:clear-cache:result --flush"
],
"doctrine:initialize-database": [
@@ -95,7 +97,7 @@
],
"phpmd:check": [
"@php -r \"if (!file_exists('./.phpmd.xml')) { copy('./.phpmd.dist.xml', './.phpmd.xml'); }\"",
- "@php vendor/bin/phpmd ./bin,./public,./src ansi .phpmd.xml --cache"
+ "@php vendor/bin/phpmd bin/,public/,src/ ansi .phpmd.xml --cache"
],
"phpstan:check": [
"@php vendor/bin/phpstan"
diff --git a/composer.lock b/composer.lock
index 6356807..991f815 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,101 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "176c33af04203780cfc99ddb9b4742f6",
+ "content-hash": "511dc23587fc62a89af37b88169e867a",
"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.2.2",
@@ -187,47 +94,42 @@
},
{
"name": "doctrine/dbal",
- "version": "3.8.7",
+ "version": "4.1.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
- "reference": "2093d670ca17f634f3c095ec10a20687eccebd99"
+ "reference": "7a8252418689feb860ea8dfeab66d64a56a64df8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/2093d670ca17f634f3c095ec10a20687eccebd99",
- "reference": "2093d670ca17f634f3c095ec10a20687eccebd99",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/7a8252418689feb860ea8dfeab66d64a56a64df8",
+ "reference": "7a8252418689feb860ea8dfeab66d64a56a64df8",
"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",
+ "php": "^8.1",
"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.11.7",
+ "jetbrains/phpstorm-stubs": "2023.2",
+ "phpstan/phpstan": "1.12.0",
+ "phpstan/phpstan-phpunit": "1.4.0",
"phpstan/phpstan-strict-rules": "^1.6",
- "phpunit/phpunit": "9.6.20",
- "psalm/plugin-phpunit": "0.18.4",
+ "phpunit/phpunit": "10.5.30",
+ "psalm/plugin-phpunit": "0.19.0",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.10.2",
- "symfony/cache": "^5.4|^6.0|^7.0",
- "symfony/console": "^4.4|^5.4|^6.0|^7.0",
- "vimeo/psalm": "4.30.0"
+ "symfony/cache": "^6.3.8|^7.0",
+ "symfony/console": "^5.4|^6.3|^7.0",
+ "vimeo/psalm": "5.25.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": {
@@ -280,7 +182,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
- "source": "https://github.com/doctrine/dbal/tree/3.8.7"
+ "source": "https://github.com/doctrine/dbal/tree/4.1.1"
},
"funding": [
{
@@ -296,7 +198,7 @@
"type": "tidelift"
}
],
- "time": "2024-08-07T11:57:25+00:00"
+ "time": "2024-09-03T08:58:39+00:00"
},
{
"name": "doctrine/deprecations",
@@ -676,16 +578,16 @@
},
{
"name": "doctrine/orm",
- "version": "3.2.1",
+ "version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/orm.git",
- "reference": "722cea6536775206e81744542b36fa7c9a4ea3e5"
+ "reference": "831a1eb7d260925528cdbb49cc1866c0357cf147"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/orm/zipball/722cea6536775206e81744542b36fa7c9a4ea3e5",
- "reference": "722cea6536775206e81744542b36fa7c9a4ea3e5",
+ "url": "https://api.github.com/repos/doctrine/orm/zipball/831a1eb7d260925528cdbb49cc1866c0357cf147",
+ "reference": "831a1eb7d260925528cdbb49cc1866c0357cf147",
"shasum": ""
},
"require": {
@@ -758,9 +660,9 @@
],
"support": {
"issues": "https://github.com/doctrine/orm/issues",
- "source": "https://github.com/doctrine/orm/tree/3.2.1"
+ "source": "https://github.com/doctrine/orm/tree/3.2.2"
},
- "time": "2024-06-26T21:48:58+00:00"
+ "time": "2024-08-23T10:03:52+00:00"
},
{
"name": "doctrine/persistence",
@@ -977,25 +879,29 @@
},
{
"name": "opencultureconsulting/basics",
- "version": "v1.1.0",
+ "version": "v2.1.2",
"source": {
"type": "git",
"url": "https://github.com/opencultureconsulting/php-basics.git",
- "reference": "f832b58a3e18b7c142401d2d3e0ce505ea6c2a6b"
+ "reference": "6abfd3b050344dde4f8ba12e07cac355ff3ab223"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/opencultureconsulting/php-basics/zipball/f832b58a3e18b7c142401d2d3e0ce505ea6c2a6b",
- "reference": "f832b58a3e18b7c142401d2d3e0ce505ea6c2a6b",
+ "url": "https://api.github.com/repos/opencultureconsulting/php-basics/zipball/6abfd3b050344dde4f8ba12e07cac355ff3ab223",
+ "reference": "6abfd3b050344dde4f8ba12e07cac355ff3ab223",
"shasum": ""
},
"require": {
- "php": "^8.0"
+ "php": "^8.1"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.48",
- "phpstan/phpstan": "^1.10.56",
- "phpstan/phpstan-strict-rules": "^1.5"
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "phpdocumentor/shim": "^3.5",
+ "phpmd/phpmd": "^2.15",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-strict-rules": "^1.6",
+ "squizlabs/php_codesniffer": "^3.10",
+ "vimeo/psalm": "^5.25"
},
"type": "library",
"autoload": {
@@ -1015,23 +921,30 @@
"role": "maintainer"
}
],
- "description": "This is a collection of generic Classes and useful Traits for PHP projects.",
- "homepage": "https://github.com/opencultureconsulting/php-basics",
+ "description": "A collection of generic classes and useful traits for PHP projects.",
+ "homepage": "https://opencultureconsulting.github.io/php-basics/",
"keywords": [
"ArrayAccess",
"IteratorAggregate",
+ "OverloadingGetter",
+ "OverloadingSetter",
+ "StrictArray",
+ "StrictCollection",
"StrictList",
"StrictQueue",
"StrictStack",
+ "TriggerExceptionError",
"countable",
"getter",
"iterator",
+ "overloading",
"setter",
"singleton",
- "throwErrorException"
+ "throwErrorException",
+ "typechecker"
],
"support": {
- "docs": "https://github.com/opencultureconsulting/php-basics/blob/main/README.md",
+ "docs": "https://opencultureconsulting.github.io/php-basics/",
"issues": "https://github.com/opencultureconsulting/php-basics/issues",
"source": "https://github.com/opencultureconsulting/php-basics"
},
@@ -1045,26 +958,26 @@
"type": "github"
}
],
- "time": "2024-01-22T20:03:54+00:00"
+ "time": "2024-09-07T19:31:09+00:00"
},
{
"name": "opencultureconsulting/psr15",
- "version": "v1.0.1",
+ "version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/opencultureconsulting/psr-15.git",
- "reference": "e473341f46ecf021e3e729a9fa5de0efa6f7068e"
+ "reference": "8b51cfccb4d9aa364eec1971b4c554c9236ded00"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/opencultureconsulting/psr-15/zipball/e473341f46ecf021e3e729a9fa5de0efa6f7068e",
- "reference": "e473341f46ecf021e3e729a9fa5de0efa6f7068e",
+ "url": "https://api.github.com/repos/opencultureconsulting/psr-15/zipball/8b51cfccb4d9aa364eec1971b4c554c9236ded00",
+ "reference": "8b51cfccb4d9aa364eec1971b4c554c9236ded00",
"shasum": ""
},
"require": {
- "guzzlehttp/psr7": "^2.6",
- "opencultureconsulting/basics": "^1.1",
- "php": "^8.0",
+ "guzzlehttp/psr7": "^2.7",
+ "opencultureconsulting/basics": "^2.1",
+ "php": "^8.1",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0"
},
@@ -1073,9 +986,13 @@
"psr/http-server-middleware-implementation": "1.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.48",
- "phpstan/phpstan": "^1.10",
- "phpstan/phpstan-strict-rules": "^1.5"
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "phpdocumentor/shim": "^3.5",
+ "phpmd/phpmd": "^2.15",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-strict-rules": "^1.6",
+ "squizlabs/php_codesniffer": "^3.10",
+ "vimeo/psalm": "^5.25"
},
"type": "library",
"autoload": {
@@ -1104,10 +1021,11 @@
"http-server-middleware",
"middleware",
"psr-15",
+ "queue",
"request"
],
"support": {
- "docs": "https://github.com/opencultureconsulting/psr-15/blob/main/README.md",
+ "docs": "https://opencultureconsulting.github.io/psr-15/",
"issues": "https://github.com/opencultureconsulting/psr-15/issues",
"source": "https://github.com/opencultureconsulting/psr-15"
},
@@ -1121,7 +1039,7 @@
"type": "github"
}
],
- "time": "2024-01-22T20:13:28+00:00"
+ "time": "2024-09-07T17:45:23+00:00"
},
{
"name": "psr/cache",
@@ -1448,16 +1366,16 @@
},
{
"name": "psr/log",
- "version": "3.0.0",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
- "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
@@ -1492,9 +1410,9 @@
"psr-3"
],
"support": {
- "source": "https://github.com/php-fig/log/tree/3.0.0"
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
},
- "time": "2021-07-14T16:46:02+00:00"
+ "time": "2024-09-11T13:17:53+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -1542,16 +1460,16 @@
},
{
"name": "symfony/cache",
- "version": "v6.4.10",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
- "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc"
+ "reference": "a463451b7f6ac4a47b98dbfc78ec2d3560c759d8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/cache/zipball/6702d2d777260e6ff3451fee2d7d78ab5f715cdc",
- "reference": "6702d2d777260e6ff3451fee2d7d78ab5f715cdc",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/a463451b7f6ac4a47b98dbfc78ec2d3560c759d8",
+ "reference": "a463451b7f6ac4a47b98dbfc78ec2d3560c759d8",
"shasum": ""
},
"require": {
@@ -1618,7 +1536,7 @@
"psr6"
],
"support": {
- "source": "https://github.com/symfony/cache/tree/v6.4.10"
+ "source": "https://github.com/symfony/cache/tree/v6.4.12"
},
"funding": [
{
@@ -1634,7 +1552,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-17T06:05:49+00:00"
+ "time": "2024-09-16T16:01:33+00:00"
},
{
"name": "symfony/cache-contracts",
@@ -1714,16 +1632,16 @@
},
{
"name": "symfony/console",
- "version": "v6.4.10",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc"
+ "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc",
- "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc",
+ "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765",
+ "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765",
"shasum": ""
},
"require": {
@@ -1788,7 +1706,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v6.4.10"
+ "source": "https://github.com/symfony/console/tree/v6.4.12"
},
"funding": [
{
@@ -1804,7 +1722,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-26T12:30:32+00:00"
+ "time": "2024-09-20T08:15:52+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -1875,16 +1793,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v6.4.9",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "b51ef8059159330b74a4d52f68e671033c0fe463"
+ "reference": "f810e3cbdf7fdc35983968523d09f349fa9ada12"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463",
- "reference": "b51ef8059159330b74a4d52f68e671033c0fe463",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/f810e3cbdf7fdc35983968523d09f349fa9ada12",
+ "reference": "f810e3cbdf7fdc35983968523d09f349fa9ada12",
"shasum": ""
},
"require": {
@@ -1921,7 +1839,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v6.4.9"
+ "source": "https://github.com/symfony/filesystem/tree/v6.4.12"
},
"funding": [
{
@@ -1937,24 +1855,24 @@
"type": "tidelift"
}
],
- "time": "2024-06-28T09:49:33+00:00"
+ "time": "2024-09-16T16:01:33+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "0424dff1c58f028c451efff2045f5d92410bd540"
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
- "reference": "0424dff1c58f028c451efff2045f5d92410bd540",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
@@ -2000,7 +1918,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
@@ -2016,24 +1934,24 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
- "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
@@ -2078,7 +1996,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
},
"funding": [
{
@@ -2094,24 +2012,24 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
- "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
@@ -2159,7 +2077,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
},
"funding": [
{
@@ -2175,24 +2093,24 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
+ "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
- "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
+ "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
@@ -2239,7 +2157,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
@@ -2255,24 +2173,24 @@
"type": "tidelift"
}
],
- "time": "2024-06-19T12:30:46+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9"
+ "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
- "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
+ "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"type": "library",
"extra": {
@@ -2315,7 +2233,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
},
"funding": [
{
@@ -2331,7 +2249,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-19T12:35:24+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/service-contracts",
@@ -2418,16 +2336,16 @@
},
{
"name": "symfony/string",
- "version": "v6.4.10",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "ccf9b30251719567bfd46494138327522b9a9446"
+ "reference": "f8a1ccebd0997e16112dfecfd74220b78e5b284b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446",
- "reference": "ccf9b30251719567bfd46494138327522b9a9446",
+ "url": "https://api.github.com/repos/symfony/string/zipball/f8a1ccebd0997e16112dfecfd74220b78e5b284b",
+ "reference": "f8a1ccebd0997e16112dfecfd74220b78e5b284b",
"shasum": ""
},
"require": {
@@ -2484,7 +2402,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v6.4.10"
+ "source": "https://github.com/symfony/string/tree/v6.4.12"
},
"funding": [
{
@@ -2500,7 +2418,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-22T10:21:14+00:00"
+ "time": "2024-09-20T08:15:52+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -2582,16 +2500,16 @@
},
{
"name": "symfony/validator",
- "version": "v6.4.10",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/validator.git",
- "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd"
+ "reference": "6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/validator/zipball/bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd",
- "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd",
+ "url": "https://api.github.com/repos/symfony/validator/zipball/6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0",
+ "reference": "6da1f0a1ee73d060a411d832cbe0539cfe9bbaa0",
"shasum": ""
},
"require": {
@@ -2659,7 +2577,7 @@
"description": "Provides tools to validate values",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/validator/tree/v6.4.10"
+ "source": "https://github.com/symfony/validator/tree/v6.4.12"
},
"funding": [
{
@@ -2675,7 +2593,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-26T12:30:32+00:00"
+ "time": "2024-09-20T08:18:25+00:00"
},
{
"name": "symfony/var-exporter",
@@ -2756,16 +2674,16 @@
},
{
"name": "symfony/yaml",
- "version": "v6.4.8",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "52903de178d542850f6f341ba92995d3d63e60c9"
+ "reference": "762ee56b2649659380e0ef4d592d807bc17b7971"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/52903de178d542850f6f341ba92995d3d63e60c9",
- "reference": "52903de178d542850f6f341ba92995d3d63e60c9",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971",
+ "reference": "762ee56b2649659380e0ef4d592d807bc17b7971",
"shasum": ""
},
"require": {
@@ -2808,7 +2726,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v6.4.8"
+ "source": "https://github.com/symfony/yaml/tree/v6.4.12"
},
"funding": [
{
@@ -2824,7 +2742,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:49:08+00:00"
+ "time": "2024-09-17T12:47:12+00:00"
}
],
"packages-dev": [
@@ -3054,26 +2972,26 @@
},
{
"name": "composer/pcre",
- "version": "3.2.0",
+ "version": "3.3.1",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
- "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90"
+ "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90",
- "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4",
+ "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
- "phpstan/phpstan": "<1.11.8"
+ "phpstan/phpstan": "<1.11.10"
},
"require-dev": {
- "phpstan/phpstan": "^1.11.8",
+ "phpstan/phpstan": "^1.11.10",
"phpstan/phpstan-strict-rules": "^1.1",
"phpunit/phpunit": "^8 || ^9"
},
@@ -3113,7 +3031,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
- "source": "https://github.com/composer/pcre/tree/3.2.0"
+ "source": "https://github.com/composer/pcre/tree/3.3.1"
},
"funding": [
{
@@ -3129,28 +3047,28 @@
"type": "tidelift"
}
],
- "time": "2024-07-25T09:36:02+00:00"
+ "time": "2024-08-27T18:44:43+00:00"
},
{
"name": "composer/semver",
- "version": "3.4.2",
+ "version": "3.4.3",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6"
+ "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6",
- "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6",
+ "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
+ "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
- "phpstan/phpstan": "^1.4",
- "symfony/phpunit-bridge": "^4.2 || ^5"
+ "phpstan/phpstan": "^1.11",
+ "symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
@@ -3194,7 +3112,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
- "source": "https://github.com/composer/semver/tree/3.4.2"
+ "source": "https://github.com/composer/semver/tree/3.4.3"
},
"funding": [
{
@@ -3210,7 +3128,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-12T11:35:52+00:00"
+ "time": "2024-09-19T14:15:21+00:00"
},
{
"name": "composer/xdebug-handler",
@@ -3409,16 +3327,16 @@
},
{
"name": "felixfbecker/language-server-protocol",
- "version": "v1.5.2",
+ "version": "v1.5.3",
"source": {
"type": "git",
"url": "https://github.com/felixfbecker/php-language-server-protocol.git",
- "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842"
+ "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842",
- "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842",
+ "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9",
+ "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9",
"shasum": ""
},
"require": {
@@ -3459,22 +3377,22 @@
],
"support": {
"issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
- "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2"
+ "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3"
},
- "time": "2022-03-02T22:36:06+00:00"
+ "time": "2024-04-30T00:40:11+00:00"
},
{
"name": "fidry/cpu-core-counter",
- "version": "1.1.0",
+ "version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/theofidry/cpu-core-counter.git",
- "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42"
+ "reference": "8520451a140d3f46ac33042715115e290cf5785f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42",
- "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42",
+ "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f",
+ "reference": "8520451a140d3f46ac33042715115e290cf5785f",
"shasum": ""
},
"require": {
@@ -3514,7 +3432,7 @@
],
"support": {
"issues": "https://github.com/theofidry/cpu-core-counter/issues",
- "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0"
+ "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0"
},
"funding": [
{
@@ -3522,20 +3440,20 @@
"type": "github"
}
],
- "time": "2024-02-07T09:43:46+00:00"
+ "time": "2024-08-06T10:04:20+00:00"
},
{
"name": "friendsofphp/php-cs-fixer",
- "version": "v3.62.0",
+ "version": "v3.64.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
- "reference": "627692f794d35c43483f34b01d94740df2a73507"
+ "reference": "58dd9c931c785a79739310aef5178928305ffa67"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/627692f794d35c43483f34b01d94740df2a73507",
- "reference": "627692f794d35c43483f34b01d94740df2a73507",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67",
+ "reference": "58dd9c931c785a79739310aef5178928305ffa67",
"shasum": ""
},
"require": {
@@ -3617,7 +3535,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
- "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.62.0"
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0"
},
"funding": [
{
@@ -3625,20 +3543,20 @@
"type": "github"
}
],
- "time": "2024-08-07T17:03:09+00:00"
+ "time": "2024-08-30T23:09:38+00:00"
},
{
"name": "netresearch/jsonmapper",
- "version": "v4.4.1",
+ "version": "v4.5.0",
"source": {
"type": "git",
"url": "https://github.com/cweiske/jsonmapper.git",
- "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0"
+ "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0",
- "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0",
+ "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5",
+ "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5",
"shasum": ""
},
"require": {
@@ -3674,22 +3592,22 @@
"support": {
"email": "cweiske@cweiske.de",
"issues": "https://github.com/cweiske/jsonmapper/issues",
- "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1"
+ "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0"
},
- "time": "2024-01-31T06:18:54+00:00"
+ "time": "2024-09-08T10:13:13+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v4.19.1",
+ "version": "v4.19.4",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b"
+ "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b",
- "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2",
+ "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2",
"shasum": ""
},
"require": {
@@ -3698,7 +3616,7 @@
},
"require-dev": {
"ircmaxell/php-yacc": "^0.0.7",
- "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"bin": [
"bin/php-parse"
@@ -3730,9 +3648,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4"
},
- "time": "2024-03-17T08:10:35+00:00"
+ "time": "2024-09-29T15:01:53+00:00"
},
{
"name": "pdepend/pdepend",
@@ -3965,16 +3883,16 @@
},
{
"name": "phar-io/gnupg",
- "version": "1.0.2",
+ "version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/phar-io/gnupg.git",
- "reference": "3c106d39f62ba3941f830ca24e125cb1b9290a87"
+ "reference": "ed8ab1740ac4e9db99500e7252911f2821357093"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/gnupg/zipball/3c106d39f62ba3941f830ca24e125cb1b9290a87",
- "reference": "3c106d39f62ba3941f830ca24e125cb1b9290a87",
+ "url": "https://api.github.com/repos/phar-io/gnupg/zipball/ed8ab1740ac4e9db99500e7252911f2821357093",
+ "reference": "ed8ab1740ac4e9db99500e7252911f2821357093",
"shasum": ""
},
"require": {
@@ -4007,9 +3925,9 @@
"description": "Thin GnuPG wrapper class around the gnupg binary, mimicking the pecl/gnupg api",
"support": {
"issues": "https://github.com/phar-io/gnupg/issues",
- "source": "https://github.com/phar-io/gnupg/tree/1.0.2"
+ "source": "https://github.com/phar-io/gnupg/tree/1.0.3"
},
- "time": "2020-11-30T10:21:26+00:00"
+ "time": "2024-08-22T20:45:57+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@@ -4307,16 +4225,16 @@
},
{
"name": "phpstan/phpdoc-parser",
- "version": "1.29.1",
+ "version": "1.32.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4"
+ "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4",
- "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4",
+ "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4",
"shasum": ""
},
"require": {
@@ -4348,22 +4266,22 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
- "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0"
},
- "time": "2024-05-31T08:52:43+00:00"
+ "time": "2024-09-26T07:23:32+00:00"
},
{
"name": "phpstan/phpstan",
- "version": "1.11.10",
+ "version": "1.12.5",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "640410b32995914bde3eed26fa89552f9c2c082f"
+ "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/640410b32995914bde3eed26fa89552f9c2c082f",
- "reference": "640410b32995914bde3eed26fa89552f9c2c082f",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
+ "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
"shasum": ""
},
"require": {
@@ -4408,25 +4326,97 @@
"type": "github"
}
],
- "time": "2024-08-08T09:02:50+00:00"
+ "time": "2024-09-26T12:45:22+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-doctrine",
+ "version": "1.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-doctrine.git",
+ "reference": "38db3bad8f1567d7bf64806738d724261f8a2b5c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/38db3bad8f1567d7bf64806738d724261f8a2b5c",
+ "reference": "38db3bad8f1567d7bf64806738d724261f8a2b5c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpstan": "^1.11.7"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.0",
+ "doctrine/common": "<2.7",
+ "doctrine/mongodb-odm": "<1.2",
+ "doctrine/orm": "<2.5",
+ "doctrine/persistence": "<1.3"
+ },
+ "require-dev": {
+ "cache/array-adapter": "^1.1",
+ "composer/semver": "^3.3.2",
+ "cweagans/composer-patches": "^1.7.3",
+ "doctrine/annotations": "^1.11 || ^2.0",
+ "doctrine/collections": "^1.6 || ^2.1",
+ "doctrine/common": "^2.7 || ^3.0",
+ "doctrine/dbal": "^2.13.8 || ^3.3.3",
+ "doctrine/lexer": "^2.0 || ^3.0",
+ "doctrine/mongodb-odm": "^1.3 || ^2.4.3",
+ "doctrine/orm": "^2.16.0",
+ "doctrine/persistence": "^2.2.1 || ^3.2",
+ "gedmo/doctrine-extensions": "^3.8",
+ "nesbot/carbon": "^2.49",
+ "nikic/php-parser": "^4.13.2",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-phpunit": "^1.3.13",
+ "phpstan/phpstan-strict-rules": "^1.5.1",
+ "phpunit/phpunit": "^9.6.16",
+ "ramsey/uuid": "^4.2",
+ "symfony/cache": "^5.4"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon",
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Doctrine extensions for PHPStan",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-doctrine/issues",
+ "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.5.3"
+ },
+ "time": "2024-09-01T13:17:34+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
- "version": "1.6.0",
+ "version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
- "reference": "363f921dd8441777d4fc137deb99beb486c77df1"
+ "reference": "daeec748b53de80a97498462513066834ec28f8b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/363f921dd8441777d4fc137deb99beb486c77df1",
- "reference": "363f921dd8441777d4fc137deb99beb486c77df1",
+ "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/daeec748b53de80a97498462513066834ec28f8b",
+ "reference": "daeec748b53de80a97498462513066834ec28f8b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
- "phpstan/phpstan": "^1.11"
+ "phpstan/phpstan": "^1.12.4"
},
"require-dev": {
"nikic/php-parser": "^4.13.0",
@@ -4455,9 +4445,81 @@
"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.6.0"
+ "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.1"
},
- "time": "2024-04-20T06:37:51+00:00"
+ "time": "2024-09-20T14:04:44+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-symfony",
+ "version": "1.4.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-symfony.git",
+ "reference": "f7d5782044bedf93aeb3f38e09c91148ee90e5a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/f7d5782044bedf93aeb3f38e09c91148ee90e5a1",
+ "reference": "f7d5782044bedf93aeb3f38e09c91148ee90e5a1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpstan": "^1.12"
+ },
+ "conflict": {
+ "symfony/framework-bundle": "<3.0"
+ },
+ "require-dev": {
+ "nikic/php-parser": "^4.13.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-phpunit": "^1.3.11",
+ "phpstan/phpstan-strict-rules": "^1.5.1",
+ "phpunit/phpunit": "^8.5.29 || ^9.5",
+ "psr/container": "1.0 || 1.1.1",
+ "symfony/config": "^5.4 || ^6.1",
+ "symfony/console": "^5.4 || ^6.1",
+ "symfony/dependency-injection": "^5.4 || ^6.1",
+ "symfony/form": "^5.4 || ^6.1",
+ "symfony/framework-bundle": "^5.4 || ^6.1",
+ "symfony/http-foundation": "^5.4 || ^6.1",
+ "symfony/messenger": "^5.4",
+ "symfony/polyfill-php80": "^1.24",
+ "symfony/serializer": "^5.4",
+ "symfony/service-contracts": "^2.2.0"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon",
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Lukáš Unger",
+ "email": "looky.msc@gmail.com",
+ "homepage": "https://lookyman.net"
+ }
+ ],
+ "description": "Symfony Framework extensions and rules for PHPStan",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-symfony/issues",
+ "source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.10"
+ },
+ "time": "2024-09-26T18:14:50+00:00"
},
{
"name": "psr/event-dispatcher",
@@ -5176,16 +5238,16 @@
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.10.2",
+ "version": "3.10.3",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017"
+ "reference": "62d32998e820bddc40f99f8251958aed187a5c9c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017",
- "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c",
+ "reference": "62d32998e820bddc40f99f8251958aed187a5c9c",
"shasum": ""
},
"require": {
@@ -5252,7 +5314,7 @@
"type": "open_collective"
}
],
- "time": "2024-07-21T23:26:44+00:00"
+ "time": "2024-09-18T10:38:58+00:00"
},
{
"name": "symfony/config",
@@ -5331,16 +5393,16 @@
},
{
"name": "symfony/dependency-injection",
- "version": "v6.4.10",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
- "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb"
+ "reference": "cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5caf9c5f6085f13b27d70a236b776c07e4a1c3eb",
- "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e",
+ "reference": "cfb9d34a1cdd4911bc737a5358fd1cf8ebfb536e",
"shasum": ""
},
"require": {
@@ -5392,7 +5454,7 @@
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/dependency-injection/tree/v6.4.10"
+ "source": "https://github.com/symfony/dependency-injection/tree/v6.4.12"
},
"funding": [
{
@@ -5408,7 +5470,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-26T07:32:07+00:00"
+ "time": "2024-09-20T08:18:25+00:00"
},
{
"name": "symfony/event-dispatcher",
@@ -5568,16 +5630,16 @@
},
{
"name": "symfony/finder",
- "version": "v6.4.10",
+ "version": "v6.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "af29198d87112bebdd397bd7735fbd115997824c"
+ "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c",
- "reference": "af29198d87112bebdd397bd7735fbd115997824c",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/d7eb6daf8cd7e9ac4976e9576b32042ef7253453",
+ "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453",
"shasum": ""
},
"require": {
@@ -5612,7 +5674,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v6.4.10"
+ "source": "https://github.com/symfony/finder/tree/v6.4.11"
},
"funding": [
{
@@ -5628,7 +5690,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-24T07:06:38+00:00"
+ "time": "2024-08-13T14:27:37+00:00"
},
{
"name": "symfony/options-resolver",
@@ -5699,20 +5761,20 @@
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
+ "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
- "reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
+ "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"type": "library",
"extra": {
@@ -5759,7 +5821,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
},
"funding": [
{
@@ -5775,24 +5837,24 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php81",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
- "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af"
+ "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af",
- "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+ "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"type": "library",
"extra": {
@@ -5835,7 +5897,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
},
"funding": [
{
@@ -5851,20 +5913,20 @@
"type": "tidelift"
}
],
- "time": "2024-06-19T12:30:46+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/process",
- "version": "v6.4.8",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5"
+ "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5",
- "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5",
+ "url": "https://api.github.com/repos/symfony/process/zipball/3f94e5f13ff58df371a7ead461b6e8068900fbb3",
+ "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3",
"shasum": ""
},
"require": {
@@ -5896,7 +5958,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v6.4.8"
+ "source": "https://github.com/symfony/process/tree/v6.4.12"
},
"funding": [
{
@@ -5912,7 +5974,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:49:08+00:00"
+ "time": "2024-09-17T12:47:12+00:00"
},
{
"name": "symfony/stopwatch",
@@ -5978,16 +6040,16 @@
},
{
"name": "vimeo/psalm",
- "version": "5.25.0",
+ "version": "5.26.1",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
- "reference": "01a8eb06b9e9cc6cfb6a320bf9fb14331919d505"
+ "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vimeo/psalm/zipball/01a8eb06b9e9cc6cfb6a320bf9fb14331919d505",
- "reference": "01a8eb06b9e9cc6cfb6a320bf9fb14331919d505",
+ "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0",
+ "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0",
"shasum": ""
},
"require": {
@@ -6008,7 +6070,7 @@
"felixfbecker/language-server-protocol": "^1.5.2",
"fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
- "nikic/php-parser": "^4.16",
+ "nikic/php-parser": "^4.17",
"php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
"sebastian/diff": "^4.0 || ^5.0 || ^6.0",
"spatie/array-to-xml": "^2.17.0 || ^3.0",
@@ -6084,7 +6146,7 @@
"issues": "https://github.com/vimeo/psalm/issues",
"source": "https://github.com/vimeo/psalm"
},
- "time": "2024-06-16T15:08:35+00:00"
+ "time": "2024-09-08T18:53:08+00:00"
},
{
"name": "webmozart/assert",
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index d8a116c..d7b701b 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -1,11 +1,11 @@
Open Culture Consulting strictly follows PSR standards.
+ ./bin
+ ./public
./src
-
-
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
index 58e4059..9f5b4f5 100644
--- a/phpstan.dist.neon
+++ b/phpstan.dist.neon
@@ -5,11 +5,14 @@
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
+ - vendor/phpstan/phpstan-doctrine/extension.neon
+ - vendor/phpstan/phpstan-symfony/extension.neon
parameters:
level: 9
- strictRules:
- noVariableVariables: false
+ treatPhpDocTypesAsCertain: false
+ ignoreErrors:
+ - identifier: ternary.shortNotAllowed
paths:
- bin
- public
diff --git a/psalm.xml.dist b/psalm.xml.dist
index cea884b..241462c 100644
--- a/psalm.xml.dist
+++ b/psalm.xml.dist
@@ -8,23 +8,40 @@
findUnusedBaselineEntry="true"
findUnusedCode="true"
findUnusedVariablesAndParams="true"
+ reportMixedIssues="false"
>
-
+
-
+
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
diff --git a/src/App.php b/src/App.php
index 9b117ea..9d7fcf8 100644
--- a/src/App.php
+++ b/src/App.php
@@ -31,19 +31,19 @@
* @author Sebastian Meyer
* @package OAIPMH2
*/
-class App
+final class App
{
/**
* The PSR-15 Server Request Handler.
*/
- protected QueueRequestHandler $requestHandler;
+ private QueueRequestHandler $requestHandler;
/**
* Instantiate application.
*/
public function __construct()
{
- $this->requestHandler = new QueueRequestHandler([new Dispatcher()]);
+ $this->requestHandler = new QueueRequestHandler(middlewares: [new Dispatcher()]);
}
/**
@@ -54,6 +54,9 @@ public function __construct()
public function run(): void
{
$this->requestHandler->handle();
+ if ($this->requestHandler->response->hasHeader('Warning')) {
+ // An exception occured. Maybe we don't want to output the response, but log an error instead?
+ }
$this->requestHandler->respond();
}
}
diff --git a/src/Configuration.php b/src/Configuration.php
index d69d651..5f00e9f 100644
--- a/src/Configuration.php
+++ b/src/Configuration.php
@@ -23,11 +23,10 @@
namespace OCC\OaiPmh2;
use OCC\Basics\Traits\Singleton;
+use OCC\OaiPmh2\Validator\ConfigurationValidator;
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;
/**
@@ -36,18 +35,18 @@
* @author Sebastian Meyer
* @package OAIPMH2
*
- * @property-read string $repositoryName
- * @property-read string $adminEmail
- * @property-read string $database
- * @property-read array $metadataPrefix
- * @property-read string $deletedRecords
- * @property-read int $maxRecords
- * @property-read int $tokenValid
+ * @property-read string $repositoryName Common name of this repository
+ * @property-read string $adminEmail Repository contact's e-mail address
+ * @property-read string $database Database's data source name (DSN)
+ * @property-read array $metadataPrefix Array of served metadata prefixes
+ * @property-read string $deletedRecords Repository's deleted records policy
+ * @property-read int $maxRecords Maximum number of records served per request
+ * @property-read int $tokenValid Number of seconds resumption tokens are valid
*
* @template TKey of string
* @template TValue of array|int|string
*/
-class Configuration
+final class Configuration
{
use Singleton;
@@ -66,110 +65,31 @@ class Configuration
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()
- ]
- ])
- ])
- ],
- 'deletedRecords' => [
- new Assert\Type('string'),
- new Assert\Choice(['no', 'persistent', 'transient']),
- 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
+ * Load and validate configuration settings from YAML file.
*
* @throws FileNotFoundException if configuration file does not exist
* @throws ValidationFailedException if configuration file is not valid
*/
- protected function loadConfigFile(): array
+ private function __construct()
{
- $configPath = Path::canonicalize(self::CONFIG_FILE);
- if (!is_readable($configPath)) {
+ $configPath = Path::canonicalize(path: self::CONFIG_FILE);
+ if (!is_readable(filename: $configPath)) {
throw new FileNotFoundException(
- sprintf(
- 'Configuration file "%s" not found or not readable.',
- $configPath
- ),
- 500,
- null,
- $configPath
+ message: 'Configuration file not found or not readable.',
+ code: 500,
+ path: $configPath
);
}
/** @var array */
- $config = Yaml::parseFile($configPath);
- $validator = Validation::createValidator();
- $violations = $validator->validate($config, $this->getValidationConstraints());
+ $config = Yaml::parseFile(filename: $configPath);
+ $violations = ConfigurationValidator::validate(config: $config);
if ($violations->count() > 0) {
- throw new ValidationFailedException(null, $violations);
- }
- return $config;
- }
-
- /**
- * Load and validate configuration settings from YAML file.
- *
- * @throws FileNotFoundException if configuration file does not exist
- * @throws ValidationFailedException if configuration file is not valid
- */
- private function __construct()
- {
- try {
- $this->settings = $this->loadConfigFile();
- } catch (FileNotFoundException | ValidationFailedException $exception) {
- throw $exception;
+ throw new ValidationFailedException(
+ value: null,
+ violations: $violations
+ );
}
+ $this->settings = $config;
}
/**
@@ -177,7 +97,7 @@ private function __construct()
*
* @param TKey $name The setting to retrieve
*
- * @return TValue|null The setting or NULL
+ * @return ?TValue The setting or NULL
*/
public function __get(string $name): mixed
{
diff --git a/src/Console.php b/src/Console.php
index 7ef0a31..10f0e75 100644
--- a/src/Console.php
+++ b/src/Console.php
@@ -34,9 +34,53 @@
*
* @author Sebastian Meyer
* @package OAIPMH2
+ *
+ * @psalm-type CliArguments = array{
+ * identifier: string,
+ * format: string,
+ * file: string,
+ * sets?: list,
+ * setSpec: string,
+ * setName: string,
+ * idColumn: string,
+ * contentColumn: string,
+ * dateColumn: string,
+ * setColumn: string,
+ * noValidation: bool,
+ * force: bool
+ * }
*/
abstract class Console extends Command
{
+ /**
+ * This holds the command's arguments and options.
+ *
+ * @var CliArguments
+ */
+ protected array $arguments;
+
+ /**
+ * This holds the entity manager singleton.
+ */
+ protected EntityManager $em;
+
+ /**
+ * This holds the PHP memory limit in bytes.
+ */
+ protected int $memoryLimit;
+
+ /**
+ * Flushes changes to the database if memory limit reaches 50%.
+ *
+ * @return void
+ */
+ protected function checkMemoryUsage(): void
+ {
+ if ((memory_get_usage() / $this->getPhpMemoryLimit()) > 0.5) {
+ $this->em->flush();
+ }
+ }
+
/**
* Clears the result cache.
*
@@ -47,11 +91,13 @@ protected function clearResultCache(): void
/** @var Application */
$app = $this->getApplication();
$app->doRun(
- new ArrayInput([
- 'command' => 'orm:clear-cache:result',
- '--flush' => true
- ]),
- new NullOutput()
+ input: new ArrayInput(
+ parameters: [
+ 'command' => 'orm:clear-cache:result',
+ '--flush' => true
+ ]
+ ),
+ output: new NullOutput()
);
}
@@ -62,23 +108,26 @@ protected function clearResultCache(): void
*/
protected function getPhpMemoryLimit(): int
{
- $ini = trim(ini_get('memory_limit'));
- $limit = (int) $ini;
- if ($limit < 0) {
- return -1;
- }
- $unit = strtolower($ini[strlen($ini) - 1]);
- switch ($unit) {
- case 'g':
- $limit *= 1024;
- // no break
- case 'm':
- $limit *= 1024;
- // no break
- case 'k':
- $limit *= 1024;
+ if (!isset($this->memoryLimit)) {
+ $ini = trim(string: ini_get(option: 'memory_limit'));
+ $limit = (int) $ini;
+ if ($limit < 0) {
+ return -1;
+ }
+ $unit = strtolower($ini[strlen($ini) - 1]);
+ switch ($unit) {
+ case 'g':
+ $limit *= 1024;
+ // no break
+ case 'm':
+ $limit *= 1024;
+ // no break
+ case 'k':
+ $limit *= 1024;
+ }
+ $this->memoryLimit = $limit;
}
- return $limit;
+ return $this->memoryLimit;
}
/**
@@ -91,32 +140,68 @@ protected function getPhpMemoryLimit(): int
*/
protected function validateInput(InputInterface $input, OutputInterface $output): bool
{
- /** @var array */
- $arguments = $input->getArguments();
+ /** @var CliArguments */
+ $mergedArguments = array_merge($input->getArguments(), $input->getOptions());
+ $this->arguments = $mergedArguments;
- $formats = Database::getInstance()->getMetadataFormats()->getQueryResult();
- if (!array_key_exists($arguments['format'], $formats)) {
- $output->writeln([
- '',
- sprintf(
- ' [ERROR] Metadata format "%s" is not supported. ',
- $arguments['format']
- ),
- ''
- ]);
- return false;
+ if (array_key_exists('format', $this->arguments)) {
+ $formats = $this->em->getMetadataFormats();
+ if (!$formats->containsKey(key: $this->arguments['format'])) {
+ $output->writeln(
+ messages: [
+ '',
+ sprintf(
+ format: ' [ERROR] Metadata format "%s" is not supported. ',
+ values: $this->arguments['format']
+ ),
+ ''
+ ]
+ );
+ return false;
+ }
}
- if (!is_readable($arguments['file'])) {
- $output->writeln([
- '',
- sprintf(
- ' [ERROR] File "%s" not found or not readable. ',
- $arguments['file']
- ),
- ''
- ]);
+ if (array_key_exists('file', $this->arguments) && !is_readable(filename: $this->arguments['file'])) {
+ $output->writeln(
+ messages: [
+ '',
+ sprintf(
+ format: ' [ERROR] File "%s" not found or not readable. ',
+ values: $this->arguments['file']
+ ),
+ ''
+ ]
+ );
return false;
}
+ if (array_key_exists('sets', $this->arguments)) {
+ $sets = $this->em->getSets();
+ $invalidSets = array_diff($this->arguments['sets'], $sets->getKeys());
+ if (count($invalidSets) !== 0) {
+ $output->writeln(
+ messages: [
+ '',
+ sprintf(
+ format: ' [ERROR] Sets "%s" are not supported. ',
+ values: implode('", "', $invalidSets)
+ ),
+ ''
+ ]
+ );
+ return false;
+ }
+ }
return true;
}
+
+ /**
+ * Create new console command instance.
+ *
+ * @param ?string $name The name of the command
+ * passing null means it must be set in configure()
+ */
+ public function __construct(?string $name = null)
+ {
+ $this->em = EntityManager::getInstance();
+ parent::__construct($name);
+ }
}
diff --git a/src/Console/AddRecordCommand.php b/src/Console/AddRecordCommand.php
index b949033..28628dd 100644
--- a/src/Console/AddRecordCommand.php
+++ b/src/Console/AddRecordCommand.php
@@ -23,7 +23,6 @@
namespace OCC\OaiPmh2\Console;
use OCC\OaiPmh2\Console;
-use OCC\OaiPmh2\Database;
use OCC\OaiPmh2\Entity\Format;
use OCC\OaiPmh2\Entity\Record;
use OCC\OaiPmh2\Entity\Set;
@@ -70,7 +69,7 @@ protected function configure(): void
$this->addArgument(
'sets',
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
- 'The list of sets to associate the record with.'
+ 'Optional: The list of sets to associate the record with.'
);
parent::configure();
}
@@ -85,36 +84,31 @@ protected function configure(): void
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
- if (!$this->validateInput($input, $output)) {
+ if (!$this->validateInput(input: $input, output: $output)) {
return Command::INVALID;
}
- /** @var string */
- $identifier = $input->getArgument('identifier');
+
/** @var Format */
- $format = Database::getInstance()
- ->getEntityManager()
- ->getReference(Format::class, $input->getArgument('format'));
- /** @var string */
- $file = $input->getArgument('file');
- /** @var string[] */
- $sets = $input->getArgument('sets');
- /** @var string */
- $content = file_get_contents($file);
+ $format = $this->em->getMetadataFormat(prefix: $this->arguments['format']);
+ $content = file_get_contents(filename: $this->arguments['file']) ?: '';
- $record = new Record($identifier, $format);
+ $record = new Record(
+ identifier: $this->arguments['identifier'],
+ format: $format
+ );
if (trim($content) !== '') {
- $record->setContent($content);
+ $record->setContent(data: $content);
}
- foreach ($sets as $set) {
- /** @var Set */
- $setSpec = Database::getInstance()
- ->getEntityManager()
- ->getReference(Set::class, $set);
- $record->addSet($setSpec);
+ if (array_key_exists('sets', $this->arguments)) {
+ foreach ($this->arguments['sets'] as $set) {
+ /** @var Set */
+ $setSpec = $this->em->getSet(spec: $set);
+ $record->addSet(set: $setSpec);
+ }
}
- Database::getInstance()->addOrUpdateRecord($record);
- Database::getInstance()->pruneOrphanSets();
+ $this->em->addOrUpdate(entity: $record);
+ $this->em->pruneOrphanedSets();
$this->clearResultCache();
@@ -122,7 +116,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'',
sprintf(
' [OK] Record "%s" with metadata prefix "%s" added or updated successfully! ',
- $identifier,
+ $this->arguments['identifier'],
$format->getPrefix()
),
''
diff --git a/src/Console/AddSetCommand.php b/src/Console/AddSetCommand.php
index 43a171d..c86ee8c 100644
--- a/src/Console/AddSetCommand.php
+++ b/src/Console/AddSetCommand.php
@@ -23,7 +23,6 @@
namespace OCC\OaiPmh2\Console;
use OCC\OaiPmh2\Console;
-use OCC\OaiPmh2\Database;
use OCC\OaiPmh2\Entity\Set;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -53,10 +52,10 @@ protected function configure(): void
$this->addArgument(
'setSpec',
InputArgument::REQUIRED,
- 'The set (spec) to update.',
+ 'The set (spec) to add or update.',
null,
function (): array {
- return array_keys(Database::getInstance()->getAllSets()->getQueryResult());
+ return $this->em->getSets()->getKeys();
}
);
$this->addArgument(
@@ -67,7 +66,7 @@ function (): array {
$this->addArgument(
'file',
InputArgument::OPTIONAL,
- 'The optional file containing the set description XML.'
+ 'Optional: The file containing the set description XML.'
);
parent::configure();
}
@@ -82,32 +81,20 @@ function (): array {
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
- /** @var array */
- $arguments = $input->getArguments();
- $description = null;
+ if (!$this->validateInput(input: $input, output: $output)) {
+ return Command::INVALID;
+ }
- if (isset($arguments['file'])) {
- if (!is_readable($arguments['file'])) {
- $output->writeln([
- '',
- sprintf(
- ' [ERROR] File "%s" not found or not readable. ',
- $arguments['file']
- ),
- ''
- ]);
- return Command::INVALID;
- } else {
- $description = (string) file_get_contents($arguments['file']);
- }
+ if (array_key_exists('file', $this->arguments)) {
+ $description = file_get_contents(filename: $this->arguments['file']) ?: null;
}
$set = new Set(
- $arguments['setSpec'],
- $arguments['setName'],
- $description
+ spec: $this->arguments['setSpec'],
+ name: $this->arguments['setName'],
+ description: $description ?? null
);
- Database::getInstance()->addOrUpdateSet($set);
+ $this->em->addOrUpdate(entity: $set);
return Command::SUCCESS;
}
diff --git a/src/Console/CsvImportCommand.php b/src/Console/CsvImportCommand.php
index 8919eec..20fd004 100644
--- a/src/Console/CsvImportCommand.php
+++ b/src/Console/CsvImportCommand.php
@@ -23,9 +23,7 @@
namespace OCC\OaiPmh2\Console;
use DateTime;
-use OCC\OaiPmh2\Configuration;
use OCC\OaiPmh2\Console;
-use OCC\OaiPmh2\Database;
use OCC\OaiPmh2\Entity\Format;
use OCC\OaiPmh2\Entity\Record;
use OCC\OaiPmh2\Entity\Set;
@@ -42,6 +40,13 @@
*
* @author Sebastian Meyer
* @package OAIPMH2
+ *
+ * @psalm-type ColumnMapping = array{
+ * idColumn: int,
+ * contentColumn: int,
+ * dateColumn: ?int,
+ * setColumn: ?int
+ * }
*/
#[AsCommand(
name: 'oai:records:import:csv',
@@ -62,7 +67,7 @@ protected function configure(): void
'The format (metadata prefix) of the records.',
null,
function (): array {
- return array_keys(Database::getInstance()->getMetadataFormats()->getQueryResult());
+ return $this->em->getMetadataFormats()->getKeys();
}
);
$this->addArgument(
@@ -88,21 +93,21 @@ function (): array {
'dateColumn',
'd',
InputOption::VALUE_OPTIONAL,
- 'Name of the CSV column which holds the records\' datetime of last change.',
+ 'Optional: Name of the CSV column which holds the records\' datetime of last change.',
'lastChanged'
);
$this->addOption(
'setColumn',
's',
InputOption::VALUE_OPTIONAL,
- 'Name of the CSV column which holds the comma-separated list of the records\' sets.',
+ 'Optional: Name of the CSV column which holds the comma-separated list of the records\' sets.',
'sets'
);
$this->addOption(
'noValidation',
null,
InputOption::VALUE_NONE,
- 'Skip content validation (improves performance for large record sets).'
+ 'Optional: Skip content validation (improves ingest performance for large record sets).'
);
parent::configure();
}
@@ -117,69 +122,60 @@ function (): array {
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
- if (!$this->validateInput($input, $output)) {
+ if (!$this->validateInput(input: $input, output: $output)) {
return Command::INVALID;
}
- $phpMemoryLimit = $this->getPhpMemoryLimit();
- /** @var array */
- $arguments = $input->getArguments();
- /** @var bool */
- $noValidation = $input->getOption('noValidation');
/** @var resource */
- $file = fopen($arguments['file'], 'r');
+ $file = fopen(filename: $this->arguments['file'], mode: 'r');
- $columns = $this->getColumnNames($input, $output, $file);
- if (count($columns) === 0) {
- return Command::INVALID;
+ $columnMapping = $this->getColumnNames(input: $input, output: $output, file: $file);
+
+ if (!isset($columnMapping)) {
+ return Command::FAILURE;
}
$count = 0;
$progressIndicator = new ProgressIndicator($output, null, 100, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']);
$progressIndicator->start('Importing...');
- while ($row = fgetcsv($file)) {
+ while ($row = fgetcsv(stream: $file)) {
/** @var Format */
- $format = Database::getInstance()
- ->getEntityManager()
- ->getReference(Format::class, $arguments['format']);
- $record = new Record($row[$columns['idColumn']], $format);
- if (strlen(trim($row[$columns['contentColumn']])) > 0) {
- $record->setContent($row[$columns['contentColumn']], !$noValidation);
+ $format = $this->em->getMetadataFormat(prefix: $this->arguments['format']);
+ $record = new Record(
+ identifier: $row[$columnMapping['idColumn']],
+ format: $format
+ );
+ if (strlen(trim($row[$columnMapping['contentColumn']])) > 0) {
+ $record->setContent(
+ data: $row[$columnMapping['contentColumn']],
+ validate: !$this->arguments['noValidation']
+ );
}
- if (isset($columns['dateColumn'])) {
- $record->setLastChanged(new DateTime($row[$columns['dateColumn']]));
+ if (isset($columnMapping['dateColumn'])) {
+ $record->setLastChanged(dateTime: new DateTime($row[$columnMapping['dateColumn']]));
}
- if (isset($columns['setColumn'])) {
- $sets = $row[$columns['setColumn']];
+ if (isset($columnMapping['setColumn'])) {
+ $sets = $row[$columnMapping['setColumn']];
foreach (explode(',', $sets) as $set) {
/** @var Set */
- $setSpec = Database::getInstance()
- ->getEntityManager()
- ->getReference(Set::class, trim($set));
- $record->addSet($setSpec);
+ $setSpec = $this->em->getSet(spec: trim($set));
+ $record->addSet(set: $setSpec);
}
}
- Database::getInstance()->addOrUpdateRecord($record, true);
+ $this->em->addOrUpdate(entity: $record, bulkMode: true);
++$count;
$progressIndicator->advance();
$progressIndicator->setMessage('Importing... ' . (string) $count . ' records processed.');
-
- // Flush to database if memory usage reaches 50% or every 10.000 records.
- if ((memory_get_usage() / $phpMemoryLimit) > 0.5 || ($count % 10000) === 0) {
- $progressIndicator->setMessage(
- 'Importing... ' . (string) $count . ' records processed. Flushing to database...'
- );
- Database::getInstance()->flush(true);
- }
+ $this->checkMemoryUsage();
}
- Database::getInstance()->flush(true);
- Database::getInstance()->pruneOrphanSets();
+ $this->em->flush();
+ $this->em->pruneOrphanedSets();
$progressIndicator->finish('All done!');
- fclose($file);
+ fclose(stream: $file);
$this->clearResultCache();
@@ -188,7 +184,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
sprintf(
' [OK] %d records with metadata prefix "%s" were imported successfully! ',
$count,
- $arguments['format']
+ $this->arguments['format']
),
''
]);
@@ -196,49 +192,57 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
/**
- * Get the column names of CSV.
+ * Get the column numbers of CSV.
*
- * @param InputInterface $input The inputs
- * @param OutputInterface $output The output interface
+ * @param InputInterface $input The input
+ * @param OutputInterface $output The output
* @param resource $file The handle for the CSV file
*
- * @return array The mapped column names
+ * @return ?ColumnMapping The mapped columns or NULL in case of an error
*/
- protected function getColumnNames(InputInterface $input, OutputInterface $output, $file): array
+ protected function getColumnNames(InputInterface $input, OutputInterface $output, $file): ?array
{
- /** @var array */
- $options = $input->getOptions();
-
- $columns = [];
+ /** @var array{idColumn: string, contentColumn: string, dateColumn: string, setColumn: string} */
+ $columns = [
+ 'idColumn' => $input->getOption('idColumn'),
+ 'contentColumn' => $input->getOption('contentColumn'),
+ 'dateColumn' => $input->getOption('dateColumn'),
+ 'setColumn' => $input->getOption('setColumn')
+ ];
$headers = fgetcsv($file);
- if (!is_array($headers)) {
+ if (!is_array($headers) || !isset($headers[0])) {
$output->writeln([
'',
sprintf(
- ' [ERROR] File "%s" does not contain valid CSV. ',
- stream_get_meta_data($file)['uri'] ?? 'unknown'
+ format: ' [ERROR] File "%s" does not contain valid CSV. ',
+ /** @phpstan-ignore-next-line - URI is always set for fopen() resources. */
+ values: stream_get_meta_data(stream: $file)['uri'] ?: 'unknown'
),
''
]);
- return [];
- } else {
- $headers = array_flip($headers);
- }
- foreach ($options as $option => $value) {
- $columns[$option] = $headers[$value] ?? null;
+ return null;
}
+ /** @var array */
+ $headers = array_flip($headers);
+
+ $callback = function (string $column) use ($headers): ?int {
+ return array_key_exists($column, $headers) ? $headers[$column] : null;
+ };
+
+ $columns = array_map($callback, $columns);
if (!isset($columns['idColumn']) || !isset($columns['contentColumn'])) {
$output->writeln([
'',
sprintf(
- ' [ERROR] File "%s" does not contain valid CSV. ',
- stream_get_meta_data($file)['uri'] ?? 'unknown'
+ format: ' [ERROR] File "%s" does not contain mandatory columns. ',
+ /** @phpstan-ignore-next-line - URI is always set for fopen() resources. */
+ values: stream_get_meta_data($file)['uri'] ?: 'unknown'
),
''
]);
- return [];
+ return null;
}
return $columns;
}
diff --git a/src/Console/DeleteRecordCommand.php b/src/Console/DeleteRecordCommand.php
index 9e64661..240841a 100644
--- a/src/Console/DeleteRecordCommand.php
+++ b/src/Console/DeleteRecordCommand.php
@@ -23,9 +23,6 @@
namespace OCC\OaiPmh2\Console;
use OCC\OaiPmh2\Console;
-use OCC\OaiPmh2\Database;
-use OCC\OaiPmh2\Entity\Format;
-use OCC\OaiPmh2\Entity\Record;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -40,7 +37,7 @@
*/
#[AsCommand(
name: 'oai:records:delete',
- description: 'Delete a record from database'
+ description: 'Delete a record while obeying deleted record policy'
)]
class DeleteRecordCommand extends Console
{
@@ -74,28 +71,24 @@ protected function configure(): void
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
- /** @var array */
- $arguments = $input->getArguments();
- $entityManager = Database::getInstance()->getEntityManager();
+ if (!$this->validateInput(input: $input, output: $output)) {
+ return Command::INVALID;
+ }
- $format = $entityManager->getReference(Format::class, $arguments['format']);
- $record = $entityManager->find(
- Record::class,
- [
- 'identifier' => $arguments['identifier'],
- 'format' => $format
- ]
+ $record = $this->em->getRecord(
+ identifier: $this->arguments['identifier'],
+ format: $this->arguments['format']
);
if (isset($record)) {
- Database::getInstance()->deleteRecord($record);
+ $this->em->delete(entity: $record);
$this->clearResultCache();
$output->writeln([
'',
sprintf(
- ' [OK] Record "%s" with metadata prefix "%s" successfully deleted. ',
- $arguments['identifier'],
- $arguments['format']
+ ' [OK] Record "%s" with metadata prefix "%s" successfully (marked as) deleted. ',
+ $this->arguments['identifier'],
+ $this->arguments['format']
),
''
]);
@@ -105,8 +98,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'',
sprintf(
' [ERROR] Record "%s" with metadata prefix "%s" not found. ',
- $arguments['identifier'],
- $arguments['format']
+ $this->arguments['identifier'],
+ $this->arguments['format']
),
''
]);
diff --git a/src/Console/PruneDeletedRecordsCommand.php b/src/Console/PruneDeletedRecordsCommand.php
index 2ae72cd..4e22a43 100644
--- a/src/Console/PruneDeletedRecordsCommand.php
+++ b/src/Console/PruneDeletedRecordsCommand.php
@@ -24,7 +24,6 @@
use OCC\OaiPmh2\Configuration;
use 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;
@@ -54,7 +53,7 @@ protected function configure(): void
'force',
'f',
InputOption::VALUE_NONE,
- 'Deletes records even under "transient" policy.'
+ 'Optional: Deletes records even under "transient" policy.'
);
parent::configure();
}
@@ -75,13 +74,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$policy === 'no'
or ($policy === 'transient' && $forced)
) {
- $deleted = Database::getInstance()->pruneDeletedRecords();
+ $deleted = $this->em->pruneDeletedRecords();
$this->clearResultCache();
$output->writeln([
'',
sprintf(
- ' [OK] %d records are deleted and were successfully removed! ',
- $deleted
+ format: ' [OK] %d deleted records were successfully removed! ',
+ values: $deleted
),
''
]);
diff --git a/src/Console/PruneResumptionTokensCommand.php b/src/Console/PruneResumptionTokensCommand.php
index 267c3b8..e76b7b0 100644
--- a/src/Console/PruneResumptionTokensCommand.php
+++ b/src/Console/PruneResumptionTokensCommand.php
@@ -23,7 +23,6 @@
namespace OCC\OaiPmh2\Console;
use 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;
@@ -51,12 +50,12 @@ class PruneResumptionTokensCommand extends Console
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $expired = Database::getInstance()->pruneResumptionTokens();
+ $expired = $this->em->pruneExpiredTokens();
$output->writeln([
'',
sprintf(
- ' [OK] %d resumption tokens are expired and were successfully deleted! ',
- $expired
+ format: ' [OK] %d expired resumption tokens were successfully deleted! ',
+ values: $expired
),
''
]);
diff --git a/src/Console/UpdateFormatsCommand.php b/src/Console/UpdateFormatsCommand.php
index 3d16243..d56982f 100644
--- a/src/Console/UpdateFormatsCommand.php
+++ b/src/Console/UpdateFormatsCommand.php
@@ -24,7 +24,6 @@
use OCC\OaiPmh2\Configuration;
use OCC\OaiPmh2\Console;
-use OCC\OaiPmh2\Database;
use OCC\OaiPmh2\Entity\Format;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -57,56 +56,55 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/** @var array> */
$formats = Configuration::getInstance()->metadataPrefix;
$this->clearResultCache();
- $inDatabase = Database::getInstance()
- ->getMetadataFormats()
- ->getQueryResult();
- $added = 0;
- $deleted = 0;
+ $inDatabase = $this->em->getMetadataFormats();
$failure = false;
foreach ($formats as $prefix => $format) {
- if (array_key_exists($prefix, $inDatabase)) {
- if (
- $format['namespace'] === $inDatabase[$prefix]->getNamespace()
- and $format['schema'] === $inDatabase[$prefix]->getSchema()
- ) {
- continue;
- }
+ if (
+ $inDatabase->containsKey(key: $prefix)
+ /** @phpstan-ignore-next-line - $inDatabase[$prefix] is always of type Format. */
+ and $format['namespace'] === $inDatabase[$prefix]->getNamespace()
+ /** @phpstan-ignore-next-line - $inDatabase[$prefix] is always of type Format. */
+ and $format['schema'] === $inDatabase[$prefix]->getSchema()
+ ) {
+ continue;
}
try {
- $format = new Format($prefix, $format['namespace'], $format['schema']);
- Database::getInstance()->addOrUpdateMetadataFormat($format);
- ++$added;
+ $format = new Format(
+ prefix: $prefix,
+ namespace: $format['namespace'],
+ schema: $format['schema']
+ );
+ $this->em->addOrUpdate(entity: $format);
$output->writeln([
sprintf(
- ' [OK] Metadata format "%s" added or updated successfully! ',
- $prefix
+ format: ' [OK] Metadata format "%s" added or updated successfully! ',
+ values: $prefix
)
]);
} catch (ValidationFailedException $exception) {
$failure = true;
$output->writeln([
sprintf(
- ' [ERROR] Could not add or update metadata format "%s". ',
- $prefix
+ format: ' [ERROR] Could not add or update metadata format "%s". ',
+ values: $prefix
),
$exception->getMessage()
]);
}
}
- foreach (array_keys($inDatabase) as $prefix) {
- if (!array_key_exists($prefix, $formats)) {
- Database::getInstance()->deleteMetadataFormat($inDatabase[$prefix]);
- ++$deleted;
- $output->writeln([
- sprintf(
- ' [OK] Metadata format "%s" and all associated records deleted successfully! ',
- $prefix
- )
- ]);
- }
+ foreach (array_diff($inDatabase->getKeys(), array_keys($formats)) as $prefix) {
+ /** @var Format */
+ $format = $inDatabase[$prefix];
+ $this->em->delete(entity: $format);
+ $output->writeln([
+ sprintf(
+ format: ' [OK] Metadata format "%s" and all associated records deleted successfully! ',
+ values: $prefix
+ )
+ ]);
}
$this->clearResultCache();
- $currentFormats = array_keys(Database::getInstance()->getMetadataFormats()->getQueryResult());
+ $currentFormats = $this->em->getMetadataFormats()->getKeys();
if (count($currentFormats) > 0) {
$output->writeln(
[
@@ -118,7 +116,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
' command "php bin/cli oai:formats:update" again! ',
''
],
- 1 | 16
+ OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_QUIET
);
} else {
$output->writeln(
@@ -129,13 +127,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
' command "php bin/cli oai:formats:update" again! ',
''
],
- 1 | 16
+ OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_QUIET
);
}
- if (!$failure) {
- return Command::SUCCESS;
- } else {
- return Command::FAILURE;
- }
+ return $failure ? Command::FAILURE : Command::SUCCESS;
}
}
diff --git a/src/Database.php b/src/Database.php
deleted file mode 100644
index c34a3a2..0000000
--- a/src/Database.php
+++ /dev/null
@@ -1,573 +0,0 @@
-
- *
- * 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\Common\Collections\Criteria;
-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\Query\Expr\Join;
-use Doctrine\ORM\Tools\Pagination\Paginator;
-use OCC\Basics\Traits\Singleton;
-use OCC\OaiPmh2\Entity\Format;
-use OCC\OaiPmh2\Entity\Record;
-use OCC\OaiPmh2\Entity\Set;
-use OCC\OaiPmh2\Entity\Token;
-use OCC\OaiPmh2\Result;
-use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
-use Symfony\Component\Filesystem\Path;
-
-/**
- * Handles all database shenanigans.
- *
- * @author Sebastian Meyer
- * @package OAIPMH2
- *
- * @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 Format $newFormat The metadata format
- *
- * @return void
- */
- public function addOrUpdateMetadataFormat(Format $newFormat): void
- {
- $oldFormat = $this->entityManager->find(Format::class, $newFormat->getPrefix());
- if (isset($oldFormat)) {
- $oldFormat->setNamespace($newFormat->getNamespace());
- $oldFormat->setSchema($newFormat->getSchema());
- } else {
- $this->entityManager->persist($newFormat);
- }
- $this->entityManager->flush();
- }
-
- /**
- * Add or update record.
- *
- * @param Record $newRecord The record
- * @param bool $bulkMode Should we operate in bulk mode (no flush)?
- *
- * @return void
- */
- public function addOrUpdateRecord(Record $newRecord, bool $bulkMode = false): void
- {
- $oldRecord = $this->entityManager->find(
- Record::class,
- [
- 'identifier' => $newRecord->getIdentifier(),
- 'format' => $newRecord->getFormat()
- ]
- );
- if (isset($oldRecord)) {
- if ($newRecord->hasContent() || Configuration::getInstance()->deletedRecords !== 'no') {
- $oldRecord->setContent($newRecord->getContent(), false);
- $oldRecord->setLastChanged($newRecord->getLastChanged());
- // Add new sets.
- foreach (array_diff($newRecord->getSets(), $oldRecord->getSets()) as $newSet) {
- $oldRecord->addSet($newSet);
- }
- // Remove old sets.
- foreach (array_diff($oldRecord->getSets(), $newRecord->getSets()) as $oldSet) {
- $oldRecord->removeSet($oldSet);
- }
- } else {
- $this->entityManager->remove($oldRecord);
- }
- } else {
- if ($newRecord->hasContent() || Configuration::getInstance()->deletedRecords !== 'no') {
- $this->entityManager->persist($newRecord);
- }
- }
- if (!$bulkMode) {
- $this->entityManager->flush();
- }
- }
-
- /**
- * Add or update set.
- *
- * @param Set $newSet The set
- *
- * @return void
- */
- public function addOrUpdateSet(Set $newSet): void
- {
- $oldSet = $this->entityManager->find(Set::class, $newSet->getSpec());
- if (isset($oldSet)) {
- $oldSet->setName($newSet->getName());
- $oldSet->setDescription($newSet->getDescription());
- } else {
- $this->entityManager->persist($newSet);
- }
- $this->entityManager->flush();
- }
-
- /**
- * Delete metadata format and all associated records.
- *
- * @param Format $format The metadata format
- *
- * @return void
- */
- public function deleteMetadataFormat(Format $format): void
- {
- $dql = $this->entityManager->createQueryBuilder();
- $dql->delete(Record::class, 'record')
- ->where($dql->expr()->eq('record.format', ':format'))
- ->setParameter('format', $format->getPrefix());
- $query = $dql->getQuery();
- $query->execute();
-
- // Explicitly remove associations with sets for deleted records.
- $sql = $this->entityManager->getConnection();
- $sql->executeStatement("DELETE FROM records_sets WHERE record_format='{$format->getPrefix()}'");
-
- $this->entityManager->remove($format);
- $this->entityManager->flush();
-
- $this->pruneOrphanSets();
- }
-
- /**
- * Delete a record.
- *
- * @param Record $record The record
- *
- * @return void
- */
- public function deleteRecord(Record $record): void
- {
- if (Configuration::getInstance()->deletedRecords === 'no') {
- $this->entityManager->remove($record);
- } else {
- $record->setContent(null);
- $record->setLastChanged(new DateTime());
- }
- $this->entityManager->flush();
- $this->pruneOrphanSets();
- }
-
- /**
- * Flush all changes to the database.
- *
- * @param bool $clear Should the entity manager get cleared as well?
- * @return void
- */
- public function flush(bool $clear = false): void
- {
- $this->entityManager->flush();
- if ($clear) {
- $this->entityManager->clear();
- }
- }
-
- /**
- * Get all sets without pagination.
- *
- * @return Result The sets
- */
- public function getAllSets(): Result
- {
- $dql = $this->entityManager->createQueryBuilder();
- $dql->select('sets')
- ->from(Set::class, 'sets', 'sets.spec');
- $query = $dql->getQuery();
- $query->enableResultCache();
- /** @var Sets $resultQuery */
- $resultQuery = $query->getResult();
- return new Result($resultQuery);
- }
-
- /**
- * 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($dql->expr()->min('record.lastChanged'))
- ->from(Record::class, 'record');
- $query = $dql->getQuery();
- $query->enableResultCache();
- /** @var ?string $result */
- $result = $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
- return $result ?? $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(Record::class, 'record')
- ->where(
- $dql->expr()->andX(
- $dql->expr()->eq('record.identifier', ':identifier'),
- $dql->expr()->isNotNull('record.content')
- )
- )
- ->setParameter('identifier', $identifier);
- }
- $query = $dql->getQuery();
- $query->enableResultCache();
- /** @var Formats $queryResult */
- $queryResult = $query->getResult();
- return new Result($queryResult);
- }
-
- /**
- * Get a single record.
- *
- * @param string $identifier The record identifier
- * @param Format $format The metadata format
- *
- * @return ?Record The record or NULL on failure
- */
- public function getRecord(string $identifier, Format $format): ?Record
- {
- return $this->entityManager->find(
- Record::class,
- [
- 'identifier' => $identifier,
- 'format' => $format
- ]
- );
- }
-
- /**
- * Get list of records.
- *
- * @param string $verb The currently requested verb ('ListIdentifiers' or 'ListRecords')
- * @param Format $metadataPrefix The metadata format
- * @param int $counter Counter for split result sets
- * @param ?DateTime $from The "from" datestamp
- * @param ?DateTime $until The "until" datestamp
- * @param ?Set $set The set spec
- *
- * @return Result The records and possibly a resumtion token
- */
- public function getRecords(
- string $verb,
- Format $metadataPrefix,
- int $counter = 0,
- ?DateTime $from = null,
- ?DateTime $until = null,
- ?Set $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', $from);
- $from = $from->format('Y-m-d\TH:i:s\Z');
- }
- if (isset($until)) {
- $dql->andWhere($dql->expr()->lte('record.lastChanged', ':until'));
- $dql->setParameter('until', $until);
- $until = $until->format('Y-m-d\TH:i:s\Z');
- }
- if (isset($set)) {
- $dql->innerJoin(
- Set::class,
- 'sets',
- Join::WITH,
- $dql->expr()->orX(
- $dql->expr()->eq('sets.spec', ':setSpec'),
- $dql->expr()->like('sets.spec', ':setLike')
- )
- );
- $dql->setParameter('setSpec', $set->getSpec());
- $dql->setParameter('setLike', $set->getSpec() . ':%');
- $set = $set->getSpec();
- }
- $query = $dql->getQuery();
- /** @var Records $queryResult */
- $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->getPrefix(),
- '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(int $counter = 0): 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 $queryResult */
- $queryResult = $query->getResult();
- $result = new Result($queryResult);
- $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($dql->expr()->count('record.identifier'))
- ->from(Record::class, 'record')
- ->where($dql->expr()->eq('record.identifier', ':identifier'))
- ->setParameter('identifier', $identifier);
- $query = $dql->getQuery();
- return (bool) $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
- }
-
- /**
- * Prune deleted records.
- *
- * @return int The number of removed records
- */
- public function pruneDeletedRecords(): int
- {
- $repository = $this->entityManager->getRepository(Record::class);
- $criteria = Criteria::create()->where(Criteria::expr()->isNull('content'));
- $records = $repository->matching($criteria);
- foreach ($records as $record) {
- $this->entityManager->remove($record);
- }
- $this->entityManager->flush();
- $this->pruneOrphanSets();
- return count($records);
- }
-
- /**
- * Prune orphan sets.
- *
- * @return int The number of removed sets
- */
- public function pruneOrphanSets(): int
- {
- $repository = $this->entityManager->getRepository(Set::class);
- $sets = $repository->findAll();
- $count = 0;
- foreach ($sets as $set) {
- if ($set->isEmpty()) {
- $this->entityManager->remove($set);
- ++$count;
- }
- }
- $this->entityManager->flush();
- return $count;
- }
-
- /**
- * Prune expired resumption tokens.
- *
- * @return int The number of deleted tokens
- */
- public function pruneResumptionTokens(): int
- {
- $repository = $this->entityManager->getRepository(Token::class);
- $criteria = Criteria::create()->where(Criteria::expr()->lt('validUntil', new DateTime()));
- $tokens = $repository->matching($criteria);
- foreach ($tokens as $token) {
- $this->entityManager->remove($token);
- }
- $this->entityManager->flush();
- return count($tokens);
- }
-
- /**
- * 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__ . '/Entity'])
- );
- $configuration->setProxyDir(__DIR__ . '/../var/generated');
- $configuration->setProxyNamespace('OCC\OaiPmh2\Entity\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',
- 'postgresql' => 'pdo_pgsql',
- 'sqlite' => 'pdo_sqlite'
- ]);
- $connection = DriverManager::getConnection($parser->parse($dsn), $configuration);
-
- $this->entityManager = new EntityManager($connection, $configuration);
- }
-}
diff --git a/src/Entity.php b/src/Entity.php
index 4e666ea..0f46dcf 100644
--- a/src/Entity.php
+++ b/src/Entity.php
@@ -22,9 +22,10 @@
namespace OCC\OaiPmh2;
-use Symfony\Component\Validator\Constraints as Assert;
+use OCC\OaiPmh2\Validator\RegExValidator;
+use OCC\OaiPmh2\Validator\UrlValidator;
+use OCC\OaiPmh2\Validator\XmlValidator;
use Symfony\Component\Validator\Exception\ValidationFailedException;
-use Symfony\Component\Validator\Validation;
/**
* Base class for all Doctrine/ORM entities.
@@ -45,17 +46,13 @@ abstract class Entity
*/
protected function validateUrl(string $url): string
{
- $url = trim($url);
- $validator = Validation::createValidator();
- $violations = $validator->validate(
- $url,
- [
- new Assert\Url(),
- new Assert\NotBlank()
- ]
- );
+ $url = trim(string: $url);
+ $violations = UrlValidator::validate(url: $url);
if ($violations->count() > 0) {
- throw new ValidationFailedException(null, $violations);
+ throw new ValidationFailedException(
+ value: null,
+ violations: $violations
+ );
}
return $url;
}
@@ -72,18 +69,12 @@ protected function validateUrl(string $url): string
*/
protected function validateRegEx(string $string, string $regEx): string
{
- $validator = Validation::createValidator();
- $violations = $validator->validate(
- $string,
- [
- new Assert\Regex([
- 'pattern' => $regEx,
- 'message' => 'This value does not match the regular expression "{{ pattern }}".'
- ])
- ]
- );
+ $violations = RegExValidator::validate(string: $string, regEx: $regEx);
if ($violations->count() > 0) {
- throw new ValidationFailedException(null, $violations);
+ throw new ValidationFailedException(
+ value: null,
+ violations: $violations
+ );
}
return $string;
}
@@ -99,19 +90,12 @@ protected function validateRegEx(string $string, string $regEx): string
*/
protected function validateXml(string $xml): string
{
- $validator = Validation::createValidator();
- $violations = $validator->validate(
- $xml,
- [
- new Assert\Type('string'),
- new Assert\NotBlank()
- ]
- );
- if (
- $violations->count() > 0
- or simplexml_load_string($xml) === false
- ) {
- throw new ValidationFailedException(null, $violations);
+ $violations = XmlValidator::validate(xml: $xml);
+ if ($violations->count() > 0) {
+ throw new ValidationFailedException(
+ value: null,
+ violations: $violations
+ );
}
return $xml;
}
diff --git a/src/Entity/Format.php b/src/Entity/Format.php
index b7ad9b0..cf7ad73 100644
--- a/src/Entity/Format.php
+++ b/src/Entity/Format.php
@@ -22,8 +22,11 @@
namespace OCC\OaiPmh2\Entity;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use OCC\OaiPmh2\Entity;
+use OCC\OaiPmh2\Repository\FormatRepository;
use Symfony\Component\Validator\Exception\ValidationFailedException;
/**
@@ -32,9 +35,9 @@
* @author Sebastian Meyer
* @package OAIPMH2
*/
-#[ORM\Entity]
+#[ORM\Entity(repositoryClass: FormatRepository::class)]
#[ORM\Table(name: 'formats')]
-class Format extends Entity
+final class Format extends Entity
{
/**
* The unique metadata prefix.
@@ -56,7 +59,21 @@ class Format extends Entity
private string $xmlSchema;
/**
- * Get the format's namespace URI.
+ * The format's associated records.
+ *
+ * @var Collection
+ */
+ #[ORM\OneToMany(
+ targetEntity: Record::class,
+ mappedBy: 'format',
+ fetch: 'EXTRA_LAZY',
+ orphanRemoval: true,
+ indexBy: 'identifier'
+ )]
+ private Collection $records;
+
+ /**
+ * Get the namespace URI for this format.
*
* @return string The namespace URI
*/
@@ -76,7 +93,17 @@ public function getPrefix(): string
}
/**
- * Get the format's schema URL.
+ * Get the associated records for this format.
+ *
+ * @return Collection The collection of records
+ */
+ public function getRecords(): Collection
+ {
+ return $this->records;
+ }
+
+ /**
+ * Get the schema URL for this format.
*
* @return string The schema URL
*/
@@ -86,7 +113,7 @@ public function getSchema(): string
}
/**
- * Set the format's namespace URI.
+ * Set the namespace URI for this format.
*
* @param string $namespace The namespace URI
*
@@ -97,14 +124,14 @@ public function getSchema(): string
public function setNamespace(string $namespace): void
{
try {
- $this->namespace = $this->validateUrl($namespace);
+ $this->namespace = $this->validateUrl(url: $namespace);
} catch (ValidationFailedException $exception) {
throw $exception;
}
}
/**
- * Set the format's schema URL.
+ * Set the schema URL for this format.
*
* @param string $schema The schema URL
*
@@ -115,7 +142,7 @@ public function setNamespace(string $namespace): void
public function setSchema(string $schema): void
{
try {
- $this->xmlSchema = $this->validateUrl($schema);
+ $this->xmlSchema = $this->validateUrl(url: $schema);
} catch (ValidationFailedException $exception) {
throw $exception;
}
@@ -133,9 +160,13 @@ public function setSchema(string $schema): void
public function __construct(string $prefix, string $namespace, string $schema)
{
try {
- $this->prefix = $this->validateRegEx($prefix, '/^[A-Za-z0-9\-_\.!~\*\'\(\)]+$/');
- $this->setNamespace($namespace);
- $this->setSchema($schema);
+ $this->prefix = $this->validateRegEx(
+ string: $prefix,
+ regEx: '/^[A-Za-z0-9\-_\.!~\*\'\(\)]+$/'
+ );
+ $this->setNamespace(namespace: $namespace);
+ $this->setSchema(schema: $schema);
+ $this->records = new ArrayCollection();
} catch (ValidationFailedException $exception) {
throw $exception;
}
diff --git a/src/Entity/Record.php b/src/Entity/Record.php
index 1804917..42dd4c9 100644
--- a/src/Entity/Record.php
+++ b/src/Entity/Record.php
@@ -27,6 +27,7 @@
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use OCC\OaiPmh2\Entity;
+use OCC\OaiPmh2\Repository\RecordRepository;
use Symfony\Component\Validator\Exception\ValidationFailedException;
/**
@@ -35,13 +36,13 @@
* @author Sebastian Meyer
* @package OAIPMH2
*/
-#[ORM\Entity]
+#[ORM\Entity(repositoryClass: RecordRepository::class)]
#[ORM\Table(name: 'records')]
#[ORM\Index(name: 'identifier_idx', columns: ['identifier'])]
#[ORM\Index(name: 'format_idx', columns: ['format'])]
#[ORM\Index(name: 'last_changed_idx', columns: ['last_changed'])]
#[ORM\Index(name: 'format_last_changed_idx', columns: ['format', 'last_changed'])]
-class Record extends Entity
+final class Record extends Entity
{
/**
* The record identifier.
@@ -54,8 +55,16 @@ class Record extends Entity
* The associated format.
*/
#[ORM\Id]
- #[ORM\ManyToOne(targetEntity: Format::class, fetch: 'EXTRA_LAZY')]
- #[ORM\JoinColumn(name: 'format', referencedColumnName: 'prefix')]
+ #[ORM\ManyToOne(
+ targetEntity: Format::class,
+ fetch: 'EXTRA_LAZY',
+ inversedBy: 'records'
+ )]
+ #[ORM\JoinColumn(
+ name: 'format',
+ referencedColumnName: 'prefix',
+ onDelete: 'CASCADE'
+ )]
private Format $format;
/**
@@ -75,7 +84,13 @@ class Record extends Entity
*
* @var Collection
*/
- #[ORM\ManyToMany(targetEntity: Set::class, inversedBy: 'records', indexBy: 'spec', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
+ #[ORM\ManyToMany(
+ targetEntity: Set::class,
+ inversedBy: 'records',
+ cascade: ['persist'],
+ fetch: 'EXTRA_LAZY',
+ indexBy: 'spec'
+ )]
#[ORM\JoinTable(name: 'records_sets')]
#[ORM\JoinColumn(name: 'record_identifier', referencedColumnName: 'identifier')]
#[ORM\JoinColumn(name: 'record_format', referencedColumnName: 'format')]
@@ -91,9 +106,9 @@ class Record extends Entity
*/
public function addSet(Set $set): void
{
- if (!$this->sets->contains($set)) {
- $this->sets->add($set);
- $set->addRecord($this);
+ if (!$this->sets->contains(element: $set)) {
+ $this->sets->add(element: $set);
+ $set->addRecord(record: $this);
}
}
@@ -146,23 +161,26 @@ public function getLastChanged(): DateTime
*/
public function getSet(string $setSpec): ?Set
{
- return $this->sets->get($setSpec);
+ return $this->sets->get(key: $setSpec);
}
/**
* Get a collection of associated sets.
*
- * @return array The associated sets
+ * @return Collection The associated sets
*/
- public function getSets(): array
+ public function getSets(): Collection
{
- return $this->sets->toArray();
+ return $this->sets;
}
/**
* Whether this record has any content.
*
* @return bool TRUE if content exists, FALSE otherwise
+ *
+ * @psalm-assert-if-true string $this->content
+ * @psalm-assert-if-true string $this->getContent()
*/
public function hasContent(): bool
{
@@ -178,9 +196,9 @@ public function hasContent(): bool
*/
public function removeSet(Set $set): void
{
- if ($this->sets->contains($set)) {
- $this->sets->removeElement($set);
- $set->removeRecord($this);
+ if ($this->sets->contains(element: $set)) {
+ $this->sets->removeElement(element: $set);
+ $set->removeRecord(record: $this);
}
}
@@ -200,7 +218,7 @@ public function setContent(?string $data = null, bool $validate = true): void
$data = trim($data);
if ($validate) {
try {
- $data = $this->validateXml($data);
+ $data = $this->validateXml(xml: $data);
} catch (ValidationFailedException $exception) {
throw $exception;
}
@@ -250,13 +268,13 @@ public function __construct(string $identifier, Format $format, ?string $data =
{
try {
$this->identifier = $this->validateRegEx(
- $identifier,
+ string: $identifier,
// xs:anyURI
- '/^(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?\/{0,2}[0-9a-zA-Z;\/?:@&=+$\\.\\-_!~*\'()%]+)?(#[0-9a-zA-Z;\/?:@&=+$\\.\\-_!~*\'()%]+)?$/'
+ regEx: '/^(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?\/{0,2}[0-9a-zA-Z;\/?:@&=+$\\.\\-_!~*\'()%]+)?(#[0-9a-zA-Z;\/?:@&=+$\\.\\-_!~*\'()%]+)?$/'
);
- $this->setFormat($format);
- $this->setContent($data);
- $this->setLastChanged($lastChanged);
+ $this->setFormat(format: $format);
+ $this->setContent(data: $data);
+ $this->setLastChanged(dateTime: $lastChanged);
$this->sets = new ArrayCollection();
} catch (ValidationFailedException $exception) {
throw $exception;
diff --git a/src/Entity/Set.php b/src/Entity/Set.php
index 0551f63..c4fcf83 100644
--- a/src/Entity/Set.php
+++ b/src/Entity/Set.php
@@ -26,6 +26,7 @@
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use OCC\OaiPmh2\Entity;
+use OCC\OaiPmh2\Repository\SetRepository;
use Symfony\Component\Validator\Exception\ValidationFailedException;
/**
@@ -34,9 +35,9 @@
* @author Sebastian Meyer
* @package OAIPMH2
*/
-#[ORM\Entity]
+#[ORM\Entity(repositoryClass: SetRepository::class)]
#[ORM\Table(name: 'sets')]
-class Set extends Entity
+final class Set extends Entity
{
/**
* The unique set spec.
@@ -60,9 +61,14 @@ class Set extends Entity
/**
* Collection of associated records.
*
- * @var Collection
+ * @var Collection
*/
- #[ORM\ManyToMany(targetEntity: Record::class, mappedBy: 'sets', fetch: 'EXTRA_LAZY')]
+ #[ORM\ManyToMany(
+ targetEntity: Record::class,
+ mappedBy: 'sets',
+ fetch: 'EXTRA_LAZY',
+ indexBy: 'identifier'
+ )]
private Collection $records;
/**
@@ -74,9 +80,9 @@ class Set extends Entity
*/
public function addRecord(Record $record): void
{
- if (!$this->records->contains($record)) {
- $this->records->add($record);
- $record->addSet($this);
+ if (!$this->records->contains(element: $record)) {
+ $this->records->add(element: $record);
+ $record->addSet(set: $this);
}
}
@@ -113,17 +119,20 @@ public function getSpec(): string
/**
* Get a collection of associated records.
*
- * @return array The associated records
+ * @return Collection The associated records
*/
- public function getRecords(): array
+ public function getRecords(): Collection
{
- return $this->records->toArray();
+ return $this->records;
}
/**
* Whether this set has a description.
*
* @return bool TRUE if description exists, FALSE otherwise
+ *
+ * @psalm-assert-if-true string $this->description
+ * @psalm-assert-if-true string $this->getDescription()
*/
public function hasDescription(): bool
{
@@ -149,16 +158,16 @@ public function isEmpty(): bool
*/
public function removeRecord(Record $record): void
{
- if ($this->records->contains($record)) {
- $this->records->removeElement($record);
- $record->removeSet($this);
+ if ($this->records->contains(element: $record)) {
+ $this->records->removeElement(element: $record);
+ $record->removeSet(set: $this);
}
}
/**
* Set the description for this set.
*
- * @param ?string $description The description
+ * @param ?string $description The description XML or NULL to unset
*
* @return void
*
@@ -169,7 +178,7 @@ public function setDescription(?string $description): void
if (isset($description)) {
$description = trim($description);
try {
- $description = $this->validateXml($description);
+ $description = $this->validateXml(xml: $description);
} catch (ValidationFailedException $exception) {
throw $exception;
}
@@ -198,15 +207,15 @@ public function setName(?string $name): void
*
* @throws ValidationFailedException
*/
- public function __construct(string $spec, ?string $name = null, string $description = null)
+ public function __construct(string $spec, ?string $name = null, ?string $description = null)
{
try {
$this->spec = $this->validateRegEx(
- $spec,
- '/^([A-Za-z0-9\-_\.!~\*\'\(\)])+(:[A-Za-z0-9\-_\.!~\*\'\(\)]+)*$/'
+ string: $spec,
+ regEx: '/^([A-Za-z0-9\-_\.!~\*\'\(\)])+(:[A-Za-z0-9\-_\.!~\*\'\(\)]+)*$/'
);
- $this->setName($name);
- $this->setDescription($description);
+ $this->setName(name: $name);
+ $this->setDescription(description: $description);
$this->records = new ArrayCollection();
} catch (ValidationFailedException $exception) {
throw $exception;
diff --git a/src/Entity/Token.php b/src/Entity/Token.php
index 10f5313..e4a3dcc 100644
--- a/src/Entity/Token.php
+++ b/src/Entity/Token.php
@@ -27,17 +27,20 @@
use Doctrine\ORM\Mapping as ORM;
use OCC\OaiPmh2\Configuration;
use OCC\OaiPmh2\Entity;
+use OCC\OaiPmh2\Repository\TokenRepository;
/**
* Doctrine/ORM Entity for resumption tokens.
*
* @author Sebastian Meyer
* @package OAIPMH2
+ *
+ * @psalm-import-type OaiRequestMetadata from \OCC\OaiPmh2\Middleware
*/
-#[ORM\Entity]
+#[ORM\Entity(repositoryClass: TokenRepository::class)]
#[ORM\Table(name: 'tokens')]
#[ORM\Index(name: 'valid_until_idx', columns: ['valid_until'])]
-class Token extends Entity
+final class Token extends Entity
{
/**
* The resumption token.
@@ -77,11 +80,11 @@ public function getToken(): string
/**
* Get the query parameters.
*
- * @return array The query parameters
+ * @return OaiRequestMetadata The query parameters
*/
public function getParameters(): array
{
- /** @var array */
+ /** @var OaiRequestMetadata */
return unserialize($this->parameters);
}
@@ -109,15 +112,15 @@ public function getVerb(): string
* Get new entity of resumption token.
*
* @param string $verb The verb for which the token is issued
- * @param array $parameters The query parameters
+ * @param OaiRequestMetadata $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;
+ $validUntil = new DateTime();
+ $validUntil->add(interval: new DateInterval(duration: 'PT' . Configuration::getInstance()->tokenValid . 'S'));
+ $this->validUntil = $validUntil;
}
}
diff --git a/src/EntityManager.php b/src/EntityManager.php
new file mode 100644
index 0000000..7844e43
--- /dev/null
+++ b/src/EntityManager.php
@@ -0,0 +1,471 @@
+
+ *
+ * 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\Decorator\EntityManagerDecorator;
+use Doctrine\ORM\EntityManager as DoctrineEntityManager;
+use Doctrine\ORM\Mapping\Driver\AttributeDriver;
+use Doctrine\ORM\Proxy\ProxyFactory;
+use Doctrine\ORM\Query\Expr\Join;
+use Doctrine\ORM\Tools\Pagination\Paginator;
+use OCC\Basics\Traits\Singleton;
+use OCC\OaiPmh2\Entity\Format;
+use OCC\OaiPmh2\Entity\Record;
+use OCC\OaiPmh2\Entity\Set;
+use OCC\OaiPmh2\Entity\Token;
+use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
+use Symfony\Component\Filesystem\Path;
+
+/**
+ * The Entity Manager controls all database shenanigans.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ *
+ * @mixin DoctrineEntityManager
+ *
+ * @psalm-import-type Params from DriverManager
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
+final class EntityManager extends EntityManagerDecorator
+{
+ use Singleton;
+
+ /**
+ * The database tables this class is allowed to handle.
+ *
+ * @var string[]
+ */
+ private const TABLES = [
+ 'formats',
+ 'records',
+ 'records_sets',
+ 'sets',
+ 'tokens'
+ ];
+
+ /**
+ * Add or update entity.
+ *
+ * @param Format|Record|Set|Token $entity The entity
+ * @param bool $bulkMode Should we operate in bulk mode (no flush)?
+ *
+ * @return void
+ */
+ public function addOrUpdate(Format|Record|Set|Token $entity, bool $bulkMode = false): void
+ {
+ $this->getRepository(className: get_class($entity))->addOrUpdate(entity: $entity);
+ if (!$bulkMode) {
+ $this->flush();
+ }
+ }
+
+ /**
+ * Delete entity.
+ *
+ * @param Format|Record|Set|Token $entity The entity
+ *
+ * @return void
+ */
+ public function delete(Format|Record|Set|Token $entity): void
+ {
+ $this->getRepository(className: get_class($entity))->delete(entity: $entity);
+ }
+
+ /**
+ * Get the earliest datestamp of any record.
+ *
+ * @return string The earliest datestamp
+ */
+ public function getEarliestDatestamp(): string
+ {
+ $timestamp = '0000-00-00T00:00:00Z';
+ $dql = $this->createQueryBuilder();
+ $dql->select(select: $dql->expr()->min('record.lastChanged'));
+ $dql->from(from: Record::class, alias: 'record');
+ $query = $dql->getQuery()->enableResultCache();
+ /** @var ?string $result */
+ $result = $query->getOneOrNullResult(hydrationMode: AbstractQuery::HYDRATE_SCALAR_COLUMN);
+ return $result ?? $timestamp;
+ }
+
+ /**
+ * Get reference to a single metadata format.
+ *
+ * @param string $prefix The metadata prefix
+ *
+ * @return ?Format The reference to the metadata format or NULL if invalid
+ */
+ public function getMetadataFormat(string $prefix): ?Format
+ {
+ return $this->getReference(entityName: Format::class, id: $prefix);
+ }
+
+ /**
+ * Get all available metadata formats (optionally for a given record identifier).
+ *
+ * @param ?string $recordIdentifier Optional record identifier
+ *
+ * @return ResultSet The metadata formats indexed by prefix
+ */
+ public function getMetadataFormats(?string $recordIdentifier = null): ResultSet
+ {
+ $entities = [];
+ if ($recordIdentifier === null) {
+ $formats = $this->getRepository(className: Format::class)->findAll();
+ } else {
+ $dql = $this->createQueryBuilder();
+ $dql->select(select: 'record.format')
+ ->from(from: Record::class, alias: 'record')
+ ->where(predicates: $dql->expr()->eq('record.identifier', ':recordIdentifier'))
+ ->setParameter(key: 'recordIdentifier', value: $recordIdentifier);
+ $query = $dql->getQuery()->enableResultCache();
+ /** @var Format[] */
+ $formats = $query->getResult(hydrationMode: AbstractQuery::HYDRATE_OBJECT);
+ }
+ foreach ($formats as $format) {
+ $entities[$format->getPrefix()] = $format;
+ }
+ return new ResultSet(elements: $entities);
+ }
+
+ /**
+ * Get a single record.
+ *
+ * @param string $identifier The record identifier
+ * @param string $format The metadata prefix
+ *
+ * @return ?Record The record or NULL if invalid
+ */
+ public function getRecord(string $identifier, string $format): ?Record
+ {
+ return $this->getRepository(className: Record::class)->findOneBy(
+ criteria: [
+ 'identifier' => $identifier,
+ 'format' => $this->getMetadataFormat(prefix: $format)
+ ]
+ );
+ }
+
+ /**
+ * 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 ResultSet The records indexed by id and maybe a resumption token
+ */
+ public function getRecords(
+ string $verb,
+ string $metadataPrefix,
+ int $counter = 0,
+ ?string $from = null,
+ ?string $until = null,
+ ?string $set = null
+ ): ResultSet {
+ $maxRecords = Configuration::getInstance()->maxRecords;
+ $cursor = $counter * $maxRecords;
+
+ $dql = $this->createQueryBuilder();
+ $dql->select(select: 'record')
+ ->from(from: Record::class, alias: 'record', indexBy: 'record.identifier')
+ ->where(predicates: $dql->expr()->eq('record.format', ':metadataPrefix'))
+ ->setParameter(
+ key: 'metadataPrefix',
+ value: $this->getMetadataFormat(prefix: $metadataPrefix)
+ )
+ ->setFirstResult(firstResult: $cursor)
+ ->setMaxResults(maxResults: $maxRecords);
+ if (isset($from)) {
+ $dql->andWhere(where: $dql->expr()->gte('record.lastChanged', ':from'));
+ $dql->setParameter(key: 'from', value: new DateTime($from));
+ }
+ if (isset($until)) {
+ $dql->andWhere(where: $dql->expr()->lte('record.lastChanged', ':until'));
+ $dql->setParameter(key: 'until', value: new DateTime($until));
+ }
+ if (isset($set)) {
+ $dql->innerJoin(
+ join: Set::class,
+ alias: 'sets',
+ conditionType: Join::WITH,
+ condition: $dql->expr()->orX(
+ $dql->expr()->eq('sets.spec', ':setSpec'),
+ $dql->expr()->like('sets.spec', ':setLike')
+ )
+ );
+ $dql->setParameter(key: 'setSpec', value: $set);
+ $dql->setParameter(key: 'setLike', value: $set . ':%');
+ }
+ $query = $dql->getQuery();
+ /** @var array */
+ $queryResult = $query->getResult();
+ $result = new ResultSet(elements: $queryResult);
+ $paginator = new Paginator(query: $query, fetchJoinCollection: true);
+ if (count($paginator) > ($cursor + count($result))) {
+ $token = new Token(
+ verb: $verb,
+ parameters: [
+ 'verb' => $verb,
+ 'identifier' => null,
+ 'metadataPrefix' => $metadataPrefix,
+ 'from' => $from,
+ 'until' => $until,
+ 'set' => $set,
+ 'resumptionToken' => null,
+ 'counter' => $counter + 1,
+ 'completeListSize' => count($paginator)
+ ]
+ );
+ $this->persist(object: $token);
+ $this->flush();
+ $result->setResumptionToken(token: $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
+ {
+ $resumptionToken = $this->getRepository(className: Token::class)->findOneBy(
+ criteria: [
+ 'token' => $token,
+ 'verb' => $verb
+ ]
+ );
+ if (isset($resumptionToken) && $resumptionToken->getValidUntil() < new DateTime()) {
+ $this->delete(entity: $resumptionToken);
+ return null;
+ }
+ return $resumptionToken;
+ }
+
+ /**
+ * Get reference to a single set.
+ *
+ * @param string $spec The set spec
+ *
+ * @return ?Set The reference to the set or NULL if invalid
+ */
+ public function getSet(string $spec): ?Set
+ {
+ return $this->getReference(entityName: Set::class, id: $spec);
+ }
+
+ /**
+ * Get all available sets.
+ *
+ * @param int $counter Counter for split result sets
+ *
+ * @return ResultSet The sets indexed by spec
+ */
+ public function getSets(int $counter = 0): ResultSet
+ {
+ $maxRecords = Configuration::getInstance()->maxRecords;
+ $cursor = $counter * $maxRecords;
+
+ $dql = $this->createQueryBuilder();
+ $dql->select(select: 'set')
+ ->from(from: Set::class, alias: 'set', indexBy: 'set.spec')
+ ->setFirstResult(firstResult: $cursor)
+ ->setMaxResults(maxResults: $maxRecords);
+ $query = $dql->getQuery()->enableResultCache();
+ /** @var array */
+ $queryResult = $query->getResult(hydrationMode: AbstractQuery::HYDRATE_OBJECT);
+ $result = new ResultSet(elements: $queryResult);
+ $paginator = new Paginator(query: $query);
+ if (count($paginator) > ($cursor + count($result))) {
+ $token = new Token(
+ verb: 'ListSets',
+ parameters: [
+ 'verb' => 'ListSets',
+ 'identifier' => null,
+ 'metadataPrefix' => null,
+ 'from' => null,
+ 'until' => null,
+ 'set' => null,
+ 'resumptionToken' => null,
+ 'counter' => $counter + 1,
+ 'completeListSize' => count($paginator)
+ ]
+ );
+ $this->persist(object: $token);
+ $this->flush();
+ $result->setResumptionToken(token: $token);
+ }
+ return $result;
+ }
+
+ /**
+ * Check if a record with the given identifier exists.
+ *
+ * @param string $identifier The record identifier
+ *
+ * @return bool Whether a record with the identifier exists
+ */
+ public function isValidRecordIdentifier(string $identifier): bool
+ {
+ $records = $this->getRepository(className: Record::class)->findBy(criteria: ['identifier' => $identifier]);
+ return (bool) count($records) > 0;
+ }
+
+ /**
+ * Prune deleted records.
+ *
+ * @return int The number of removed records
+ */
+ public function pruneDeletedRecords(): int
+ {
+ $dql = $this->createQueryBuilder();
+ $dql->delete(delete: Record::class, alias: 'record')
+ ->where(predicates: $dql->expr()->isNull('record.content'));
+ /** @var int */
+ $deleted = $dql->getQuery()->execute();
+ if ($deleted > 0) {
+ $this->pruneOrphanedSets();
+ }
+ return $deleted;
+ }
+
+ /**
+ * Prune expired resumption tokens.
+ *
+ * @return int The number of deleted tokens
+ */
+ public function pruneExpiredTokens(): int
+ {
+ $dql = $this->createQueryBuilder();
+ $dql->delete(delete: Token::class, alias: 'token')
+ ->where(predicates: $dql->expr()->lt('token.validUntil', new DateTime()));
+ /** @var int */
+ return $dql->getQuery()->execute();
+ }
+
+ /**
+ * Prune orphan sets.
+ *
+ * @return int The number of removed sets
+ */
+ public function pruneOrphanedSets(): int
+ {
+ $sets = $this->getRepository(className: Set::class)->findAll();
+ $count = 0;
+ foreach ($sets as $set) {
+ if ($set->isEmpty()) {
+ $count += 1;
+ $this->remove(object: $set);
+ }
+ }
+ if ($count > 0) {
+ $this->flush();
+ }
+ return $count;
+ }
+
+ /**
+ * Instantiate new Doctrine entity manager and connect to database.
+ */
+ private function __construct()
+ {
+ $config = new DoctrineConfiguration();
+ $config->setAutoGenerateProxyClasses(
+ autoGenerate: ProxyFactory::AUTOGENERATE_NEVER
+ );
+ $config->setMetadataCache(
+ cache: new PhpFilesAdapter(
+ namespace: 'Metadata',
+ directory: __DIR__ . '/../var/cache'
+ )
+ );
+ $config->setMetadataDriverImpl(
+ driverImpl: new AttributeDriver(
+ paths: [__DIR__ . '/Entity']
+ )
+ );
+ $config->setProxyDir(dir: __DIR__ . '/../var/generated');
+ $config->setProxyNamespace(ns: 'OCC\OaiPmh2\Entity\Proxy');
+ $config->setQueryCache(
+ cache: new PhpFilesAdapter(
+ namespace: 'Query',
+ directory: __DIR__ . '/../var/cache'
+ )
+ );
+ $config->setResultCache(
+ cache: new PhpFilesAdapter(
+ namespace: 'Result',
+ directory: __DIR__ . '/../var/cache'
+ )
+ );
+ $config->setSchemaAssetsFilter(
+ schemaAssetsFilter: static function (string|AbstractAsset $assetName): bool {
+ if ($assetName instanceof AbstractAsset) {
+ $assetName = $assetName->getName();
+ }
+ return in_array(needle: $assetName, haystack: self::TABLES, strict: true);
+ }
+ );
+
+ $baseDir = Path::canonicalize(path: __DIR__ . '/../');
+ $dsn = str_replace(
+ search: '%BASEDIR%',
+ replace: $baseDir,
+ subject: Configuration::getInstance()->database
+ );
+ $parser = new DsnParser(
+ schemeMapping: [
+ 'mariadb' => 'pdo_mysql',
+ 'mssql' => 'pdo_sqlsrv',
+ 'mysql' => 'pdo_mysql',
+ 'oracle' => 'pdo_oci',
+ 'postgresql' => 'pdo_pgsql',
+ 'sqlite' => 'pdo_sqlite'
+ ]
+ );
+ $conn = DriverManager::getConnection(
+ // Generic return type of DsnParser::parse() is not correctly recognized.
+ // phpcs:ignore
+ params: $parser->parse(dsn: $dsn),
+ config: $config
+ );
+
+ parent::__construct(new DoctrineEntityManager(conn: $conn, config: $config));
+ }
+}
diff --git a/src/Middleware.php b/src/Middleware.php
index 8ca7538..18994d7 100644
--- a/src/Middleware.php
+++ b/src/Middleware.php
@@ -22,7 +22,9 @@
namespace OCC\OaiPmh2;
+use DOMElement;
use GuzzleHttp\Psr7\Utils;
+use OCC\OaiPmh2\Entity\Token;
use OCC\OaiPmh2\Middleware\ErrorHandler;
use OCC\PSR15\AbstractMiddleware;
use Psr\Http\Message\ResponseInterface;
@@ -33,13 +35,99 @@
*
* @author Sebastian Meyer
* @package OAIPMH2
+ *
+ * @psalm-type OaiRequestMetadata = array{
+ * verb: string,
+ * identifier: ?string,
+ * metadataPrefix: ?string,
+ * from: ?string,
+ * until: ?string,
+ * set: ?string,
+ * resumptionToken: ?string,
+ * counter: int,
+ * completeListSize: int
+ * }
*/
abstract class Middleware extends AbstractMiddleware
{
+ /**
+ * This holds the request metadata.
+ *
+ * @var OaiRequestMetadata
+ */
+ protected array $arguments = [
+ 'verb' => '',
+ 'identifier' => null,
+ 'metadataPrefix' => null,
+ 'from' => null,
+ 'until' => null,
+ 'set' => null,
+ 'resumptionToken' => null,
+ 'counter' => 0,
+ 'completeListSize' => 0
+ ];
+
+ /**
+ * This holds the entity manager singleton.
+ */
+ protected EntityManager $em;
+
/**
* This holds the prepared response document.
*/
- protected Document $preparedResponse;
+ protected Response $preparedResponse;
+
+ /**
+ * Add resumption token information to response document.
+ *
+ * @param DOMElement $node The DOM node to add the resumption token to
+ * @param ?Token $token The new resumption token or NULL if none
+ *
+ * @return void
+ */
+ protected function addResumptionToken(DOMElement $node, ?Token $token): void
+ {
+ if (isset($token) || isset($this->arguments['resumptionToken'])) {
+ $resumptionToken = $this->preparedResponse->createElement(localName: 'resumptionToken');
+ if (isset($token)) {
+ $resumptionToken->nodeValue = $token->getToken();
+ $resumptionToken->setAttribute(
+ qualifiedName: 'expirationDate',
+ value: $token->getValidUntil()->format(format: 'Y-m-d\TH:i:s\Z')
+ );
+ $this->arguments['completeListSize'] = $token->getParameters()['completeListSize'];
+ }
+ $resumptionToken->setAttribute(
+ qualifiedName: 'completeListSize',
+ value: (string) $this->arguments['completeListSize']
+ );
+ $resumptionToken->setAttribute(
+ qualifiedName: 'cursor',
+ value: (string) ($this->arguments['counter'] * Configuration::getInstance()->maxRecords)
+ );
+ $node->appendChild(node: $resumptionToken);
+ }
+ }
+
+ /**
+ * Check for resumption token and populate request arguments.
+ *
+ * @return void
+ */
+ protected function checkResumptionToken(): void
+ {
+ if (isset($this->arguments['resumptionToken'])) {
+ $token = $this->em->getResumptionToken(
+ token: $this->arguments['resumptionToken'],
+ verb: $this->arguments['verb']
+ );
+ if (isset($token)) {
+ $this->arguments = array_merge($this->arguments, $token->getParameters());
+ } else {
+ ErrorHandler::getInstance()->withError(errorCode: 'badResumptionToken');
+ }
+ }
+ }
/**
* Prepare response document.
@@ -59,7 +147,10 @@ abstract protected function prepareResponse(ServerRequestInterface $request): vo
*/
protected function processRequest(ServerRequestInterface $request): ServerRequestInterface
{
- $this->prepareResponse($request);
+ /** @var OaiRequestMetadata */
+ $arguments = $request->getAttributes();
+ $this->arguments = array_merge($this->arguments, $arguments);
+ $this->prepareResponse(request: $request);
return $request;
}
@@ -73,18 +164,22 @@ protected function processRequest(ServerRequestInterface $request): ServerReques
protected function processResponse(ResponseInterface $response): ResponseInterface
{
if (!ErrorHandler::getInstance()->hasErrors() && isset($this->preparedResponse)) {
- $response = $response->withBody(Utils::streamFor((string) $this->preparedResponse));
+ $response = $response->withBody(
+ body: Utils::streamFor(
+ resource: (string) $this->preparedResponse
+ )
+ );
}
return $response;
}
/**
* The constructor must have the same signature for all derived classes, thus make it final.
+ *
+ * @see https://psalm.dev/229
*/
final public function __construct()
{
- // Make constructor final to avoid issues in dispatcher.
- // @see https://psalm.dev/229
+ $this->em = EntityManager::getInstance();
}
-
}
diff --git a/src/Middleware/Dispatcher.php b/src/Middleware/Dispatcher.php
index 101272e..646ed80 100644
--- a/src/Middleware/Dispatcher.php
+++ b/src/Middleware/Dispatcher.php
@@ -22,6 +22,7 @@
namespace OCC\OaiPmh2\Middleware;
+use OCC\OaiPmh2\EntityManager;
use OCC\OaiPmh2\Middleware;
use OCC\PSR15\AbstractMiddleware;
use Psr\Http\Message\ResponseInterface;
@@ -37,6 +38,8 @@ class Dispatcher extends AbstractMiddleware
{
/**
* List of defined OAI-PMH parameters.
+ *
+ * @var string[]
*/
protected const OAI_PARAMS = [
'verb',
@@ -62,14 +65,14 @@ protected function getRequestWithAttributes(ServerRequestInterface $request): Se
/** @var array */
$arguments = $request->getQueryParams();
} elseif ($request->getMethod() === 'POST') {
- if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') {
+ if ($request->getHeaderLine(name: 'Content-Type') === 'application/x-www-form-urlencoded') {
/** @var array */
$arguments = (array) $request->getParsedBody();
}
}
- if ($this->validateArguments($arguments)) {
+ if ($this->validateArguments(arguments: $arguments)) {
foreach ($arguments as $param => $value) {
- $request = $request->withAttribute($param, $value);
+ $request = $request->withAttribute(name: $param, value: $value);
}
}
return $request;
@@ -84,17 +87,17 @@ protected function getRequestWithAttributes(ServerRequestInterface $request): Se
*/
protected function processRequest(ServerRequestInterface $request): ServerRequestInterface
{
- $request = $this->getRequestWithAttributes($request);
+ $request = $this->getRequestWithAttributes(request: $request);
$errorHandler = ErrorHandler::getInstance();
if (!$errorHandler->hasErrors()) {
/** @var string */
- $verb = $request->getAttribute('verb');
+ $verb = $request->getAttribute(name: 'verb');
$middleware = __NAMESPACE__ . '\\' . $verb;
- if (is_a($middleware, Middleware::class, true)) {
- $this->requestHandler->queue->enqueue(new $middleware());
+ if (is_a(object_or_class: $middleware, class: Middleware::class, allow_string: true)) {
+ $this->requestHandler->queue->enqueue(value: new $middleware());
}
}
- $this->requestHandler->queue->enqueue($errorHandler);
+ $this->requestHandler->queue->enqueue(value: $errorHandler);
return $request;
}
@@ -110,7 +113,7 @@ protected function processResponse(ResponseInterface $response): ResponseInterfa
// TODO: Add support for content compression
// https://openarchives.org/OAI/openarchivesprotocol.html#ResponseCompression
// https://github.com/middlewares/encoder
- return $response->withHeader('Content-Type', 'text/xml');
+ return $response->withHeader(name: 'Content-Type', value: 'text/xml');
}
/**
@@ -124,62 +127,166 @@ protected function processResponse(ResponseInterface $response): ResponseInterfa
*/
protected function validateArguments(array $arguments): bool
{
- $errorHandler = ErrorHandler::getInstance();
if (
count(array_diff(array_keys($arguments), self::OAI_PARAMS)) !== 0
or !isset($arguments['verb'])
) {
- $errorHandler->withError('badArgument');
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
+ } else {
+ match ($arguments['verb']) {
+ 'GetRecord' => $this->validateGetRecord(arguments: $arguments),
+ 'Identify' => $this->validateIdentify(arguments: $arguments),
+ 'ListIdentifiers', 'ListRecords' => $this->validateListRecords(arguments: $arguments),
+ 'ListMetadataFormats' => $this->validateListFormats(arguments: $arguments),
+ 'ListSets' => $this->validateListSets(arguments: $arguments),
+ default => ErrorHandler::getInstance()->withError(errorCode: 'badVerb')
+ };
+ if (!ErrorHandler::getInstance()->hasErrors()) {
+ $this->validateMetadataPrefix(prefix: $arguments['metadataPrefix'] ?? null);
+ $this->validateDateTime(datetime: $arguments['from'] ?? null);
+ $this->validateDateTime(datetime: $arguments['until'] ?? null);
+ $this->validateSet($arguments['set'] ?? null);
+ }
+ }
+ return !ErrorHandler::getInstance()->hasErrors();
+ }
+
+ /**
+ * Validate "from" and "until" argument.
+ *
+ * @param ?string $datetime The datetime string to validate or NULL if none
+ *
+ * @return void
+ */
+ protected function validateDateTime(?string $datetime): void
+ {
+ if (isset($datetime)) {
+ $date = date_parse(datetime: $datetime);
+ if ($date['warning_count'] > 0 || $date['error_count'] > 0) {
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
+ }
+ }
+ }
+
+ /**
+ * Validate request arguments for verb GetRecord.
+ *
+ * @param string[] $arguments The request parameters
+ *
+ * @return void
+ */
+ protected function validateGetRecord(array $arguments): void
+ {
+ if (
+ count($arguments) !== 3
+ or !isset($arguments['identifier'])
+ or !isset($arguments['metadataPrefix'])
+ ) {
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
+ }
+ }
+
+ /**
+ * Validate request arguments for verb Identify.
+ *
+ * @param string[] $arguments The request parameters
+ *
+ * @return void
+ */
+ protected function validateIdentify(array $arguments): void
+ {
+ if (count($arguments) !== 1) {
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
+ }
+ }
+
+ /**
+ * Validate request arguments for verb ListMetadataFormats.
+ *
+ * @param string[] $arguments The request parameters
+ *
+ * @return void
+ */
+ protected function validateListFormats(array $arguments): void
+ {
+ if (count($arguments) !== 1) {
+ if (!isset($arguments['identifier']) || count($arguments) !== 2) {
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
+ }
+ }
+ }
+
+ /**
+ * Validate request arguments for verbs ListIdentifiers and ListRecords.
+ *
+ * @param string[] $arguments The request parameters
+ *
+ * @return void
+ */
+ protected function validateListRecords(array $arguments): void
+ {
+ if (
+ isset($arguments['metadataPrefix'])
+ xor isset($arguments['resumptionToken'])
+ ) {
+ if (
+ (isset($arguments['resumptionToken']) && count($arguments) !== 2)
+ or isset($arguments['identifier'])
+ ) {
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
+ }
} else {
- switch ($arguments['verb']) {
- case 'GetRecord':
- if (
- count($arguments) !== 3
- or !isset($arguments['identifier'])
- or !isset($arguments['metadataPrefix'])
- ) {
- $errorHandler->withError('badArgument');
- }
- break;
- case 'Identify':
- if (count($arguments) !== 1) {
- $errorHandler->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->withError('badArgument');
- }
- } else {
- $errorHandler->withError('badArgument');
- }
- break;
- case 'ListMetadataFormats':
- if (count($arguments) !== 1) {
- if (!isset($arguments['identifier']) || count($arguments) !== 2) {
- $errorHandler->withError('badArgument');
- }
- }
- break;
- case 'ListSets':
- if (count($arguments) !== 1) {
- if (!isset($arguments['resumptionToken']) || count($arguments) !== 2) {
- $errorHandler->withError('badArgument');
- }
- }
- break;
- default:
- $errorHandler->withError('badVerb');
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
+ }
+ }
+
+ /**
+ * Validate request arguments for verb ListSets.
+ *
+ * @param string[] $arguments The request parameters
+ *
+ * @return void
+ */
+ protected function validateListSets(array $arguments): void
+ {
+ if (count($arguments) !== 1) {
+ if (!isset($arguments['resumptionToken']) || count($arguments) !== 2) {
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
+ }
+ }
+ }
+
+ /**
+ * Validate "metadataPrefix" argument.
+ *
+ * @param ?string $prefix The metadata prefix
+ *
+ * @return void
+ */
+ protected function validateMetadataPrefix(?string $prefix): void
+ {
+ if (isset($prefix)) {
+ $formats = EntityManager::getInstance()->getMetadataFormats();
+ if (!$formats->containsKey(key: $prefix)) {
+ ErrorHandler::getInstance()->withError(errorCode: 'cannotDisseminateFormat');
+ }
+ }
+ }
+
+ /**
+ * Validate "set" argument.
+ *
+ * @param ?string $spec The set spec
+ *
+ * @return void
+ */
+ protected function validateSet(?string $spec): void
+ {
+ if (isset($spec)) {
+ $sets = EntityManager::getInstance()->getSets();
+ if (!$sets->containsKey(key: $spec)) {
+ ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
}
}
- return !$errorHandler->hasErrors();
}
}
diff --git a/src/Middleware/ErrorHandler.php b/src/Middleware/ErrorHandler.php
index 129ebf5..76cf08e 100644
--- a/src/Middleware/ErrorHandler.php
+++ b/src/Middleware/ErrorHandler.php
@@ -25,7 +25,7 @@
use DomainException;
use GuzzleHttp\Psr7\Utils;
use OCC\Basics\Traits\Singleton;
-use OCC\OaiPmh2\Document;
+use OCC\OaiPmh2\Response;
use OCC\PSR15\AbstractMiddleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
@@ -48,7 +48,7 @@ class ErrorHandler extends AbstractMiddleware
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.',
+ 'badVerb' => 'The 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.',
@@ -70,12 +70,19 @@ class ErrorHandler extends AbstractMiddleware
*/
protected function getResponseBody(): StreamInterface
{
- $document = new Document($this->requestHandler->request);
+ $response = new Response(serverRequest: $this->requestHandler->request);
foreach (array_unique($this->errors) as $errorCode) {
- $error = $document->createElement('error', self::OAI_ERRORS[$errorCode], true);
- $error->setAttribute('code', $errorCode);
+ $error = $response->createElement(
+ localName: 'error',
+ value: self::OAI_ERRORS[$errorCode],
+ appendToRoot: true
+ );
+ $error->setAttribute(
+ qualifiedName: 'code',
+ value: $errorCode
+ );
}
- return Utils::streamFor((string) $document);
+ return Utils::streamFor(resource: (string) $response);
}
/**
@@ -98,7 +105,7 @@ public function hasErrors(): bool
protected function processResponse(ResponseInterface $response): ResponseInterface
{
if ($this->hasErrors()) {
- $response = $response->withBody($this->getResponseBody());
+ $response = $response->withBody(body: $this->getResponseBody());
}
return $response;
}
@@ -118,11 +125,11 @@ public function withError(string $errorCode): ErrorHandler
$this->errors[] = $errorCode;
} else {
throw new DomainException(
- sprintf(
- 'Valid OAI-PMH error code expected, "%s" given.',
- $errorCode
+ message: sprintf(
+ format: 'Valid OAI-PMH error code expected, "%s" given.',
+ values: $errorCode
),
- 500
+ code: 500
);
}
return $this;
diff --git a/src/Middleware/GetRecord.php b/src/Middleware/GetRecord.php
index 04f841d..c0d6046 100644
--- a/src/Middleware/GetRecord.php
+++ b/src/Middleware/GetRecord.php
@@ -22,14 +22,13 @@
namespace OCC\OaiPmh2\Middleware;
-use OCC\OaiPmh2\Database;
-use OCC\OaiPmh2\Document;
-use OCC\OaiPmh2\Entity\Format;
use OCC\OaiPmh2\Middleware;
+use OCC\OaiPmh2\Response;
use Psr\Http\Message\ServerRequestInterface;
/**
* Process the "GetRecord" request.
+ *
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#GetRecord
*
* @author Sebastian Meyer
@@ -46,54 +45,67 @@ class GetRecord extends Middleware
*/
protected function prepareResponse(ServerRequestInterface $request): void
{
- /** @var array */
- $params = $request->getAttributes();
- /** @var Format */
- $format = Database::getInstance()->getEntityManager()->getReference(Format::class, $params['metadataPrefix']);
- $oaiRecord = Database::getInstance()->getRecord($params['identifier'], $format);
+ $oaiRecord = $this->em->getRecord(
+ identifier: (string) $this->arguments['identifier'],
+ format: (string) $this->arguments['metadataPrefix']
+ );
if (!isset($oaiRecord)) {
- if (Database::getInstance()->idDoesExist($params['identifier'])) {
- ErrorHandler::getInstance()->withError('cannotDisseminateFormat');
+ if ($this->em->isValidRecordIdentifier(identifier: (string) $this->arguments['identifier'])) {
+ ErrorHandler::getInstance()->withError(errorCode: 'cannotDisseminateFormat');
} else {
- ErrorHandler::getInstance()->withError('idDoesNotExist');
+ ErrorHandler::getInstance()->withError(errorCode: 'idDoesNotExist');
}
return;
- } else {
- $oaiRecordContent = $oaiRecord->getContent();
}
- $document = new Document($request);
- $getRecord = $document->createElement('GetRecord', '', true);
+ $response = new Response(serverRequest: $request);
+ $getRecord = $response->createElement(
+ localName: 'GetRecord',
+ value: '',
+ appendToRoot: true
+ );
- $record = $document->createElement('record');
- $getRecord->appendChild($record);
+ $record = $response->createElement(localName: 'record');
+ $getRecord->appendChild(node: $record);
- $header = $document->createElement('header');
- if (!isset($oaiRecordContent)) {
- $header->setAttribute('status', 'deleted');
+ $header = $response->createElement(localName: 'header');
+ if (!$oaiRecord->hasContent()) {
+ $header->setAttribute(
+ qualifiedName: 'status',
+ value: 'deleted'
+ );
}
- $record->appendChild($header);
+ $record->appendChild(node: $header);
- $identifier = $document->createElement('identifier', $oaiRecord->getIdentifier());
- $header->appendChild($identifier);
+ $identifier = $response->createElement(
+ localName: 'identifier',
+ value: $oaiRecord->getIdentifier()
+ );
+ $header->appendChild(node: $identifier);
- $datestamp = $document->createElement('datestamp', $oaiRecord->getLastChanged()->format('Y-m-d\TH:i:s\Z'));
- $header->appendChild($datestamp);
+ $datestamp = $response->createElement(
+ localName: 'datestamp',
+ value: $oaiRecord->getLastChanged()->format(format: 'Y-m-d\TH:i:s\Z')
+ );
+ $header->appendChild(node: $datestamp);
foreach ($oaiRecord->getSets() as $set) {
- $setSpec = $document->createElement('setSpec', $set->getName());
- $header->appendChild($setSpec);
+ $setSpec = $response->createElement(
+ localName: 'setSpec',
+ value: $set->getName()
+ );
+ $header->appendChild(node: $setSpec);
}
- if (isset($oaiRecordContent)) {
- $metadata = $document->createElement('metadata');
- $record->appendChild($metadata);
+ if ($oaiRecord->hasContent()) {
+ $metadata = $response->createElement(localName: 'metadata');
+ $record->appendChild(node: $metadata);
- $data = $document->importData($oaiRecordContent);
- $metadata->appendChild($data);
+ $data = $response->importData(data: $oaiRecord->getContent());
+ $metadata->appendChild(node: $data);
}
- $this->preparedResponse = $document;
+ $this->preparedResponse = $response;
}
}
diff --git a/src/Middleware/Identify.php b/src/Middleware/Identify.php
index 2cff920..14dca53 100644
--- a/src/Middleware/Identify.php
+++ b/src/Middleware/Identify.php
@@ -24,13 +24,13 @@
use GuzzleHttp\Psr7\Uri;
use OCC\OaiPmh2\Configuration;
-use OCC\OaiPmh2\Database;
-use OCC\OaiPmh2\Document;
use OCC\OaiPmh2\Middleware;
+use OCC\OaiPmh2\Response;
use Psr\Http\Message\ServerRequestInterface;
/**
* Process the "Identify" request.
+ *
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#Identify
*
* @author Sebastian Meyer
@@ -47,47 +47,75 @@ class Identify extends Middleware
*/
protected function prepareResponse(ServerRequestInterface $request): void
{
- $document = new Document($request);
- $identify = $document->createElement('Identify', '', true);
+ $response = new Response(serverRequest: $request);
+ $identify = $response->createElement(
+ localName: 'Identify',
+ value: '',
+ appendToRoot: true
+ );
- $name = Configuration::getInstance()->repositoryName;
- $repositoryName = $document->createElement('repositoryName', $name);
- $identify->appendChild($repositoryName);
+ $repositoryName = $response->createElement(
+ localName: 'repositoryName',
+ value: Configuration::getInstance()->repositoryName
+ );
+ $identify->appendChild(node: $repositoryName);
$uri = Uri::composeComponents(
- $request->getUri()->getScheme(),
- $request->getUri()->getAuthority(),
- $request->getUri()->getPath(),
- null,
- null
+ scheme: $request->getUri()->getScheme(),
+ authority: $request->getUri()->getAuthority(),
+ path: $request->getUri()->getPath(),
+ query: null,
+ fragment: null
);
- $baseURL = $document->createElement('baseURL', $uri);
- $identify->appendChild($baseURL);
+ $baseURL = $response->createElement(
+ localName: 'baseURL',
+ value: $uri
+ );
+ $identify->appendChild(node: $baseURL);
- $protocolVersion = $document->createElement('protocolVersion', '2.0');
- $identify->appendChild($protocolVersion);
+ $protocolVersion = $response->createElement(
+ localName: 'protocolVersion',
+ value: '2.0'
+ );
+ $identify->appendChild(node: $protocolVersion);
- $email = Configuration::getInstance()->adminEmail;
- $adminEmail = $document->createElement('adminEmail', $email);
- $identify->appendChild($adminEmail);
+ $adminEmail = $response->createElement(
+ localName: 'adminEmail',
+ value: Configuration::getInstance()->adminEmail
+ );
+ $identify->appendChild(node: $adminEmail);
- $datestamp = Database::getInstance()->getEarliestDatestamp();
- $earliestDatestamp = $document->createElement('earliestDatestamp', $datestamp);
- $identify->appendChild($earliestDatestamp);
+ $earliestDatestamp = $response->createElement(
+ localName: 'earliestDatestamp',
+ value: $this->em->getEarliestDatestamp()
+ );
+ $identify->appendChild(node: $earliestDatestamp);
- $deletedRecord = $document->createElement('deletedRecord', 'transient');
- $identify->appendChild($deletedRecord);
+ $deletedRecord = $response->createElement(
+ localName: 'deletedRecord',
+ value: Configuration::getInstance()->deletedRecords
+ );
+ $identify->appendChild(node: $deletedRecord);
- $granularity = $document->createElement('granularity', 'YYYY-MM-DDThh:mm:ssZ');
- $identify->appendChild($granularity);
+ $granularity = $response->createElement(
+ localName: 'granularity',
+ value: 'YYYY-MM-DDThh:mm:ssZ'
+ );
+ $identify->appendChild(node: $granularity);
// TODO: Implement explicit content compression support.
- // $compressionDeflate = $document->createElement('compression', 'deflate');
- // $identify->appendChild($compressionDeflate);
+ // $compressionDeflate = $response->createElement(
+ // localName: 'compression',
+ // value: 'deflate'
+ // );
+ // $identify->appendChild(node: $compressionDeflate);
- // $compressionGzip = $document->createElement('compression', 'gzip');
- // $identify->appendChild($compressionGzip);
+ // $compressionGzip = $response->createElement(
+ // localName: 'compression',
+ // value: 'gzip'
+ // );
+ // $identify->appendChild(node: $compressionGzip);
- $this->preparedResponse = $document;
+ $this->preparedResponse = $response;
}
}
diff --git a/src/Middleware/ListIdentifiers.php b/src/Middleware/ListIdentifiers.php
index 93ebdad..66aed60 100644
--- a/src/Middleware/ListIdentifiers.php
+++ b/src/Middleware/ListIdentifiers.php
@@ -22,16 +22,13 @@
namespace OCC\OaiPmh2\Middleware;
-use DateTime;
-use OCC\OaiPmh2\Configuration;
-use OCC\OaiPmh2\Database;
-use OCC\OaiPmh2\Document;
-use OCC\OaiPmh2\Entity\Record;
use OCC\OaiPmh2\Middleware;
+use OCC\OaiPmh2\Response;
use Psr\Http\Message\ServerRequestInterface;
/**
* Process the "ListIdentifiers" request.
+ *
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListIdentifiers
*
* @author Sebastian Meyer
@@ -40,7 +37,7 @@
class ListIdentifiers extends Middleware
{
/**
- * Prepare the response body for verb "ListIdentifiers" and "ListRecords".
+ * Prepare the response body for verbs "ListIdentifiers" and "ListRecords".
*
* @param ServerRequestInterface $request The incoming request
*
@@ -48,127 +45,78 @@ class ListIdentifiers extends Middleware
*/
protected function prepareResponse(ServerRequestInterface $request): void
{
- $counter = 0;
- $completeListSize = 0;
- $maxRecords = Configuration::getInstance()->maxRecords;
-
- /** @var array */
- $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()->getQueryResult();
- if (!array_key_exists($metadataPrefix, $prefixes)) {
- ErrorHandler::getInstance()->withError('cannotDisseminateFormat');
- return;
- }
- if (isset($from)) {
- $from = new DateTime($from);
- }
- if (isset($until)) {
- $until = new DateTime($until);
- }
- if (isset($set)) {
- $sets = Database::getInstance()->getSets()->getQueryResult();
- if (!array_key_exists($set, $sets)) {
- ErrorHandler::getInstance()->withError('noSetHierarchy');
- return;
- }
- $set = $sets[$set];
- }
-
- $records = Database::getInstance()->getRecords(
- $verb,
- $prefixes[$metadataPrefix],
- $counter,
- $from,
- $until,
- $set
+ $this->checkResumptionToken();
+
+ $records = $this->em->getRecords(
+ verb: $this->arguments['verb'],
+ metadataPrefix: (string) $this->arguments['metadataPrefix'],
+ counter: $this->arguments['counter'],
+ from: $this->arguments['from'],
+ until: $this->arguments['until'],
+ set: $this->arguments['set']
);
- $newToken = $records->getResumptionToken();
if (count($records) === 0) {
- ErrorHandler::getInstance()->withError('noRecordsMatch');
+ ErrorHandler::getInstance()->withError(errorCode: 'noRecordsMatch');
return;
- } elseif (isset($newToken)) {
- $completeListSize = $newToken->getParameters()['completeListSize'];
}
- $document = new Document($request);
- $list = $document->createElement($verb, '', true);
+ $response = new Response(serverRequest: $request);
+ $list = $response->createElement(
+ localName: $this->arguments['verb'],
+ value: '',
+ appendToRoot: true
+ );
+ $baseNode = $list;
- /** @var Record $oaiRecord */
foreach ($records as $oaiRecord) {
- if ($verb === 'ListIdentifiers') {
- $baseNode = $list;
- } else {
- $record = $document->createElement('record');
- $list->appendChild($record);
+ if ($this->arguments['verb'] === 'ListRecords') {
+ $record = $response->createElement(localName: 'record');
+ $list->appendChild(node: $record);
$baseNode = $record;
}
- $header = $document->createElement('header');
- if (!$oaiRecord->hasContent()) {
- $header->setAttribute('status', 'deleted');
- }
- $baseNode->appendChild($header);
+ $header = $response->createElement(localName: 'header');
+ $baseNode->appendChild(node: $header);
- $identifier = $document->createElement('identifier', $oaiRecord->getIdentifier());
- $header->appendChild($identifier);
+ $identifier = $response->createElement(
+ localName: 'identifier',
+ value: $oaiRecord->getIdentifier()
+ );
+ $header->appendChild(node: $identifier);
- $datestamp = $document->createElement('datestamp', $oaiRecord->getLastChanged()->format('Y-m-d\TH:i:s\Z'));
- $header->appendChild($datestamp);
+ $datestamp = $response->createElement(
+ localName: 'datestamp',
+ value: $oaiRecord->getLastChanged()->format(format: 'Y-m-d\TH:i:s\Z')
+ );
+ $header->appendChild(node: $datestamp);
foreach ($oaiRecord->getSets() as $oaiSet) {
- $setSpec = $document->createElement('setSpec', $oaiSet->getName());
- $header->appendChild($setSpec);
+ $setSpec = $response->createElement(
+ localName: 'setSpec',
+ value: $oaiSet->getName()
+ );
+ $header->appendChild(node: $setSpec);
}
- if ($verb === 'ListRecords' && $oaiRecord->hasContent()) {
- $metadata = $document->createElement('metadata');
- $baseNode->appendChild($metadata);
+ if (!$oaiRecord->hasContent()) {
+ $header->setAttribute(
+ qualifiedName: 'status',
+ value: 'deleted'
+ );
+ } elseif ($this->arguments['verb'] === 'ListRecords') {
+ $metadata = $response->createElement(localName: 'metadata');
+ $baseNode->appendChild(node: $metadata);
- /** @var string */
- $content = $oaiRecord->getContent();
- $data = $document->importData($content);
- $metadata->appendChild($data);
+ $data = $response->importData(data: $oaiRecord->getContent());
+ $metadata->appendChild(node: $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 = $response;
- $this->preparedResponse = $document;
+ $this->addResumptionToken(
+ node: $list,
+ token: $records->getResumptionToken() ?? null
+ );
}
}
diff --git a/src/Middleware/ListMetadataFormats.php b/src/Middleware/ListMetadataFormats.php
index 091dbb4..479f6c5 100644
--- a/src/Middleware/ListMetadataFormats.php
+++ b/src/Middleware/ListMetadataFormats.php
@@ -22,14 +22,13 @@
namespace OCC\OaiPmh2\Middleware;
-use OCC\OaiPmh2\Database;
-use OCC\OaiPmh2\Document;
-use OCC\OaiPmh2\Entity\Format;
use OCC\OaiPmh2\Middleware;
+use OCC\OaiPmh2\Response;
use Psr\Http\Message\ServerRequestInterface;
/**
* Process the "ListMetadataFormats" request.
+ *
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListMetadataFormats
*
* @author Sebastian Meyer
@@ -46,37 +45,50 @@ class ListMetadataFormats extends Middleware
*/
protected function prepareResponse(ServerRequestInterface $request): void
{
- /** @var ?string */
- $identifier = $request->getAttribute('identifier');
- $formats = Database::getInstance()->getMetadataFormats($identifier);
+ $formats = $this->em->getMetadataFormats(recordIdentifier: $this->arguments['identifier']);
if (count($formats) === 0) {
- if (!isset($identifier) || Database::getInstance()->idDoesExist($identifier)) {
- ErrorHandler::getInstance()->withError('noMetadataFormats');
+ if (
+ !isset($this->arguments['identifier'])
+ || $this->em->isValidRecordIdentifier(identifier: $this->arguments['identifier'])
+ ) {
+ ErrorHandler::getInstance()->withError(errorCode: 'noMetadataFormats');
} else {
- ErrorHandler::getInstance()->withError('idDoesNotExist');
+ ErrorHandler::getInstance()->withError(errorCode: 'idDoesNotExist');
}
return;
}
- $document = new Document($request);
- $listMetadataFormats = $document->createElement('ListMetadataFormats', '', true);
+ $response = new Response(serverRequest: $request);
+ $listMetadataFormats = $response->createElement(
+ localName: 'ListMetadataFormats',
+ value: '',
+ appendToRoot: true
+ );
- /** @var Format $oaiFormat */
foreach ($formats as $oaiFormat) {
- $metadataFormat = $document->createElement('metadataFormat');
- $listMetadataFormats->appendChild($metadataFormat);
+ $metadataFormat = $response->createElement(localName: 'metadataFormat');
+ $listMetadataFormats->appendChild(node: $metadataFormat);
- $metadataPrefix = $document->createElement('metadataPrefix', $oaiFormat->getPrefix());
- $metadataFormat->appendChild($metadataPrefix);
+ $metadataPrefix = $response->createElement(
+ localName: 'metadataPrefix',
+ value: $oaiFormat->getPrefix()
+ );
+ $metadataFormat->appendChild(node: $metadataPrefix);
- $schema = $document->createElement('schema', $oaiFormat->getSchema());
- $metadataFormat->appendChild($schema);
+ $schema = $response->createElement(
+ localName: 'schema',
+ value: $oaiFormat->getSchema()
+ );
+ $metadataFormat->appendChild(node: $schema);
- $metadataNamespace = $document->createElement('metadataNamespace', $oaiFormat->getNamespace());
- $metadataFormat->appendChild($metadataNamespace);
+ $metadataNamespace = $response->createElement(
+ localName: 'metadataNamespace',
+ value: $oaiFormat->getNamespace()
+ );
+ $metadataFormat->appendChild(node: $metadataNamespace);
}
- $this->preparedResponse = $document;
+ $this->preparedResponse = $response;
}
}
diff --git a/src/Middleware/ListRecords.php b/src/Middleware/ListRecords.php
index 6cd01ec..1e601f2 100644
--- a/src/Middleware/ListRecords.php
+++ b/src/Middleware/ListRecords.php
@@ -24,6 +24,7 @@
/**
* Process the "ListRecords" request.
+ *
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListRecords
*
* @author Sebastian Meyer
@@ -34,6 +35,6 @@ 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.
+ * the records' data. Hence this is just a class alias.
*/
}
diff --git a/src/Middleware/ListSets.php b/src/Middleware/ListSets.php
index 0d2ba2f..2f40a2a 100644
--- a/src/Middleware/ListSets.php
+++ b/src/Middleware/ListSets.php
@@ -22,21 +22,17 @@
namespace OCC\OaiPmh2\Middleware;
-use OCC\OaiPmh2\Configuration;
-use OCC\OaiPmh2\Database;
-use OCC\OaiPmh2\Document;
-use OCC\OaiPmh2\Entity\Set;
use OCC\OaiPmh2\Middleware;
+use OCC\OaiPmh2\Response;
use Psr\Http\Message\ServerRequestInterface;
/**
* Process the "ListSets" request.
+ *
* @see https://openarchives.org/OAI/openarchivesprotocol.html#ListSets
*
* @author Sebastian Meyer
* @package OAIPMH2
- *
- * @template Sets of array
*/
class ListSets extends Middleware
{
@@ -49,78 +45,52 @@ class ListSets extends Middleware
*/
protected function prepareResponse(ServerRequestInterface $request): void
{
- $counter = 0;
- $completeListSize = 0;
- $maxRecords = Configuration::getInstance()->maxRecords;
+ $this->checkResumptionToken();
- /** @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 = $this->em->getSets(counter: $this->arguments['counter']);
- $sets = Database::getInstance()->getSets($counter);
- $newToken = $sets->getResumptionToken();
if (count($sets) === 0) {
- ErrorHandler::getInstance()->withError('noSetHierarchy');
+ ErrorHandler::getInstance()->withError(errorCode: 'noSetHierarchy');
return;
- } elseif (isset($newToken)) {
- $completeListSize = $newToken->getParameters()['completeListSize'];
}
- $document = new Document($request);
- $list = $document->createElement('ListSets', '', true);
+ $response = new Response(serverRequest: $request);
+ $list = $response->createElement(
+ localName: 'ListSets',
+ value: '',
+ appendToRoot: true
+ );
- /** @var Set $oaiSet */
foreach ($sets as $oaiSet) {
- $set = $document->createElement('set');
- $list->appendChild($set);
+ $set = $response->createElement(localName: 'set');
+ $list->appendChild(node: $set);
- $setSpec = $document->createElement('setSpec', $oaiSet->getSpec());
- $set->appendChild($setSpec);
+ $setSpec = $response->createElement(
+ localName: 'setSpec',
+ value: $oaiSet->getSpec()
+ );
+ $set->appendChild(node: $setSpec);
- $setName = $document->createElement('setName', $oaiSet->getName());
- $set->appendChild($setName);
+ $setName = $response->createElement(
+ localName: 'setName',
+ value: $oaiSet->getName()
+ );
+ $set->appendChild(node: $setName);
if ($oaiSet->hasDescription()) {
- $setDescription = $document->createElement('setDescription');
- $set->appendChild($setDescription);
+ $setDescription = $response->createElement(localName: 'setDescription');
+ $set->appendChild(node: $setDescription);
- /** @var string */
- $description = $oaiSet->getDescription();
- $data = $document->importData($description);
- $setDescription->appendChild($data);
+ $data = $response->importData(data: $oaiSet->getDescription());
+ $setDescription->appendChild(node: $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 = $response;
- $this->preparedResponse = $document;
+ $this->addResumptionToken(
+ node: $list,
+ token: $sets->getResumptionToken() ?? null
+ );
}
}
diff --git a/src/Repository/FormatRepository.php b/src/Repository/FormatRepository.php
new file mode 100644
index 0000000..7b96dff
--- /dev/null
+++ b/src/Repository/FormatRepository.php
@@ -0,0 +1,72 @@
+
+ *
+ * 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\Repository;
+
+use Doctrine\ORM\EntityRepository;
+use OCC\OaiPmh2\Entity\Format;
+use OCC\OaiPmh2\EntityManager;
+
+/**
+ * Doctrine/ORM Repository for formats.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ *
+ * @extends EntityRepository
+ */
+final class FormatRepository extends EntityRepository
+{
+ /**
+ * Add or update metadata format.
+ *
+ * @param Format $entity The metadata format
+ *
+ * @return void
+ */
+ public function addOrUpdate(Format $entity): void
+ {
+ $oldFormat = $this->find(id: $entity->getPrefix());
+ if (isset($oldFormat)) {
+ $oldFormat->setNamespace(namespace: $entity->getNamespace());
+ $oldFormat->setSchema(schema: $entity->getSchema());
+ } else {
+ $this->getEntityManager()->persist(object: $entity);
+ }
+ }
+
+ /**
+ * Delete metadata format and all associated records.
+ *
+ * @param Format $entity The metadata format
+ *
+ * @return void
+ */
+ public function delete(Format $entity): void
+ {
+ /** @var EntityManager */
+ $entityManager = $this->getEntityManager();
+ $entityManager->remove(object: $entity);
+ $entityManager->flush();
+ $entityManager->pruneOrphanedSets();
+ }
+}
diff --git a/src/Repository/RecordRepository.php b/src/Repository/RecordRepository.php
new file mode 100644
index 0000000..c2ecc6f
--- /dev/null
+++ b/src/Repository/RecordRepository.php
@@ -0,0 +1,103 @@
+
+ *
+ * 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\Repository;
+
+use DateTime;
+use Doctrine\ORM\EntityRepository;
+use OCC\OaiPmh2\Configuration;
+use OCC\OaiPmh2\Entity\Record;
+use OCC\OaiPmh2\EntityManager;
+
+/**
+ * Doctrine/ORM Repository for records.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ *
+ * @extends EntityRepository
+ */
+final class RecordRepository extends EntityRepository
+{
+ /**
+ * Add or update record.
+ *
+ * @param Record $entity The record
+ *
+ * @return void
+ */
+ public function addOrUpdate(Record $entity): void
+ {
+ /** @var EntityManager */
+ $entityManager = $this->getEntityManager();
+ $oldRecord = $this->find(
+ id: [
+ 'identifier' => $entity->getIdentifier(),
+ 'format' => $entity->getFormat()
+ ]
+ );
+ if (isset($oldRecord)) {
+ if ($entity->hasContent() || Configuration::getInstance()->deletedRecords !== 'no') {
+ $oldRecord->setContent(data: $entity->getContent(), validate: false);
+ $oldRecord->setLastChanged(dateTime: $entity->getLastChanged());
+ $newSets = $entity->getSets()->toArray();
+ $oldSets = $oldRecord->getSets()->toArray();
+ // Add new sets.
+ foreach (array_diff(array: $newSets, arrays: $oldSets) as $newSet) {
+ $oldRecord->addSet(set: $newSet);
+ }
+ // Remove old sets.
+ foreach (array_diff(array: $oldSets, arrays: $newSets) as $oldSet) {
+ $oldRecord->removeSet(set: $oldSet);
+ }
+ } else {
+ $entityManager->remove(object: $oldRecord);
+ }
+ } else {
+ if ($entity->hasContent() || Configuration::getInstance()->deletedRecords !== 'no') {
+ $entityManager->persist(object: $entity);
+ }
+ }
+ }
+
+ /**
+ * Delete a record.
+ *
+ * @param Record $entity The record
+ *
+ * @return void
+ */
+ public function delete(Record $entity): void
+ {
+ /** @var EntityManager */
+ $entityManager = $this->getEntityManager();
+ if (Configuration::getInstance()->deletedRecords === 'no') {
+ $entityManager->remove(object: $entity);
+ $entityManager->flush();
+ $entityManager->pruneOrphanedSets();
+ } else {
+ $entity->setContent();
+ $entity->setLastChanged(dateTime: new DateTime());
+ $entityManager->flush();
+ }
+ }
+}
diff --git a/src/Repository/SetRepository.php b/src/Repository/SetRepository.php
new file mode 100644
index 0000000..be74432
--- /dev/null
+++ b/src/Repository/SetRepository.php
@@ -0,0 +1,71 @@
+
+ *
+ * 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\Repository;
+
+use Doctrine\ORM\EntityRepository;
+use OCC\OaiPmh2\Configuration;
+use OCC\OaiPmh2\Entity\Set;
+use OCC\OaiPmh2\ResultSet;
+
+/**
+ * Doctrine/ORM Repository for sets.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ *
+ * @extends EntityRepository
+ */
+final class SetRepository extends EntityRepository
+{
+ /**
+ * Add or update set.
+ *
+ * @param Set $entity The set
+ *
+ * @return void
+ */
+ public function addOrUpdate(Set $entity): void
+ {
+ $oldSet = $this->find(id: $entity->getSpec());
+ if (isset($oldSet)) {
+ $oldSet->setName(name: $entity->getName());
+ $oldSet->setDescription(description: $entity->getDescription());
+ } else {
+ $this->getEntityManager()->persist(object: $entity);
+ }
+ }
+
+ /**
+ * Delete set.
+ *
+ * @param Set $entity The set
+ *
+ * @return void
+ */
+ public function delete(Set $entity): void
+ {
+ $entityManager = $this->getEntityManager();
+ $entityManager->remove(object: $entity);
+ $entityManager->flush();
+ }
+}
diff --git a/src/Repository/TokenRepository.php b/src/Repository/TokenRepository.php
new file mode 100644
index 0000000..129d255
--- /dev/null
+++ b/src/Repository/TokenRepository.php
@@ -0,0 +1,64 @@
+
+ *
+ * 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\Repository;
+
+use DateTime;
+use Doctrine\ORM\EntityRepository;
+use OCC\OaiPmh2\Entity\Token;
+
+/**
+ * Doctrine/ORM Repository for resumption tokens.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ *
+ * @extends EntityRepository
+ */
+final class TokenRepository extends EntityRepository
+{
+ /**
+ * Add resumption token.
+ *
+ * @param Token $entity The resumption token
+ *
+ * @return void
+ */
+ public function addOrUpdate(Token $entity): void
+ {
+ $this->getEntityManager()->persist(object: $entity);
+ }
+
+ /**
+ * Delete resumption token.
+ *
+ * @param Token $entity The resumption token
+ *
+ * @return void
+ */
+ public function delete(Token $entity): void
+ {
+ $entityManager = $this->getEntityManager();
+ $entityManager->remove(object: $entity);
+ $entityManager->flush();
+ }
+}
diff --git a/src/Document.php b/src/Response.php
similarity index 62%
rename from src/Document.php
rename to src/Response.php
index d504207..ac4e68f 100644
--- a/src/Document.php
+++ b/src/Response.php
@@ -35,22 +35,17 @@
* @author Sebastian Meyer
* @package OAIPMH2
*/
-class Document
+final class Response
{
/**
* This holds the DOMDocument of the OAI-PMH XML response.
*/
- protected DOMDocument $dom;
+ private DOMDocument $dom;
/**
* This holds the root node of the OAI-PMH XML response.
*/
- protected DOMElement $rootNode;
-
- /**
- * This holds the current server request.
- */
- protected ServerRequestInterface $serverRequest;
+ private DOMElement $rootNode;
/**
* Add XSL processing instructions to XML response document.
@@ -61,24 +56,24 @@ protected function addProcessingInstructions(): void
{
$uri = $this->serverRequest->getUri();
$basePath = $uri->getPath();
- if (str_ends_with($basePath, 'index.php')) {
- $basePath = pathinfo($basePath, PATHINFO_DIRNAME);
+ if (str_ends_with(haystack: $basePath, needle: 'index.php')) {
+ $basePath = pathinfo(path: $basePath, flags: PATHINFO_DIRNAME);
}
$stylesheet = Uri::composeComponents(
- $uri->getScheme(),
- $uri->getAuthority(),
- rtrim($basePath, '/') . '/resources/stylesheet.xsl',
- null,
- null
+ scheme: $uri->getScheme(),
+ authority: $uri->getAuthority(),
+ path: rtrim(string: $basePath, characters: '/') . '/resources/stylesheet.xsl',
+ query: null,
+ fragment: null
);
$xslt = $this->dom->createProcessingInstruction(
- 'xml-stylesheet',
- sprintf(
- 'type="text/xsl" href="%s"',
- $stylesheet
+ target: 'xml-stylesheet',
+ data: sprintf(
+ format: 'type="text/xsl" href="%s"',
+ values: $stylesheet
)
);
- $this->dom->appendChild($xslt);
+ $this->dom->appendChild(node: $xslt);
}
/**
@@ -90,20 +85,27 @@ protected function appendRequest(): void
{
$uri = $this->serverRequest->getUri();
$baseUrl = Uri::composeComponents(
- $uri->getScheme(),
- $uri->getAuthority(),
- $uri->getPath(),
- null,
- null
+ scheme: $uri->getScheme(),
+ authority: $uri->getAuthority(),
+ path: $uri->getPath(),
+ query: null,
+ fragment: null
+ );
+ $request = $this->createElement(
+ localName: 'request',
+ value: $baseUrl,
+ appendToRoot: true
);
- $request = $this->dom->createElement('request', $baseUrl);
- $this->rootNode->appendChild($request);
/** @var array */
$params = $this->serverRequest->getAttributes();
foreach ($params as $param => $value) {
$request->setAttribute(
- $param,
- htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8')
+ qualifiedName: $param,
+ value: htmlspecialchars(
+ string: $value,
+ flags: ENT_XML1 | ENT_COMPAT,
+ encoding: 'UTF-8'
+ )
);
}
}
@@ -115,8 +117,11 @@ protected function appendRequest(): void
*/
protected function appendResponseDate(): void
{
- $responseDate = $this->dom->createElement('responseDate', gmdate('Y-m-d\TH:i:s\Z'));
- $this->rootNode->appendChild($responseDate);
+ $this->createElement(
+ localName: 'responseDate',
+ value: gmdate(format: 'Y-m-d\TH:i:s\Z'),
+ appendToRoot: true
+ );
}
/**
@@ -126,20 +131,20 @@ protected function appendResponseDate(): void
*/
protected function appendRootElement(): void
{
- $this->rootNode = $this->dom->createElement('OAI-PMH');
+ $this->rootNode = $this->dom->createElement(localName: 'OAI-PMH');
$this->rootNode->setAttribute(
- 'xmlns',
- 'http://www.openarchives.org/OAI/2.0/'
+ qualifiedName: 'xmlns',
+ value: 'http://www.openarchives.org/OAI/2.0/'
);
$this->rootNode->setAttribute(
- 'xmlns:xsi',
- 'http://www.w3.org/2001/XMLSchema-instance'
+ qualifiedName: 'xmlns:xsi',
+ value: 'http://www.w3.org/2001/XMLSchema-instance'
);
$this->rootNode->setAttribute(
- 'xsi:schemaLocation',
- 'http://www.openarchives.org/OAI/2.0/ https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'
+ qualifiedName: 'xsi:schemaLocation',
+ value: 'http://www.openarchives.org/OAI/2.0/ https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'
);
- $this->dom->appendChild($this->rootNode);
+ $this->dom->appendChild(node: $this->rootNode);
}
/**
@@ -149,7 +154,7 @@ protected function appendRootElement(): void
*/
protected function createDocument(): void
{
- $this->dom = new DOMDocument('1.0', 'UTF-8');
+ $this->dom = new DOMDocument(version: '1.0', encoding: 'UTF-8');
$this->dom->preserveWhiteSpace = false;
$this->addProcessingInstructions();
}
@@ -166,11 +171,15 @@ protected function createDocument(): void
public function createElement(string $localName, string $value = '', bool $appendToRoot = false): DOMElement
{
$node = $this->dom->createElement(
- $localName,
- htmlspecialchars($value, ENT_XML1, 'UTF-8')
+ localName: $localName,
+ value: htmlspecialchars(
+ string: $value,
+ flags: ENT_XML1,
+ encoding: 'UTF-8'
+ )
);
if ($appendToRoot) {
- $this->rootNode->appendChild($node);
+ $this->rootNode->appendChild(node: $node);
}
return $node;
}
@@ -182,21 +191,21 @@ public function createElement(string $localName, string $value = '', bool $appen
*
* @return DOMNode The imported XML node
*
- * @throws DOMException
+ * @throws DOMException if the data cannot be imported
*/
public function importData(string $data): DOMNode
{
- $document = new DOMDocument('1.0', 'UTF-8');
+ $document = new DOMDocument(version: '1.0', encoding: 'UTF-8');
$document->preserveWhiteSpace = false;
- if ($document->loadXML($data) === true) {
+ if ($document->loadXML(source: $data) === true) {
/** @var DOMElement */
$rootNode = $document->documentElement;
- $node = $this->dom->importNode($rootNode, true);
+ $node = $this->dom->importNode(node: $rootNode, deep: true);
return $node;
} else {
throw new DOMException(
- 'Could not import the XML data. Most likely it is not well-formed.',
- 500
+ message: 'Could not import the XML data. Most likely it is not well-formed.',
+ code: 500
);
}
}
@@ -206,9 +215,8 @@ public function importData(string $data): DOMNode
*
* @param ServerRequestInterface $serverRequest The PSR-7 HTTP Server Request
*/
- public function __construct(ServerRequestInterface $serverRequest)
+ public function __construct(private ServerRequestInterface $serverRequest)
{
- $this->serverRequest = $serverRequest;
$this->createDocument();
$this->appendRootElement();
$this->appendResponseDate();
diff --git a/src/Result.php b/src/ResultSet.php
similarity index 63%
rename from src/Result.php
rename to src/ResultSet.php
index f92db3f..fce32ae 100644
--- a/src/Result.php
+++ b/src/ResultSet.php
@@ -22,13 +22,7 @@
namespace OCC\OaiPmh2;
-use Countable;
-use Iterator;
-use OCC\Basics\InterfaceTraits\Countable as CountableTrait;
-use OCC\Basics\InterfaceTraits\Iterator as IteratorTrait;
-use OCC\OaiPmh2\Entity\Format;
-use OCC\OaiPmh2\Entity\Record;
-use OCC\OaiPmh2\Entity\Set;
+use Doctrine\Common\Collections\ArrayCollection;
use OCC\OaiPmh2\Entity\Token;
/**
@@ -37,35 +31,15 @@
* @author Sebastian Meyer
* @package OAIPMH2
*
- * @template QueryResult of array
- * @implements Iterator
+ * @template TEntity of Entity
+ * @extends ArrayCollection
*/
-class Result implements Countable, Iterator
+final class ResultSet extends ArrayCollection
{
- use CountableTrait;
- use IteratorTrait;
-
- /**
- * This holds the Doctrine result set.
- *
- * @var QueryResult
- */
- private array $data;
-
/**
* This holds the optional resumption token.
*/
- protected ?Token $resumptionToken = null;
-
- /**
- * Get the query result.
- *
- * @return QueryResult The result set
- */
- public function getQueryResult(): array
- {
- return $this->data;
- }
+ private ?Token $resumptionToken;
/**
* Get the resumption token.
@@ -92,10 +66,12 @@ public function setResumptionToken(Token $token): void
/**
* Create new result set.
*
- * @param QueryResult $queryResult The Doctrine result set
+ * @param array $elements Array of entities
+ * @param Token $token Optional resumption token
*/
- public function __construct(array $queryResult)
+ public function __construct(array $elements = [], Token $token = null)
{
- $this->data = $queryResult;
+ parent::__construct(elements: $elements);
+ $this->resumptionToken = $token;
}
}
diff --git a/src/Validator/ConfigurationValidator.php b/src/Validator/ConfigurationValidator.php
new file mode 100644
index 0000000..17a622c
--- /dev/null
+++ b/src/Validator/ConfigurationValidator.php
@@ -0,0 +1,114 @@
+
+ *
+ * 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\Validator;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+use Symfony\Component\Validator\Validation;
+
+/**
+ * Validator for configuration settings.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ */
+class ConfigurationValidator
+{
+ /**
+ * Get constraints for configuration array.
+ *
+ * @return array The collection of constraints
+ */
+ protected static function getValidationConstraints(): array
+ {
+ return [
+ new Assert\Collection(
+ fields: [
+ 'repositoryName' => [
+ new Assert\Type(type: 'string'),
+ new Assert\NotBlank()
+ ],
+ 'adminEmail' => [
+ new Assert\Type(type: 'string'),
+ new Assert\Email(options: ['mode' => 'html5']),
+ new Assert\NotBlank()
+ ],
+ 'database' => [
+ new Assert\Type(type: 'string'),
+ new Assert\NotBlank()
+ ],
+ 'metadataPrefix' => [
+ new Assert\Type(type: 'array'),
+ new Assert\All(
+ constraints: [
+ new Assert\Collection(
+ fields: [
+ 'schema' => [
+ new Assert\Type(type: 'string'),
+ new Assert\Url(),
+ new Assert\NotBlank()
+ ],
+ 'namespace' => [
+ new Assert\Type(type: 'string'),
+ new Assert\Url(),
+ new Assert\NotBlank()
+ ]
+ ]
+ )
+ ]
+ )
+ ],
+ 'deletedRecords' => [
+ new Assert\Type(type: 'string'),
+ new Assert\Choice(options: ['no', 'persistent', 'transient']),
+ new Assert\NotBlank()
+ ],
+ 'maxRecords' => [
+ new Assert\Type(type: 'int'),
+ new Assert\Range(options: ['min' => 1, 'max' => 100])
+ ],
+ 'tokenValid' => [
+ new Assert\Type(type: 'int'),
+ new Assert\Range(options: ['min' => 300, 'max' => 86400])
+ ]
+ ]
+ )
+ ];
+ }
+
+ /**
+ * Validate the given configuration array.
+ *
+ * @param array $config The configuration array to validate
+ *
+ * @return ConstraintViolationListInterface The list of violations
+ */
+ public static function validate(array $config): ConstraintViolationListInterface
+ {
+ return Validation::createValidator()->validate(
+ value: $config,
+ constraints: self::getValidationConstraints()
+ );
+ }
+}
diff --git a/src/Validator/RegExValidator.php b/src/Validator/RegExValidator.php
new file mode 100644
index 0000000..b1a059c
--- /dev/null
+++ b/src/Validator/RegExValidator.php
@@ -0,0 +1,72 @@
+
+ *
+ * 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\Validator;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+use Symfony\Component\Validator\Validation;
+
+/**
+ * Validator for Regular Expressions.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ */
+class RegExValidator
+{
+ /**
+ * Get constraints for regular expression.
+ *
+ * @param string $regEx The regular expression for validation
+ *
+ * @return array The array of constraints
+ */
+ protected static function getValidationConstraints(string $regEx): array
+ {
+ return [
+ new Assert\Regex(
+ pattern: [
+ 'pattern' => $regEx,
+ 'message' => 'This value does not match the regular expression "{{ pattern }}".'
+ ]
+ )
+ ];
+ }
+
+ /**
+ * Check if a string matches a given regular expression.
+ *
+ * @param string $string The string
+ * @param string $regEx The regular expression
+ *
+ * @return ConstraintViolationListInterface The list of violations
+ */
+ public static function validate(string $string, string $regEx): ConstraintViolationListInterface
+ {
+ return Validation::createValidator()->validate(
+ value: $string,
+ constraints: self::getValidationConstraints(regEx: $regEx)
+ );
+ }
+}
diff --git a/src/Validator/UrlValidator.php b/src/Validator/UrlValidator.php
new file mode 100644
index 0000000..e345570
--- /dev/null
+++ b/src/Validator/UrlValidator.php
@@ -0,0 +1,65 @@
+
+ *
+ * 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\Validator;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+use Symfony\Component\Validator\Validation;
+
+/**
+ * Validator for URLs.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ */
+class UrlValidator
+{
+ /**
+ * Get constraints for URLs.
+ *
+ * @return array The array of constraints
+ */
+ protected static function getValidationConstraints(): array
+ {
+ return [
+ new Assert\Url(),
+ new Assert\NotBlank()
+ ];
+ }
+
+ /**
+ * Check if the given string is a valid URL.
+ *
+ * @param string $url The URL
+ *
+ * @return ConstraintViolationListInterface The list of violations
+ */
+ public static function validate(string $url): ConstraintViolationListInterface
+ {
+ return Validation::createValidator()->validate(
+ value: $url,
+ constraints: self::getValidationConstraints()
+ );
+ }
+}
diff --git a/src/Validator/XmlValidator.php b/src/Validator/XmlValidator.php
new file mode 100644
index 0000000..805f1b9
--- /dev/null
+++ b/src/Validator/XmlValidator.php
@@ -0,0 +1,79 @@
+
+ *
+ * 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\Validator;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\ConstraintViolation;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+use Symfony\Component\Validator\Validation;
+
+/**
+ * Validator for XML.
+ *
+ * @author Sebastian Meyer
+ * @package OAIPMH2
+ */
+class XmlValidator
+{
+ /**
+ * Get constraints for XML.
+ *
+ * @return array The array of constraints
+ */
+ protected static function getValidationConstraints(): array
+ {
+ return [
+ new Assert\Type(type: 'string'),
+ new Assert\NotBlank()
+ ];
+ }
+
+ /**
+ * Check if the given string is valid XML.
+ *
+ * @param string $xml The XML string
+ *
+ * @return ConstraintViolationListInterface The list of violations
+ */
+ public static function validate(string $xml): ConstraintViolationListInterface
+ {
+ $violations = Validation::createValidator()->validate(
+ value: $xml,
+ constraints: self::getValidationConstraints()
+ );
+ if (simplexml_load_string(data: $xml) === false) {
+ $violations->add(
+ violation: new ConstraintViolation(
+ message: 'Value could not be parsed as XML.',
+ messageTemplate: 'Value could not be parsed as XML.',
+ parameters: [],
+ root: $xml,
+ propertyPath: null,
+ invalidValue: $xml
+ )
+ );
+ }
+ return $violations;
+ }
+}