diff --git a/composer.lock b/composer.lock index f43ebcb6..5b446336 100644 --- a/composer.lock +++ b/composer.lock @@ -145,16 +145,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.8.0", + "version": "6.10.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc" + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", - "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", "shasum": "" }, "require": { @@ -164,7 +164,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "^23.2", + "json-schema/json-schema-test-suite": "dev-main", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -214,9 +214,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" }, - "time": "2026-04-02T12:43:11+00:00" + "time": "2026-06-16T20:50:26+00:00" }, { "name": "magicalex/write-ini-file", @@ -618,16 +618,16 @@ }, { "name": "phpgt/cron", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/phpgt/Cron.git", - "reference": "8a9ea03e5f3656f00375d772ed1e218d161c944b" + "reference": "15ec73221d852685b51320c69acc64e00bba9dda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Cron/zipball/8a9ea03e5f3656f00375d772ed1e218d161c944b", - "reference": "8a9ea03e5f3656f00375d772ed1e218d161c944b", + "url": "https://api.github.com/repos/phpgt/Cron/zipball/15ec73221d852685b51320c69acc64e00bba9dda", + "reference": "15ec73221d852685b51320c69acc64e00bba9dda", "shasum": "" }, "require": { @@ -641,7 +641,7 @@ "phpmd/phpmd": "^2.13", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.1", - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^4.0" }, "bin": [ "bin/cron" @@ -649,6 +649,7 @@ "type": "library", "autoload": { "psr-4": { + "GT\\Cron\\": "./src/", "Gt\\Cron\\": "./src/" } }, @@ -656,15 +657,15 @@ "description": "Run scripts or static functions at regular intervals.", "support": { "issues": "https://github.com/phpgt/Cron/issues", - "source": "https://github.com/phpgt/Cron/tree/v1.1.0" + "source": "https://github.com/phpgt/Cron/tree/v1.2.0" }, "funding": [ { - "url": "https://github.com/sponsors/PhpGt", + "url": "https://github.com/sponsors/phpgt", "type": "github" } ], - "time": "2026-04-07T22:05:39+00:00" + "time": "2026-06-25T21:46:17+00:00" }, { "name": "phpgt/csrf", @@ -893,16 +894,16 @@ }, { "name": "phpgt/database", - "version": "v1.7.0", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/phpgt/Database.git", - "reference": "d5a939e91ce3d5c78b77d9e9b3e1a54b13948db9" + "reference": "82e14aa1103372e273dbb45ffeaf45e21ef790b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Database/zipball/d5a939e91ce3d5c78b77d9e9b3e1a54b13948db9", - "reference": "d5a939e91ce3d5c78b77d9e9b3e1a54b13948db9", + "url": "https://api.github.com/repos/phpgt/Database/zipball/82e14aa1103372e273dbb45ffeaf45e21ef790b5", + "reference": "82e14aa1103372e273dbb45ffeaf45e21ef790b5", "shasum": "" }, "require": { @@ -918,7 +919,7 @@ "phpmd/phpmd": "^2.13", "phpstan/phpstan": "^v1.10", "phpunit/phpunit": "^10.1", - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^3.7 || ^4.0" }, "bin": [ "bin/migrate" @@ -926,6 +927,7 @@ "type": "library", "autoload": { "psr-4": { + "GT\\Database\\": "./src", "Gt\\Database\\": "./src" } }, @@ -950,7 +952,7 @@ "description": "Database API organisation.", "support": { "issues": "https://github.com/phpgt/Database/issues", - "source": "https://github.com/phpgt/Database/tree/v1.7.0" + "source": "https://github.com/phpgt/Database/tree/v1.8.0" }, "funding": [ { @@ -958,7 +960,7 @@ "type": "github" } ], - "time": "2026-04-07T22:13:10+00:00" + "time": "2026-06-10T10:27:56+00:00" }, { "name": "phpgt/dataobject", @@ -1215,22 +1217,22 @@ }, { "name": "phpgt/fetch", - "version": "v1.2.2", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/phpgt/Fetch.git", - "reference": "bc3473624eed6ca87953755580b3a8b5d10aaf9f" + "reference": "ab5df842c77c118cef954f0fb4939aa2ff965e1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Fetch/zipball/bc3473624eed6ca87953755580b3a8b5d10aaf9f", - "reference": "bc3473624eed6ca87953755580b3a8b5d10aaf9f", + "url": "https://api.github.com/repos/phpgt/Fetch/zipball/ab5df842c77c118cef954f0fb4939aa2ff965e1e", + "reference": "ab5df842c77c118cef954f0fb4939aa2ff965e1e", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", - "php": ">=8.3", + "php": ">=8.4", "phpgt/async": "^1.0", "phpgt/curl": "^3.2", "phpgt/http": "^1.3", @@ -1240,13 +1242,14 @@ }, "require-dev": { "phpmd/phpmd": "^2.13", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^1.10 || ^2.0", "phpunit/phpunit": "^10.2", - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^3.7 || ^4.0" }, "type": "library", "autoload": { "psr-4": { + "GT\\Fetch\\": "./src", "Gt\\Fetch\\": "./src" } }, @@ -1276,7 +1279,7 @@ ], "support": { "issues": "https://github.com/phpgt/Fetch/issues", - "source": "https://github.com/phpgt/Fetch/tree/v1.2.2" + "source": "https://github.com/phpgt/Fetch/tree/v1.2.3" }, "funding": [ { @@ -1284,7 +1287,7 @@ "type": "github" } ], - "time": "2025-12-09T23:16:42+00:00" + "time": "2026-06-22T09:58:51+00:00" }, { "name": "phpgt/filecache", @@ -1461,16 +1464,16 @@ }, { "name": "phpgt/json", - "version": "v2.2.1", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/phpgt/Json.git", - "reference": "df697c3fc7b3527ae3a01d3f96b1bfbab80feee2" + "reference": "d35835d4fb273a6e83106ceed79f45534c07d674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Json/zipball/df697c3fc7b3527ae3a01d3f96b1bfbab80feee2", - "reference": "df697c3fc7b3527ae3a01d3f96b1bfbab80feee2", + "url": "https://api.github.com/repos/phpgt/Json/zipball/d35835d4fb273a6e83106ceed79f45534c07d674", + "reference": "d35835d4fb273a6e83106ceed79f45534c07d674", "shasum": "" }, "require": { @@ -1505,7 +1508,7 @@ "description": " Structured, type-safe, immutable JSON objects.", "support": { "issues": "https://github.com/phpgt/Json/issues", - "source": "https://github.com/phpgt/Json/tree/v2.2.1" + "source": "https://github.com/phpgt/Json/tree/v2.3.0" }, "funding": [ { @@ -1513,7 +1516,7 @@ "type": "github" } ], - "time": "2026-04-21T13:31:54+00:00" + "time": "2026-07-09T17:54:44+00:00" }, { "name": "phpgt/logger", @@ -1715,16 +1718,16 @@ }, { "name": "phpgt/routing", - "version": "v1.1.7", + "version": "v1.1.8", "source": { "type": "git", "url": "https://github.com/phpgt/Routing.git", - "reference": "701479c7223a88e9649733e8e43a359150da9f8d" + "reference": "86158ac0ef0b5a85b8c460608cd68e96f7a1fd87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Routing/zipball/701479c7223a88e9649733e8e43a359150da9f8d", - "reference": "701479c7223a88e9649733e8e43a359150da9f8d", + "url": "https://api.github.com/repos/phpgt/Routing/zipball/86158ac0ef0b5a85b8c460608cd68e96f7a1fd87", + "reference": "86158ac0ef0b5a85b8c460608cd68e96f7a1fd87", "shasum": "" }, "require": { @@ -1775,7 +1778,7 @@ ], "support": { "issues": "https://github.com/phpgt/Routing/issues", - "source": "https://github.com/phpgt/Routing/tree/v1.1.7" + "source": "https://github.com/phpgt/Routing/tree/v1.1.8" }, "funding": [ { @@ -1783,7 +1786,7 @@ "type": "github" } ], - "time": "2026-04-16T17:15:54+00:00" + "time": "2026-07-09T17:02:53+00:00" }, { "name": "phpgt/server", @@ -1930,20 +1933,20 @@ }, { "name": "phpgt/sqlbuilder", - "version": "v1.0.1", + "version": "v1.1.0", "source": { "type": "git", - "url": "https://github.com/PhpGt/SqlBuilder.git", - "reference": "ac60cad74bb1ac2d8d667e142a56fd6c9d23291a" + "url": "https://github.com/phpgt/SqlBuilder.git", + "reference": "557289307cc188f708586d1d4e59b0bf77a3d608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/SqlBuilder/zipball/ac60cad74bb1ac2d8d667e142a56fd6c9d23291a", - "reference": "ac60cad74bb1ac2d8d667e142a56fd6c9d23291a", + "url": "https://api.github.com/repos/phpgt/SqlBuilder/zipball/557289307cc188f708586d1d4e59b0bf77a3d608", + "reference": "557289307cc188f708586d1d4e59b0bf77a3d608", "shasum": "" }, "require": { - "php": ">=8.0" + "php": ">=8.4" }, "require-dev": { "phpmd/phpmd": "^2.13", @@ -1954,22 +1957,23 @@ "type": "library", "autoload": { "psr-4": { + "GT\\SqlBuilder\\": "./src", "Gt\\SqlBuilder\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", "description": "Object oriented representation of SQL queries.", "support": { - "issues": "https://github.com/PhpGt/SqlBuilder/issues", - "source": "https://github.com/PhpGt/SqlBuilder/tree/v1.0.1" + "issues": "https://github.com/phpgt/SqlBuilder/issues", + "source": "https://github.com/phpgt/SqlBuilder/tree/v1.1.0" }, "funding": [ { - "url": "https://github.com/sponsors/PhpGt", + "url": "https://github.com/sponsors/phpgt", "type": "github" } ], - "time": "2023-09-17T12:33:25+00:00" + "time": "2026-05-06T17:09:03+00:00" }, { "name": "phpgt/sync", @@ -2031,16 +2035,16 @@ }, { "name": "phpgt/typesafegetter", - "version": "v1.3.3", + "version": "v1.3.4", "source": { "type": "git", "url": "https://github.com/phpgt/TypeSafeGetter.git", - "reference": "a0d339103828791989cbb81f760d252f3c2f8b8c" + "reference": "729261c04b76febaa21a921a3719f625abe9b55e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/TypeSafeGetter/zipball/a0d339103828791989cbb81f760d252f3c2f8b8c", - "reference": "a0d339103828791989cbb81f760d252f3c2f8b8c", + "url": "https://api.github.com/repos/phpgt/TypeSafeGetter/zipball/729261c04b76febaa21a921a3719f625abe9b55e", + "reference": "729261c04b76febaa21a921a3719f625abe9b55e", "shasum": "" }, "require": { @@ -2055,6 +2059,7 @@ "type": "library", "autoload": { "psr-4": { + "GT\\TypeSafeGetter\\": "./src", "Gt\\TypeSafeGetter\\": "./src" } }, @@ -2071,7 +2076,7 @@ "description": "An interface for objects that expose type-safe getter methods.", "support": { "issues": "https://github.com/phpgt/TypeSafeGetter/issues", - "source": "https://github.com/phpgt/TypeSafeGetter/tree/v1.3.3" + "source": "https://github.com/phpgt/TypeSafeGetter/tree/v1.3.4" }, "funding": [ { @@ -2079,7 +2084,7 @@ "type": "github" } ], - "time": "2026-03-10T22:28:01+00:00" + "time": "2026-05-04T15:53:13+00:00" }, { "name": "phpgt/ulid", @@ -2461,28 +2466,29 @@ "packages-dev": [ { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -2520,7 +2526,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -2530,13 +2536,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/xdebug-handler", @@ -2666,20 +2668,19 @@ }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -2718,9 +2719,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "pdepend/pdepend", @@ -2988,11 +2989,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.2", + "version": "2.2.5", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", - "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0", + "reference": "909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0", "shasum": "" }, "require": { @@ -3048,20 +3049,20 @@ "type": "github" } ], - "time": "2026-06-05T09:00:01+00:00" + "time": "2026-07-05T06:31:06+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "12.5.6", + "version": "12.5.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "876099a072646c7745f673d7aeab5382c4439691" + "reference": "186dab580576598076de6818596d12b61801880e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", - "reference": "876099a072646c7745f673d7aeab5382c4439691", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", "shasum": "" }, "require": { @@ -3072,13 +3073,13 @@ "php": ">=8.3", "phpunit/php-text-template": "^5.0", "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.0.3", - "sebastian/lines-of-code": "^4.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", "sebastian/version": "^6.0", "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.5.1" + "phpunit/phpunit": "^12.5.28" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -3116,7 +3117,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" }, "funding": [ { @@ -3136,7 +3137,7 @@ "type": "tidelift" } ], - "time": "2026-04-15T08:23:17+00:00" + "time": "2026-06-01T13:24:19+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3397,43 +3398,43 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.23", + "version": "12.5.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969" + "reference": "0608d157a284f15cc73b99a3327eff06b66a176d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", - "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0608d157a284f15cc73b99a3327eff06b66a176d", + "reference": "0608d157a284f15cc73b99a3327eff06b66a176d", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.6", + "phpunit/php-code-coverage": "^12.5.7", "phpunit/php-file-iterator": "^6.0.1", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.6", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.1.0", - "sebastian/exporter": "^7.0.2", - "sebastian/global-state": "^8.0.2", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", "sebastian/object-enumerator": "^7.0.0", "sebastian/recursion-context": "^7.0.1", - "sebastian/type": "^6.0.3", + "sebastian/type": "^6.0.4", "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, @@ -3475,7 +3476,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.31" }, "funding": [ { @@ -3483,7 +3484,7 @@ "type": "other" } ], - "time": "2026-04-18T06:12:49+00:00" + "time": "2026-07-06T14:54:16+00:00" }, { "name": "psr/log", @@ -3537,23 +3538,23 @@ }, { "name": "sebastian/cli-parser", - "version": "4.2.0", + "version": "4.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", - "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { @@ -3582,7 +3583,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" }, "funding": [ { @@ -3602,20 +3603,20 @@ "type": "tidelift" } ], - "time": "2025-09-14T09:36:45+00:00" + "time": "2026-05-17T05:29:34+00:00" }, { "name": "sebastian/comparator", - "version": "7.1.6", + "version": "7.1.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "c769009dee98f494e0edc3fd4f4087501688f11e" + "reference": "7c65c1e79836812819705b473a90c12399542485" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e", - "reference": "c769009dee98f494e0edc3fd4f4087501688f11e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", "shasum": "" }, "require": { @@ -3623,10 +3624,10 @@ "ext-mbstring": "*", "php": ">=8.3", "sebastian/diff": "^7.0", - "sebastian/exporter": "^7.0" + "sebastian/exporter": "^7.0.3" }, "require-dev": { - "phpunit/phpunit": "^12.2" + "phpunit/phpunit": "^12.5.25" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -3674,7 +3675,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" }, "funding": [ { @@ -3694,7 +3695,7 @@ "type": "tidelift" } ], - "time": "2026-04-14T08:23:15+00:00" + "time": "2026-05-21T04:45:25+00:00" }, { "name": "sebastian/complexity", @@ -3823,23 +3824,23 @@ }, { "name": "sebastian/environment", - "version": "8.1.0", + "version": "8.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6" + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6", - "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.26" }, "suggest": { "ext-posix": "*" @@ -3875,7 +3876,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.1.0" + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" }, "funding": [ { @@ -3895,29 +3896,29 @@ "type": "tidelift" } ], - "time": "2026-04-15T12:13:01+00:00" + "time": "2026-05-25T13:40:20+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.2", + "version": "7.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", - "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", "shasum": "" }, "require": { "ext-mbstring": "*", "php": ">=8.3", - "sebastian/recursion-context": "^7.0" + "sebastian/recursion-context": "^7.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { @@ -3965,7 +3966,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" }, "funding": [ { @@ -3985,30 +3986,30 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:16:11+00:00" + "time": "2026-05-20T04:37:17+00:00" }, { "name": "sebastian/global-state", - "version": "8.0.2", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", - "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", "shasum": "" }, "require": { "php": ">=8.3", "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0" + "sebastian/recursion-context": "^7.0.1" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.28" }, "type": "library", "extra": { @@ -4039,7 +4040,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" }, "funding": [ { @@ -4059,28 +4060,28 @@ "type": "tidelift" } ], - "time": "2025-08-29T11:29:25+00:00" + "time": "2026-06-01T15:10:33+00:00" }, { "name": "sebastian/lines-of-code", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", - "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { @@ -4109,15 +4110,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" } ], - "time": "2025-02-07T04:57:28+00:00" + "time": "2026-05-19T16:22:07+00:00" }, { "name": "sebastian/object-enumerator", @@ -4311,23 +4324,23 @@ }, { "name": "sebastian/type", - "version": "6.0.3", + "version": "6.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + "reference": "82ff822c2edc46724be9f7411d3163021f602773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", - "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { @@ -4356,7 +4369,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" }, "funding": [ { @@ -4376,7 +4389,7 @@ "type": "tidelift" } ], - "time": "2025-08-09T06:57:12+00:00" + "time": "2026-05-20T06:45:45+00:00" }, { "name": "sebastian/version", @@ -4565,16 +4578,16 @@ }, { "name": "symfony/config", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b" + "reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/2d19dde43fa2ff720b9a40763ace7226594f503b", - "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b", + "url": "https://api.github.com/repos/symfony/config/zipball/7b665e443381ea7c4db03eb03b4bf79ea2b020eb", + "reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb", "shasum": "" }, "require": { @@ -4620,7 +4633,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.8" + "source": "https://github.com/symfony/config/tree/v7.4.14" }, "funding": [ { @@ -4640,20 +4653,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-09T07:51:57+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "f7025fd7b687c240426562f86ada06a93b1e771d" + "reference": "2c8c64a33e2e6911579e1ff79a8e06c27d48d402" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f7025fd7b687c240426562f86ada06a93b1e771d", - "reference": "f7025fd7b687c240426562f86ada06a93b1e771d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2c8c64a33e2e6911579e1ff79a8e06c27d48d402", + "reference": "2c8c64a33e2e6911579e1ff79a8e06c27d48d402", "shasum": "" }, "require": { @@ -4704,7 +4717,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/v7.4.8" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.14" }, "funding": [ { @@ -4724,20 +4737,20 @@ "type": "tidelift" } ], - "time": "2026-03-31T06:50:29+00:00" + "time": "2026-06-24T07:41:05+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -4750,7 +4763,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -4775,7 +4788,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -4786,25 +4799,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.8", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", - "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", "shasum": "" }, "require": { @@ -4841,7 +4858,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.8" + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -4861,11 +4878,11 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4924,7 +4941,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -4946,18 +4963,105 @@ ], "time": "2026-04-10T16:19:22+00:00" }, + { + "name": "symfony/polyfill-deepclone", + "version": "v1.40.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-deepclone.git", + "reference": "dca4ccba5f360070b574414dce4c1e7a559844fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/dca4ccba5f360070b574414dce4c1e7a559844fa", + "reference": "dca4ccba5f360070b574414dce4c1e7a559844fa", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "provide": { + "ext-deepclone": "*" + }, + "suggest": { + "ext-deepclone": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\DeepClone\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the deepclone extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "deepclone", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.40.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-12T07:27:17+00:00" + }, { "name": "symfony/polyfill-mbstring", - "version": "v1.36.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -5009,7 +5113,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -5029,20 +5133,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -5060,7 +5164,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -5096,7 +5200,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -5116,24 +5220,26 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/var-exporter", - "version": "v8.0.8", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "15776bb07a91b089037da89f8832fa41d5fa6ec6" + "reference": "75b74315b4e4be40e5534cf9c5cc30dd0907ed71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/15776bb07a91b089037da89f8832fa41d5fa6ec6", - "reference": "15776bb07a91b089037da89f8832fa41d5fa6ec6", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/75b74315b4e4be40e5534cf9c5cc30dd0907ed71", + "reference": "75b74315b4e4be40e5534cf9c5cc30dd0907ed71", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-deepclone": "^1.40" }, "require-dev": { "symfony/property-access": "^7.4|^8.0", @@ -5163,11 +5269,12 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "description": "Provides tools to export, instantiate, hydrate, clone and lazy-load PHP objects", "homepage": "https://symfony.com", "keywords": [ "clone", "construct", + "deep-clone", "export", "hydrate", "instantiate", @@ -5176,7 +5283,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v8.0.8" + "source": "https://github.com/symfony/var-exporter/tree/v8.1.1" }, "funding": [ { @@ -5196,7 +5303,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-06-27T09:05:56+00:00" }, { "name": "theseer/tokenizer", diff --git a/config.default.ini b/config.default.ini index e61fa762..87b1ea47 100644 --- a/config.default.ini +++ b/config.default.ini @@ -19,7 +19,7 @@ error_page_dir=page/_error router_file=router.php router_class=AppRouter redirect_response_code=307 -default_content_type=text/html +default_content_type=application/json [view] component_directory=page/_component @@ -66,6 +66,8 @@ query_directory=query migration_path=_migration migration_table=_migration query_path=query +charset=utf8mb4 +collation=utf8mb4_general_ci [security] ;default_headers="X-Content-Type-Options: nosniff; X-Frame-Options: deny; Content-Security-Policy: default-src 'none'" diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php index 29156d3c..110edc90 100644 --- a/src/Dispatch/Dispatcher.php +++ b/src/Dispatch/Dispatcher.php @@ -239,6 +239,7 @@ public function __construct( ); $this->viewStreamer = $viewStreamer ?? new ViewStreamer(); $this->headerManager = $headerManager ?? new HeaderManager(); + $this->setupJsonDocumentErrorCallback(); } // phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh @@ -290,48 +291,153 @@ public function generateBasicErrorResponse( Throwable $actualThrowable, Throwable $innerThrowable, ):Response { -// TODO: Handle innerThrowable for if there's an error thrown in WebEngine itself. + $errorStatusCode = $this->getBasicErrorStatusCode($actualThrowable); + $errorType = get_class($actualThrowable); + $errorMessage = $this->getBasicErrorMessage( + $actualThrowable, + $errorStatusCode, + ); + $detail = $this->getBasicErrorDetail( + $actualThrowable, + $innerThrowable, + $errorStatusCode, + ); + + if($this->view instanceof JSONView || $this->viewModel instanceof JSONDocument) { + return $this->createBasicJsonErrorResponse( + $errorStatusCode, + $errorType, + $errorMessage, + $detail, + ); + } + + return $this->createBasicHtmlErrorResponse( + $errorStatusCode, + $errorType, + $errorMessage, + $detail, + ); + } + + private function getBasicErrorStatusCode(Throwable $throwable):int { $errorStatusCode = $this->response->getStatusCode(); - if(!$errorStatusCode) { - $errorStatusCode = $actualThrowable instanceof ResponseStatusException - ? $actualThrowable->getHttpCode() - : StatusCode::INTERNAL_SERVER_ERROR; + if($errorStatusCode) { + return $errorStatusCode; } - $errorType = get_class($actualThrowable); -// var_dump($errorType);die(); - $errorMessage = $actualThrowable->getMessage(); - $detail = ""; - $errorPageDir = $this->config->getString("app.error_page_dir"); + return $throwable instanceof ResponseStatusException + ? $throwable->getHttpCode() + : StatusCode::INTERNAL_SERVER_ERROR; + } - if(!$errorMessage) { - if($actualThrowable instanceof HttpNotFound) { - $errorMessage = "The server could not find the requested resource."; + private function getBasicErrorMessage( + Throwable $throwable, + int $errorStatusCode, + ):string { + $errorMessage = $throwable->getMessage(); + if($errorMessage) { + return $errorMessage; + } - if(!$this->config->getBool("app.production")) { - $detail .= " Additionally, there was no error page found in your " - . "application at $errorPageDir/$errorStatusCode.html"; - } - } + if($throwable instanceof HttpNotFound) { + return "The server could not find the requested resource."; } + return StatusCode::REASON_PHRASE[$errorStatusCode] ?? "Error"; + } + + private function getBasicErrorDetail( + Throwable $throwable, + Throwable $innerThrowable, + int $errorStatusCode, + ):string { + $detail = $this->getBasicNotFoundErrorDetail($throwable, $errorStatusCode); if($errorStatusCode >= 500 && !$this->config->getBool("app.production")) { - $detail .= implode(":", [ - $actualThrowable->getFile(), - $actualThrowable->getLine(), - ]) . "\n\n"; - foreach($actualThrowable->getTrace() as $i => $t) { - if(isset($t["file"]) && str_ends_with($t["file"], "/vendor/phpgt/servicecontainer/src/Injector.php")) { - break; - } - $detail .= "#$i\n"; + $detail .= $this->getBasicDebugErrorDetail($throwable); + } + if(!$this->config->getBool("app.production")) { + $detail .= $this->getBasicInnerThrowableDetail($innerThrowable); + } - foreach($t as $key => $value) { - $detail .= "$key:\t$value\n"; - } + return $detail; + } + + private function getBasicNotFoundErrorDetail( + Throwable $throwable, + int $errorStatusCode, + ):string { + if(!($throwable instanceof HttpNotFound) + || $throwable->getMessage() + || $this->config->getBool("app.production")) { + return ""; + } + + $errorPageDir = $this->config->getString("app.error_page_dir"); + return " Additionally, there was no error page found in your " + . "application at $errorPageDir/$errorStatusCode.html"; + } + + private function getBasicDebugErrorDetail(Throwable $throwable):string { + $detail = implode(":", [ + $throwable->getFile(), + $throwable->getLine(), + ]) . "\n\n"; + foreach($throwable->getTrace() as $i => $t) { + if(isset($t["file"]) && str_ends_with($t["file"], "/vendor/phpgt/servicecontainer/src/Injector.php")) { + break; + } + $detail .= "#$i\n"; + + foreach($t as $key => $value) { + $detail .= "$key:\t$value\n"; } } + return $detail; + } + + private function getBasicInnerThrowableDetail(Throwable $throwable):string { + return "\n\nFailed to render framework error response: " + . get_class($throwable) + . ": " + . $throwable->getMessage() + . "\n" + . $throwable->getFile() + . ":" + . $throwable->getLine() + . "\n"; + } + + private function createBasicJsonErrorResponse( + int $errorStatusCode, + string $errorType, + string $errorMessage, + string $detail, + ):Response { + $error = [ + "error" => $errorMessage, + "status" => $errorStatusCode, + "type" => $errorType, + ]; + if($detail !== "") { + $error["detail"] = $detail; + } + + $body = new Stream(); + $body->write((json_encode($error) ?: "{}") . "\n"); + $response = new Response(null, null, $this->request); + $response = $response->withHeader("Content-Type", "application/json"); + $response = $response->withBody($body); + return $response->withStatus($errorStatusCode); + } + + private function createBasicHtmlErrorResponse( + int $errorStatusCode, + string $errorType, + string $errorMessage, + string $detail, + ):Response { // TODO: Load this HTML from a file in the root of WebEngine! $html = << @@ -359,6 +465,34 @@ private function setupResponse():Response { return $response; } + private function setupJsonDocumentErrorCallback():void { + if(!($this->view instanceof JSONView) || !($this->viewModel instanceof JSONDocument)) { + return; + } + + $this->viewModel->setErrorCallback(function( + string $message, + int $statusCode, + ):void { + if(!$this->response->getStatusCode()) { + $this->response = $this->response->withStatus($statusCode); + } + if(!$this->response->hasHeader("Content-Type")) { + $this->response = $this->response->withHeader( + "Content-Type", + "application/json", + ); + } + + $this->viewStreamer->stream($this->view, $this->viewModel); + ($this->finishCallback)($this->response); + + if($this->interruptResponse) { + throw new ResponseInterrupt(); + } + }); + } + private function handleLogicExecution( Assembly $logicAssembly, Input $input, @@ -498,24 +632,8 @@ public function processResponse( } } - if($this->logicExecutionOrder) { - $this->response = $this->response->withHeader( - self::LOGIC_EXECUTION_HEADER, - implode(";", $this->logicExecutionOrder), - ); - } - - if($responseWithHeader = $this->headerManager->applyWithHeader( - $this->response->getResponseHeaders(), - $this->response->withHeader(...) - )) { - $this->response = $responseWithHeader; - } - - $documentBinder = $this->serviceContainer->get(Binder::class); - assert($documentBinder instanceof DocumentBinder); - $documentBinder->cleanupDocument(); - $this->protectHtmlDocumentFromCsrf(); + $this->applyResponseHeaders(); + $this->prepareViewModelForStreaming(); $this->viewStreamer->stream($this->view, $this->viewModel); $this->consumeCsrfToken($csrfToken); @@ -530,6 +648,39 @@ public function processResponse( } // phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh + private function applyResponseHeaders():void { + if($this->logicExecutionOrder) { + $this->response = $this->response->withHeader( + self::LOGIC_EXECUTION_HEADER, + implode(";", $this->logicExecutionOrder), + ); + } + + if($responseWithHeader = $this->headerManager->applyWithHeader( + $this->response->getResponseHeaders(), + $this->response->withHeader(...) + )) { + $this->response = $responseWithHeader; + } + } + + private function prepareViewModelForStreaming():void { + if($this->viewModel instanceof HTMLDocument) { + $documentBinder = $this->serviceContainer->get(Binder::class); + assert($documentBinder instanceof DocumentBinder); + $documentBinder->cleanupDocument(); + $this->protectHtmlDocumentFromCsrf(); + return; + } + + if($this->view instanceof JSONView && !$this->response->hasHeader("Content-Type")) { + $this->response = $this->response->withHeader( + "Content-Type", + "application/json", + ); + } + } + private function recordLogicExecution(string $functionReference):void { $refWithoutAttributes = explode("#", $functionReference, 2)[0]; $separatorPosition = strrpos($refWithoutAttributes, "::"); @@ -565,6 +716,10 @@ private function normaliseLogicExecutionFile(string $file):string { } private function verifyCsrfRequest(string $method, InputData $inputData):?string { + if(!($this->viewModel instanceof HTMLDocument)) { + return null; + } + if($method !== "POST" || $this->isIgnoredCsrfPath()) { return null; } diff --git a/src/View/BaseView.php b/src/View/BaseView.php index 4d6a9a29..930114a8 100644 --- a/src/View/BaseView.php +++ b/src/View/BaseView.php @@ -2,6 +2,7 @@ namespace GT\WebEngine\View; use GT\Dom\HTMLDocument; +use GT\Json\Schema\JSONDocument; use Psr\Http\Message\StreamInterface; abstract class BaseView { @@ -20,7 +21,7 @@ public function addViewFile(string $fileName):void { array_push($this->viewFileArray, $fileName); } - public function stream(HTMLDocument $viewModel):void { + public function stream(HTMLDocument|JSONDocument $viewModel):void { $this->outputStream->write((string)$viewModel); } } diff --git a/src/View/JSONView.php b/src/View/JSONView.php index 5d9298db..63ad827d 100644 --- a/src/View/JSONView.php +++ b/src/View/JSONView.php @@ -1,6 +1,7 @@ outputStream->write((string)$viewModel . "\n"); + } } diff --git a/src/View/ViewStreamer.php b/src/View/ViewStreamer.php index 784ed9d2..901a9ec9 100644 --- a/src/View/ViewStreamer.php +++ b/src/View/ViewStreamer.php @@ -2,11 +2,12 @@ namespace GT\WebEngine\View; use GT\Dom\HTMLDocument; +use GT\Json\Schema\JSONDocument; class ViewStreamer { public function stream( HTMLView|JSONView|NullView $view, - HTMLDocument $viewModel, + HTMLDocument|JSONDocument $viewModel, ):void { $view->stream($viewModel); } diff --git a/test/phpunit/DefaultRouterTest.php b/test/phpunit/DefaultRouterTest.php index cfe0cb7c..bc3750a2 100644 --- a/test/phpunit/DefaultRouterTest.php +++ b/test/phpunit/DefaultRouterTest.php @@ -16,6 +16,7 @@ use Gt\ServiceContainer\Container; use GT\WebEngine\View\HeaderFooterPartialConflictException; use GT\WebEngine\View\HTMLView; +use GT\WebEngine\View\JSONView; use PHPUnit\Framework\TestCase; require_once dirname(__DIR__, 2) . "/router.default.php"; @@ -145,6 +146,30 @@ public function testRoute_pageRequest_withDirectAndIndexView_throwsAmbiguousPath $sut->route($request); } + public function testRoute_apiRequest_withLogicOnly_usesJsonView():void { + mkdir($this->tmpDir . "/api", recursive: true); + file_put_contents($this->tmpDir . "/api/hello.php", "tmpDir); + + $request = self::createMock(Request::class); + $request->method("getMethod")->willReturn("GET"); + $request->method("getHeaderLine") + ->with("accept") + ->willReturn("application/json"); + $request->method("getUri")->willReturn(new Uri("https://example.test/hello")); + + $sut = new DefaultRouter(new RouterConfig(307, "text/html")); + $container = new Container(); + $container->set($request); + $sut->setContainer($container); + $sut->route($request); + + self::assertSame(JSONView::class, $sut->getViewClass()); + self::assertSame(["api/hello.php"], iterator_to_array($sut->getLogicAssembly())); + self::assertSame([], iterator_to_array($sut->getViewAssembly())); + } + private function removeDirectory(string $dir):void { if(!is_dir($dir)) { return; diff --git a/test/phpunit/Dispatch/DispatcherTest.php b/test/phpunit/Dispatch/DispatcherTest.php index 1f9dc104..4028f63f 100644 --- a/test/phpunit/Dispatch/DispatcherTest.php +++ b/test/phpunit/Dispatch/DispatcherTest.php @@ -24,6 +24,7 @@ use GT\Http\Stream; use GT\Http\Uri; use GT\Input\Input; +use GT\Json\Schema\JSONDocument; use GT\Logger\LogConfig; use GT\Routing\Assembly; use GT\Routing\BaseRouter; @@ -43,6 +44,8 @@ use GT\WebEngine\Logic\ViewModelProcessor; use GT\WebEngine\Test\Fixture\TestLogHandler; use GT\WebEngine\View\HTMLView; +use GT\WebEngine\View\JSONView; +use GT\WebEngine\View\NullView; use GT\WebEngine\View\ViewStreamer; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\MockObject\MockObject; @@ -140,6 +143,95 @@ public function testGenerateResponse_processesResponseAndDefaultsStatusToOk():vo self::assertSame($this->getPrivateProperty($sut, "sessionInit"), $sut->getSessionInit()); } + public function testGenerateResponse_streamsJsonDocumentMutatedByPostLogic():void { + $stream = new Stream(); + $view = new JSONView($stream); + $viewModel = new JSONDocument(); + $logicAssembly = $this->createAssembly("/tmp/api.php"); + + $viewModelInit = $this->createMock(ViewModelInit::class); + $viewModelInit->expects(self::once()) + ->method("getViewModelProcessor") + ->willReturn(null); + $viewModelInit->expects(self::never()) + ->method("initHTMLDocument"); + + $logicExecutor = $this->createMock(LogicExecutor::class); + $logicExecutor->method("invoke") + ->willReturnCallback(function(Assembly $assembly, string $name)use($logicAssembly, $viewModel):\Generator { + self::assertSame($logicAssembly, $assembly); + if($name !== "go") { + return; + } + + $viewModel->set("hello", "Greg"); + yield "/tmp/api.php::go()"; + }); + + $sut = $this->createDispatcher( + request: $this->createRequest("/hello", "POST"), + view: $view, + viewModel: $viewModel, + logicAssembly: $logicAssembly, + viewModelInit: $viewModelInit, + logicExecutor: $logicExecutor, + viewStreamer: new ViewStreamer(), + ); + + $response = $sut->generateResponse(); + + self::assertSame(StatusCode::OK, $response->getStatusCode()); + self::assertSame("application/json", $response->getHeaderLine("Content-Type")); + self::assertSame("{\"hello\":\"Greg\"}\n", (string)$stream); + self::assertSame("/tmp/api:go", $response->getHeaderLine("X-Logic-Execution")); + } + + public function testGenerateResponse_jsonDocumentErrorFinishesResponseAndInterruptsLogic():void { + $stream = new Stream(); + $view = new JSONView($stream); + $viewModel = new JSONDocument(); + $logicAssembly = $this->createAssembly("/tmp/api.php"); + $finishedResponse = null; + + $viewModelInit = $this->createMock(ViewModelInit::class); + $viewModelInit->method("getViewModelProcessor") + ->willReturn(null); + + $logicExecutor = $this->createMock(LogicExecutor::class); + $logicExecutor->method("invoke") + ->willReturnCallback(function(Assembly $assembly, string $name)use($logicAssembly, $viewModel):\Generator { + self::assertSame($logicAssembly, $assembly); + if($name !== "go") { + return; + } + + $viewModel->error("missing parameter: name", StatusCode::UNPROCESSABLE_ENTITY); + $viewModel->set("hello", "Greg"); + yield "/tmp/api.php::go()"; + }); + + $sut = $this->createDispatcher( + request: $this->createRequest("/hello", "POST"), + view: $view, + viewModel: $viewModel, + logicAssembly: $logicAssembly, + viewModelInit: $viewModelInit, + logicExecutor: $logicExecutor, + viewStreamer: new ViewStreamer(), + finishCallback: function(Response $response)use(&$finishedResponse):void { + $finishedResponse = $response; + }, + ); + + $response = $sut->generateResponse(); + + self::assertSame(StatusCode::UNPROCESSABLE_ENTITY, $response->getStatusCode()); + self::assertSame("application/json", $response->getHeaderLine("Content-Type")); + self::assertSame("{\"error\":\"missing parameter: name\"}\n", (string)$stream); + self::assertSame($response, $finishedResponse); + self::assertSame("", $response->getHeaderLine("X-Logic-Execution")); + } + public function testGenerateResponse_executesComponentAndPageLogicAndAppliesHeaders():void { $viewModel = new HTMLDocument( '' @@ -612,7 +704,10 @@ public function testGenerateBasicErrorResponse_inDevelopmentIncludesTraceUntilIn ], ]); - $response = $sut->generateBasicErrorResponse($throwable, new Exception("inner")); + $response = $sut->generateBasicErrorResponse( + $throwable, + new Exception("template exploded"), + ); $body = (string)$response->getBody(); self::assertSame(500, $response->getStatusCode()); @@ -620,6 +715,8 @@ public function testGenerateBasicErrorResponse_inDevelopmentIncludesTraceUntilIn self::assertStringContainsString("#0", $body); self::assertStringContainsString("file:\t/tmp/app/Before.php", $body); self::assertStringNotContainsString("After.php", $body); + self::assertStringContainsString("Failed to render framework error response", $body); + self::assertStringContainsString("template exploded", $body); } public function testGenerateBasicErrorResponse_inProductionOmitsDebugDetail():void { @@ -636,6 +733,52 @@ public function testGenerateBasicErrorResponse_inProductionOmitsDebugDetail():vo self::assertStringContainsString("boom", $body); self::assertStringNotContainsString(__FILE__, $body); self::assertStringNotContainsString("#0", $body); + self::assertStringNotContainsString("inner", $body); + } + + public function testGenerateBasicErrorResponse_forJsonViewReturnsJson():void { + $sut = $this->createDispatcher( + production: true, + view: new JSONView(new Stream()), + viewModel: new JSONDocument(), + ); + $this->setResponseStatus($sut, 500); + + $response = $sut->generateBasicErrorResponse( + new Exception("missing parameter: name"), + new Exception("inner"), + ); + + self::assertSame(500, $response->getStatusCode()); + self::assertSame("application/json", $response->getHeaderLine("Content-Type")); + self::assertSame( + [ + "error" => "missing parameter: name", + "status" => 500, + "type" => Exception::class, + ], + json_decode((string)$response->getBody(), true), + ); + self::assertStringEndsWith("\n", (string)$response->getBody()); + } + + public function testGenerateBasicErrorResponse_forJsonViewUsesStatusTextWhenThrowableMessageIsEmpty():void { + $sut = $this->createDispatcher( + production: true, + view: new JSONView(new Stream()), + viewModel: new JSONDocument(), + ); + $this->setResponseStatus($sut, 500); + + $response = $sut->generateBasicErrorResponse( + new Exception(), + new Exception("inner"), + ); + + self::assertSame( + "Internal Server Error", + json_decode((string)$response->getBody(), true)["error"], + ); } public function testGenerateBasicErrorResponse_defaultsToThrowableHttpCodeWhenResponseStatusUnset():void { @@ -801,8 +944,8 @@ private function createDispatcher( ?Request $request = null, ?array $globals = null, ?Input $input = null, - ?HTMLView $view = null, - ?HTMLDocument $viewModel = null, + NullView|JSONView|HTMLView|null $view = null, + HTMLDocument|JSONDocument|null $viewModel = null, ?Assembly $viewAssembly = null, ?Assembly $logicAssembly = null, ?ViewModelProcessor $viewModelProcessor = null, @@ -811,6 +954,7 @@ private function createDispatcher( ?ViewStreamer $viewStreamer = null, ?HeaderManager $headerManager = null, ?SessionInit $sessionInit = null, + ?\Closure $finishCallback = null, ):Dispatcher { $config ??= $this->createConfig($production); $request ??= $this->createRequest("/page/"); @@ -863,8 +1007,10 @@ private function createDispatcher( $logicExecutor ??= $this->createMock(LogicExecutor::class); $logicExecutor->method("invoke") ->willReturnCallback(fn():\Generator => $this->emptyGenerator()); - $viewStreamer ??= $this->createMock(ViewStreamer::class); - $viewStreamer->method("stream"); + if(!$viewStreamer) { + $viewStreamer = $this->createMock(ViewStreamer::class); + $viewStreamer->method("stream"); + } $headerManager ??= $this->createMock(HeaderManager::class); $headerManager->method("applyWithHeader")->willReturn(null); @@ -872,7 +1018,7 @@ private function createDispatcher( $config, $request, $globals, - static fn() => null, + $finishCallback ?? static fn() => null, null, $appAutoloader, $logicStreamHandler, diff --git a/test/phpunit/View/BaseViewTest.php b/test/phpunit/View/BaseViewTest.php index 916bb1f9..deb5aee4 100644 --- a/test/phpunit/View/BaseViewTest.php +++ b/test/phpunit/View/BaseViewTest.php @@ -1,6 +1,7 @@ newRecordingStream(); + $sut = $this->newTestableView($stream); + $doc = new JSONDocument(); + $doc->set("hello", "Greg"); + $sut->stream($doc); + self::assertSame('{"hello":"Greg"}', (string)$stream); + } + public function testAddViewFile_appendsInOrder():void { $stream = $this->newRecordingStream(); /** @var BaseView&object{getViewFiles: callable():array} $sut */ diff --git a/test/phpunit/View/JSONViewTest.php b/test/phpunit/View/JSONViewTest.php new file mode 100644 index 00000000..d67434cc --- /dev/null +++ b/test/phpunit/View/JSONViewTest.php @@ -0,0 +1,47 @@ +buffer; } + public function close():void {} + public function detach() {} + public function getSize():?int { return strlen($this->buffer); } + public function tell():int { return strlen($this->buffer); } + public function eof():bool { return true; } + public function isSeekable():bool { return false; } + public function seek($offset, $whence = SEEK_SET):void {} + public function rewind():void {} + public function isWritable():bool { return true; } + public function write($string):int { $this->buffer .= (string)$string; return strlen((string)$string); } + public function isReadable():bool { return false; } + public function read($length):string { return ""; } + public function getContents():string { return $this->buffer; } + public function getMetadata($key = null) { return null; } + }; + } + + public function testCreateViewModel_returnsJsonDocument():void { + $sut = new JSONView($this->newRecordingStream()); + + self::assertInstanceOf(JSONDocument::class, $sut->createViewModel()); + } + + public function testStream_appendsTrailingNewline():void { + $stream = $this->newRecordingStream(); + $sut = new JSONView($stream); + $doc = new JSONDocument(); + $doc->set("hello", "Greg"); + + $sut->stream($doc); + + self::assertSame("{\"hello\":\"Greg\"}\n", (string)$stream); + } +} diff --git a/test/phpunit/View/ViewStreamerTest.php b/test/phpunit/View/ViewStreamerTest.php index 15c9404c..5c4a4292 100644 --- a/test/phpunit/View/ViewStreamerTest.php +++ b/test/phpunit/View/ViewStreamerTest.php @@ -2,7 +2,9 @@ namespace GT\WebEngine\Test\View; use GT\Dom\HTMLDocument; +use GT\Json\Schema\JSONDocument; use GT\WebEngine\View\HTMLView; +use GT\WebEngine\View\JSONView; use GT\WebEngine\View\ViewStreamer; use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamInterface; @@ -22,6 +24,21 @@ public function testStream_delegatesToViewStreamMethod():void { $sut->stream($view, $document); } + public function testStream_delegatesJsonDocumentToViewStreamMethod():void { + $document = new JSONDocument(); + $document->set("hello", "Greg"); + $view = $this->getMockBuilder(JSONView::class) + ->setConstructorArgs([$this->newRecordingStream()]) + ->onlyMethods(["stream"]) + ->getMock(); + $view->expects(self::once()) + ->method("stream") + ->with($document); + + $sut = new ViewStreamer(); + $sut->stream($view, $document); + } + private function newRecordingStream():StreamInterface { return new class implements StreamInterface { public string $buffer = "";