Release 2.40.0 (#5621)

This commit is contained in:
Kevin Papst
2025-09-26 10:49:06 +02:00
committed by GitHub
parent 6d78c6ba36
commit 7937fa281a
54 changed files with 531 additions and 399 deletions

333
composer.lock generated
View File

@@ -134,56 +134,6 @@
},
"time": "2022-12-07T17:46:57+00:00"
},
{
"name": "behat/transliterator",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/Behat/Transliterator.git",
"reference": "baac5873bac3749887d28ab68e2f74db3a4408af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Behat/Transliterator/zipball/baac5873bac3749887d28ab68e2f74db3a4408af",
"reference": "baac5873bac3749887d28ab68e2f74db3a4408af",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
"chuyskywalker/rolling-curl": "^3.1",
"php-yaoi/php-yaoi": "^1.0",
"phpunit/phpunit": "^8.5.25 || ^9.5.19"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Behat\\Transliterator\\": "src/Behat/Transliterator"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Artistic-1.0"
],
"description": "String transliterator",
"keywords": [
"i18n",
"slug",
"transliterator"
],
"support": {
"issues": "https://github.com/Behat/Transliterator/issues",
"source": "https://github.com/Behat/Transliterator/tree/v1.5.0"
},
"abandoned": true,
"time": "2022-03-30T09:27:43+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
@@ -342,16 +292,16 @@
},
{
"name": "dasprid/enum",
"version": "1.0.6",
"version": "1.0.7",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90"
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
"shasum": ""
},
"require": {
@@ -386,9 +336,9 @@
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.6"
"source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
},
"time": "2024-08-09T14:30:48+00:00"
"time": "2025-09-16T12:23:56+00:00"
},
{
"name": "doctrine/cache",
@@ -662,16 +612,16 @@
},
{
"name": "doctrine/dbal",
"version": "3.10.1",
"version": "3.10.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "3626601014388095d3af9de7e9e958623b7ef005"
"reference": "c6c16cf787eaba3112203dfcd715fa2059c62282"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/3626601014388095d3af9de7e9e958623b7ef005",
"reference": "3626601014388095d3af9de7e9e958623b7ef005",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/c6c16cf787eaba3112203dfcd715fa2059c62282",
"reference": "c6c16cf787eaba3112203dfcd715fa2059c62282",
"shasum": ""
},
"require": {
@@ -687,10 +637,10 @@
},
"require-dev": {
"doctrine/cache": "^1.11|^2.0",
"doctrine/coding-standard": "13.0.0",
"doctrine/coding-standard": "13.0.1",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
"phpstan/phpstan": "2.1.17",
"phpstan/phpstan": "2.1.22",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "9.6.23",
"slevomat/coding-standard": "8.16.2",
@@ -756,7 +706,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.10.1"
"source": "https://github.com/doctrine/dbal/tree/3.10.2"
},
"funding": [
{
@@ -772,7 +722,7 @@
"type": "tidelift"
}
],
"time": "2025-08-05T12:18:06+00:00"
"time": "2025-09-04T23:51:27+00:00"
},
{
"name": "doctrine/deprecations",
@@ -824,16 +774,16 @@
},
{
"name": "doctrine/doctrine-bundle",
"version": "2.15.1",
"version": "2.16.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/DoctrineBundle.git",
"reference": "5a305c5e776f9d3eb87f5b94d40d50aff439211d"
"reference": "1c10de0fe995f01eca6b073d1c2549ef0b603a7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5a305c5e776f9d3eb87f5b94d40d50aff439211d",
"reference": "5a305c5e776f9d3eb87f5b94d40d50aff439211d",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/1c10de0fe995f01eca6b073d1c2549ef0b603a7f",
"reference": "1c10de0fe995f01eca6b073d1c2549ef0b603a7f",
"shasum": ""
},
"require": {
@@ -867,11 +817,11 @@
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan-phpunit": "2.0.3",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^9.6.22",
"phpunit/phpunit": "^10.5.53",
"psr/log": "^1.1.4 || ^2.0 || ^3.0",
"symfony/doctrine-messenger": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
"symfony/messenger": "^6.4 || ^7.0",
"symfony/phpunit-bridge": "^7.2",
"symfony/property-info": "^6.4 || ^7.0",
"symfony/security-bundle": "^6.4 || ^7.0",
"symfony/stopwatch": "^6.4 || ^7.0",
@@ -926,7 +876,7 @@
],
"support": {
"issues": "https://github.com/doctrine/DoctrineBundle/issues",
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.15.1"
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.16.2"
},
"funding": [
{
@@ -942,7 +892,7 @@
"type": "tidelift"
}
],
"time": "2025-07-30T15:48:28+00:00"
"time": "2025-09-10T19:14:48+00:00"
},
{
"name": "doctrine/doctrine-migrations-bundle",
@@ -2077,20 +2027,19 @@
},
{
"name": "gedmo/doctrine-extensions",
"version": "v3.20.0",
"version": "v3.21.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine-extensions/DoctrineExtensions.git",
"reference": "ea1d37586b8e4bae2a815feb38b177894b12c44c"
"reference": "eb53dfcb2b592327b76ac5226fbb003d32aea37e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/ea1d37586b8e4bae2a815feb38b177894b12c44c",
"reference": "ea1d37586b8e4bae2a815feb38b177894b12c44c",
"url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eb53dfcb2b592327b76ac5226fbb003d32aea37e",
"reference": "eb53dfcb2b592327b76ac5226fbb003d32aea37e",
"shasum": ""
},
"require": {
"behat/transliterator": "^1.2",
"doctrine/collections": "^1.2 || ^2.0",
"doctrine/deprecations": "^1.0",
"doctrine/event-manager": "^1.2 || ^2.0",
@@ -2098,9 +2047,11 @@
"php": "^7.4 || ^8.0",
"psr/cache": "^1 || ^2 || ^3",
"psr/clock": "^1",
"symfony/cache": "^5.4 || ^6.0 || ^7.0"
"symfony/cache": "^5.4 || ^6.0 || ^7.0",
"symfony/string": "^5.4 || ^6.0 || ^7.0"
},
"conflict": {
"behat/transliterator": "<1.2 || >=2.0",
"doctrine/annotations": "<1.13 || >=3.0",
"doctrine/common": "<2.13 || >=4.0",
"doctrine/dbal": "<3.7 || >=5.0",
@@ -2108,6 +2059,7 @@
"doctrine/orm": "<2.20 || >=3.0 <3.3 || >=4.0"
},
"require-dev": {
"behat/transliterator": "^1.2",
"doctrine/annotations": "^1.13 || ^2.0",
"doctrine/cache": "^1.11 || ^2.0",
"doctrine/common": "^2.13 || ^3.0",
@@ -2124,7 +2076,7 @@
"rector/rector": "^2.0.6",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/doctrine-bridge": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^6.0 || ^7.0",
"symfony/phpunit-bridge": "^6.4 || ^7.0",
"symfony/uid": "^5.4 || ^6.0 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
},
@@ -2183,7 +2135,7 @@
"support": {
"docs": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc",
"issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues",
"source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.20.0"
"source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.21.0"
},
"funding": [
{
@@ -2203,7 +2155,7 @@
"type": "github"
}
],
"time": "2025-04-04T17:19:27+00:00"
"time": "2025-09-22T17:04:34+00:00"
},
{
"name": "jms/metadata",
@@ -2537,16 +2489,16 @@
},
{
"name": "league/csv",
"version": "9.24.1",
"version": "9.25.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8"
"reference": "f856f532866369fb1debe4e7c5a1db185f40ef86"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8",
"reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/f856f532866369fb1debe4e7c5a1db185f40ef86",
"reference": "f856f532866369fb1debe4e7c5a1db185f40ef86",
"shasum": ""
},
"require": {
@@ -2562,7 +2514,7 @@
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"phpstan/phpstan-strict-rules": "^1.6.2",
"phpunit/phpunit": "^10.5.16 || ^11.5.22",
"phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.3.6",
"symfony/var-dumper": "^6.4.8 || ^7.3.0"
},
"suggest": {
@@ -2624,7 +2576,7 @@
"type": "github"
}
],
"time": "2025-06-25T14:53:51+00:00"
"time": "2025-09-11T08:29:08+00:00"
},
{
"name": "lorenzo/pinky",
@@ -3201,16 +3153,16 @@
},
{
"name": "nelmio/api-doc-bundle",
"version": "v5.5.0",
"version": "v5.6.2",
"source": {
"type": "git",
"url": "https://github.com/nelmio/NelmioApiDocBundle.git",
"reference": "ce6434107ccb57d0686e25ae6c7767c75a3c6e4e"
"reference": "9b7ece3141b74699008be55231d6907b5e8bc883"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/ce6434107ccb57d0686e25ae6c7767c75a3c6e4e",
"reference": "ce6434107ccb57d0686e25ae6c7767c75a3c6e4e",
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/9b7ece3141b74699008be55231d6907b5e8bc883",
"reference": "9b7ece3141b74699008be55231d6907b5e8bc883",
"shasum": ""
},
"require": {
@@ -3312,7 +3264,7 @@
],
"support": {
"issues": "https://github.com/nelmio/NelmioApiDocBundle/issues",
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v5.5.0"
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v5.6.2"
},
"funding": [
{
@@ -3320,7 +3272,7 @@
"type": "github"
}
],
"time": "2025-08-04T14:41:29+00:00"
"time": "2025-09-19T12:23:35+00:00"
},
{
"name": "nelmio/cors-bundle",
@@ -3704,16 +3656,16 @@
},
{
"name": "paragonie/constant_time_encoding",
"version": "v2.7.0",
"version": "v2.8.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105"
"reference": "ce27936c8dfb73e3ab9c94469130428af9752c96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105",
"reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/ce27936c8dfb73e3ab9c94469130428af9752c96",
"reference": "ce27936c8dfb73e3ab9c94469130428af9752c96",
"shasum": ""
},
"require": {
@@ -3767,7 +3719,7 @@
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"time": "2024-05-08T12:18:48+00:00"
"time": "2025-09-22T20:41:46+00:00"
},
{
"name": "paragonie/random_compat",
@@ -4048,16 +4000,16 @@
},
{
"name": "phpoffice/phpspreadsheet",
"version": "2.4.0",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "3a3cad86101a77019eb2fc693aab1a8c11b18b94"
"reference": "096ae6faf94b49b2cf53e92a0073133c941e1f57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3a3cad86101a77019eb2fc693aab1a8c11b18b94",
"reference": "3a3cad86101a77019eb2fc693aab1a8c11b18b94",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/096ae6faf94b49b2cf53e92a0073133c941e1f57",
"reference": "096ae6faf94b49b2cf53e92a0073133c941e1f57",
"shasum": ""
},
"require": {
@@ -4147,9 +4099,9 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.0"
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.1"
},
"time": "2025-08-10T06:45:13+00:00"
"time": "2025-09-01T18:41:37+00:00"
},
{
"name": "phpoffice/phpword",
@@ -4261,16 +4213,16 @@
},
{
"name": "phpstan/phpdoc-parser",
"version": "2.2.0",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8"
"reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8",
"reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495",
"reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495",
"shasum": ""
},
"require": {
@@ -4302,9 +4254,9 @@
"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/2.2.0"
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0"
},
"time": "2025-07-13T07:04:09+00:00"
"time": "2025-08-30T15:50:23+00:00"
},
{
"name": "psr/cache",
@@ -10552,16 +10504,16 @@
},
{
"name": "zircote/swagger-php",
"version": "5.3.2",
"version": "5.4.0",
"source": {
"type": "git",
"url": "https://github.com/zircote/swagger-php.git",
"reference": "d8fa9dc4c3b2fc8651ae780021bb9719b1e63d40"
"reference": "e25c377ec04db4d2b91186e2debaa1fb135f5cc5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/d8fa9dc4c3b2fc8651ae780021bb9719b1e63d40",
"reference": "d8fa9dc4c3b2fc8651ae780021bb9719b1e63d40",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/e25c377ec04db4d2b91186e2debaa1fb135f5cc5",
"reference": "e25c377ec04db4d2b91186e2debaa1fb135f5cc5",
"shasum": ""
},
"require": {
@@ -10632,9 +10584,9 @@
],
"support": {
"issues": "https://github.com/zircote/swagger-php/issues",
"source": "https://github.com/zircote/swagger-php/tree/5.3.2"
"source": "https://github.com/zircote/swagger-php/tree/5.4.0"
},
"time": "2025-08-25T21:57:16+00:00"
"time": "2025-09-12T03:49:27+00:00"
}
],
"packages-dev": [
@@ -11180,16 +11132,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.86.0",
"version": "v3.87.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "4a952bd19dc97879b0620f495552ef09b55f7d36"
"reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4a952bd19dc97879b0620f495552ef09b55f7d36",
"reference": "4a952bd19dc97879b0620f495552ef09b55f7d36",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
"reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
"shasum": ""
},
"require": {
@@ -11200,39 +11152,38 @@
"ext-hash": "*",
"ext-json": "*",
"ext-tokenizer": "*",
"fidry/cpu-core-counter": "^1.2",
"fidry/cpu-core-counter": "^1.3",
"php": "^7.4 || ^8.0",
"react/child-process": "^0.6.6",
"react/event-loop": "^1.5",
"react/promise": "^3.2",
"react/promise": "^3.3",
"react/socket": "^1.16",
"react/stream": "^1.4",
"sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
"symfony/console": "^5.4.47 || ^6.4.13 || ^7.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0",
"symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0",
"symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0",
"symfony/options-resolver": "^5.4.45 || ^6.4.16 || ^7.0",
"symfony/polyfill-mbstring": "^1.32",
"symfony/polyfill-php80": "^1.32",
"symfony/polyfill-php81": "^1.32",
"symfony/process": "^5.4.47 || ^6.4.20 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.19 || ^7.0"
"symfony/console": "^5.4.47 || ^6.4.24 || ^7.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/polyfill-mbstring": "^1.33",
"symfony/polyfill-php80": "^1.33",
"symfony/polyfill-php81": "^1.33",
"symfony/process": "^5.4.47 || ^6.4.24 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0"
},
"require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.6",
"facile-it/paraunit": "^1.3.1 || ^2.7",
"infection/infection": "^0.29.14",
"justinrainbow/json-schema": "^5.3 || ^6.4",
"justinrainbow/json-schema": "^6.5",
"keradus/cli-executor": "^2.2",
"mikey179/vfsstream": "^1.6.12",
"php-coveralls/php-coveralls": "^2.8",
"php-cs-fixer/accessible-object": "^1.1",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.23 || ^10.5.47 || ^11.5.25",
"symfony/polyfill-php84": "^1.32",
"symfony/var-dumper": "^5.4.48 || ^6.4.23 || ^7.3.1",
"symfony/yaml": "^5.4.45 || ^6.4.23 || ^7.3.1"
"phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34",
"symfony/polyfill-php84": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2",
"symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2"
},
"suggest": {
"ext-dom": "For handling output formats in XML",
@@ -11273,7 +11224,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.86.0"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.87.2"
},
"funding": [
{
@@ -11281,7 +11232,7 @@
"type": "github"
}
],
"time": "2025-08-13T22:36:21+00:00"
"time": "2025-09-10T09:51:40+00:00"
},
{
"name": "masterminds/html5",
@@ -11470,16 +11421,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.22",
"version": "2.1.28",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4"
"reference": "578fa296a166605d97b94091f724f1257185d278"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4",
"reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278",
"reference": "578fa296a166605d97b94091f724f1257185d278",
"shasum": ""
},
"require": {
@@ -11524,7 +11475,7 @@
"type": "github"
}
],
"time": "2025-08-04T19:17:37+00:00"
"time": "2025-09-19T08:58:49+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -11575,16 +11526,16 @@
},
{
"name": "phpstan/phpstan-doctrine",
"version": "2.0.4",
"version": "2.0.6",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-doctrine.git",
"reference": "6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8"
"reference": "934f5734812341358fc41c44006b30fa00c785f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8",
"reference": "6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/934f5734812341358fc41c44006b30fa00c785f0",
"reference": "934f5734812341358fc41c44006b30fa00c785f0",
"shasum": ""
},
"require": {
@@ -11641,9 +11592,9 @@
"description": "Doctrine extensions for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.4"
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.6"
},
"time": "2025-07-17T11:57:55+00:00"
"time": "2025-09-10T07:06:30+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
@@ -11748,16 +11699,16 @@
},
{
"name": "phpstan/phpstan-symfony",
"version": "2.0.7",
"version": "2.0.8",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "392f7ab8f52a0a776977be4e62535358c28e1b15"
"reference": "8820c22d785c235f69bb48da3d41e688bc8a1796"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/392f7ab8f52a0a776977be4e62535358c28e1b15",
"reference": "392f7ab8f52a0a776977be4e62535358c28e1b15",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/8820c22d785c235f69bb48da3d41e688bc8a1796",
"reference": "8820c22d785c235f69bb48da3d41e688bc8a1796",
"shasum": ""
},
"require": {
@@ -11813,9 +11764,9 @@
"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/2.0.7"
"source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.8"
},
"time": "2025-07-22T09:40:57+00:00"
"time": "2025-09-07T06:55:50+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -12140,16 +12091,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.53",
"version": "10.5.56",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "32768472ebfb6969e6c7399f1c7b09009723f653"
"reference": "e00fdca14e854ed26256da31f4ab1c9a58453742"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653",
"reference": "32768472ebfb6969e6c7399f1c7b09009723f653",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e00fdca14e854ed26256da31f4ab1c9a58453742",
"reference": "e00fdca14e854ed26256da31f4ab1c9a58453742",
"shasum": ""
},
"require": {
@@ -12170,10 +12121,10 @@
"phpunit/php-timer": "^6.0.0",
"sebastian/cli-parser": "^2.0.1",
"sebastian/code-unit": "^2.0.0",
"sebastian/comparator": "^5.0.3",
"sebastian/comparator": "^5.0.4",
"sebastian/diff": "^5.1.1",
"sebastian/environment": "^6.1.0",
"sebastian/exporter": "^5.1.2",
"sebastian/exporter": "^5.1.3",
"sebastian/global-state": "^6.0.2",
"sebastian/object-enumerator": "^5.0.0",
"sebastian/recursion-context": "^5.0.1",
@@ -12221,7 +12172,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.53"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.56"
},
"funding": [
{
@@ -12245,7 +12196,7 @@
"type": "tidelift"
}
],
"time": "2025-08-20T14:40:06+00:00"
"time": "2025-09-23T06:21:55+00:00"
},
{
"name": "react/cache",
@@ -12943,16 +12894,16 @@
},
{
"name": "sebastian/comparator",
"version": "5.0.3",
"version": "5.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e"
"reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
"reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e",
"reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e",
"shasum": ""
},
"require": {
@@ -13008,15 +12959,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3"
"source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4"
},
"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/comparator",
"type": "tidelift"
}
],
"time": "2024-10-18T14:56:07+00:00"
"time": "2025-09-07T05:25:07+00:00"
},
{
"name": "sebastian/complexity",
@@ -13209,16 +13172,16 @@
},
{
"name": "sebastian/exporter",
"version": "5.1.2",
"version": "5.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "955288482d97c19a372d3f31006ab3f37da47adf"
"reference": "9e7e86260de48e405ec3086bcb62e677ef192e7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf",
"reference": "955288482d97c19a372d3f31006ab3f37da47adf",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/9e7e86260de48e405ec3086bcb62e677ef192e7f",
"reference": "9e7e86260de48e405ec3086bcb62e677ef192e7f",
"shasum": ""
},
"require": {
@@ -13227,7 +13190,7 @@
"sebastian/recursion-context": "^5.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
"phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
@@ -13275,15 +13238,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
"source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2"
"source": "https://github.com/sebastianbergmann/exporter/tree/5.1.3"
},
"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/exporter",
"type": "tidelift"
}
],
"time": "2024-03-02T07:17:12+00:00"
"time": "2025-09-22T05:25:48+00:00"
},
{
"name": "sebastian/global-state",

View File

@@ -423,11 +423,6 @@ parameters:
count: 1
path: src/Command/ReloadCommand.php
-
message: "#^Access to an undefined property SimpleXMLElement\\|false\\:\\:\\$file\\.$#"
count: 1
path: src/Command/TranslationCommand.php
-
message: "#^Argument of an invalid type list\\<string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
count: 4
@@ -3198,21 +3193,6 @@ parameters:
count: 1
path: src/Plugin/PluginMetadata.php
-
message: "#^Cannot access offset 'duration' on mixed\\.$#"
count: 3
path: src/Project/ProjectStatisticService.php
-
message: "#^Cannot access offset 'id' on mixed\\.$#"
count: 4
path: src/Project/ProjectStatisticService.php
-
message: "#^Cannot access offset 'lastRecord' on mixed\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Cannot call method getEnd\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
count: 2
@@ -3243,31 +3223,11 @@ parameters:
count: 2
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$datetime of method App\\\\Timesheet\\\\DateTimeFactory\\:\\:createDateTime\\(\\) expects string, mixed given\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$duration of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setDuration\\(\\) expects int, float\\|int given\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$durationDay of method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:setDurationDay\\(\\) expects int, mixed given\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$durationMonth of method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:setDurationMonth\\(\\) expects int, mixed given\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$durationWeek of method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:setDurationWeek\\(\\) expects int, mixed given\\.$#"
count: 1
path: src/Project/ProjectStatisticService.php
-
message: "#^Parameter \\#1 \\$project of class App\\\\Reporting\\\\ProjectDetails\\\\ProjectDetailsModel constructor expects App\\\\Entity\\\\Project, App\\\\Entity\\\\Project\\|null given\\.$#"
count: 1
@@ -3618,26 +3578,6 @@ parameters:
count: 1
path: src/Timesheet/Rounding/ClosestRounding.php
-
message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#"
count: 2
path: src/Timesheet/Rounding/DefaultRounding.php
-
message: "#^Cannot call method setTimestamp\\(\\) on DateTime\\|null\\.$#"
count: 2
path: src/Timesheet/Rounding/DefaultRounding.php
-
message: "#^Cannot clone DateTime\\|null\\.$#"
count: 2
path: src/Timesheet/Rounding/DefaultRounding.php
-
message: "#^Parameter \\#1 \\$begin of method App\\\\Entity\\\\Timesheet\\:\\:setBegin\\(\\) expects DateTime, DateTime\\|null given\\.$#"
count: 1
path: src/Timesheet/Rounding/DefaultRounding.php
-
message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#"
count: 2

View File

@@ -13,7 +13,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
final class NotFoundException extends NotFoundHttpException
{
public function __construct(string $message = 'Not found', \Exception $previous = null, int $code = 404, array $headers = [])
public function __construct(string $message = 'Not found', ?\Throwable $previous = null, int $code = 404, array $headers = [])
{
parent::__construct($message, $previous, $code, $headers);
}

View File

@@ -11,6 +11,7 @@ namespace App\API;
use App\Entity\AccessToken;
use App\Entity\User;
use App\Entity\UserPreference;
use App\Event\PrepareUserEvent;
use App\Form\API\UserApiCreateForm;
use App\Form\API\UserApiEditForm;
@@ -23,10 +24,12 @@ use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Request\ParamFetcherInterface;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -224,4 +227,44 @@ final class UserController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Update user preferences
*/
#[IsGranted('edit', 'profile')]
#[OA\Response(response: 200, description: 'Sets the value of a consifgured preference. You cannot create unknown preferences: if the given name is not configured, an exception will be raised.', content: new OA\JsonContent(ref: '#/components/schemas/UserEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'User ID to set the custom-field value for', required: true)]
#[OA\RequestBody(required: true, content: new OA\JsonContent(type: 'array', items: new OA\Items(new Model(type: UserPreference::class))))]
#[Route(methods: ['PATCH'], path: '/{id}/preferences', requirements: ['id' => '\d+'])]
public function updateUserPreference(User $profile, Request $request, EventDispatcherInterface $dispatcher): Response
{
$event = new PrepareUserEvent($profile, false);
$dispatcher->dispatch($event);
foreach ($request->request->all() as $preference) {
// why is this not handled by FosRestBundle ?
if (!\is_array($preference)) {
throw new BadRequestHttpException('Invalid request, array expected');
}
if (!\array_key_exists('name', $preference) || !\array_key_exists('value', $preference)) {
throw new BadRequestHttpException('Missing required parameter "name" or "value"');
}
$name = $preference['name'];
$value = $preference['value'];
if (null === ($meta = $profile->getPreference($name))) {
throw $this->createNotFoundException(\sprintf('Unknown custom-field "%s" requested', $name));
}
$meta->setValue($value);
}
$this->repository->saveUser($profile);
$view = new View($profile, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view);
}
}

View File

@@ -162,7 +162,7 @@ class ActivityStatisticService
/**
* @param Activity[] $activities
*/
private function createStatisticQueryBuilder(array $activities, \DateTimeInterface $begin = null, ?\DateTimeInterface $end = null): QueryBuilder
private function createStatisticQueryBuilder(array $activities, ?\DateTimeInterface $begin = null, ?\DateTimeInterface $end = null): QueryBuilder
{
$qb = $this->timesheetRepository->createQueryBuilder('t');
$qb

View File

@@ -457,7 +457,7 @@ final class TranslationCommand extends Command
$unit['resname'] = $source;
}
if ($unit['resname'] !== null) {
$unit['id'] = $this->generateId($unit['resname']); // @phpstan-ignore offsetAssign.valueType
$unit['id'] = $this->generateId($unit['resname']);
}
}
@@ -492,7 +492,7 @@ final class TranslationCommand extends Command
);
}
$unit->target[0] = $translations[$key];
$unit->target['state'] = 'needs-translation'; // @phpstan-ignore assign.propertyType
$unit->target['state'] = 'needs-translation';
$foundEmpty = true;
}

View File

@@ -17,11 +17,11 @@ final class Constants
/**
* The current release version
*/
public const VERSION = '2.39.0';
public const VERSION = '2.40.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 23900;
public const VERSION_ID = 24000;
/**
* The software name
*/

View File

@@ -239,7 +239,8 @@ final class ExportController extends AbstractController
}
return $this->render('export/template.html.twig', [
'form' => $form->createView()
'form' => $form->createView(),
'template' => $exportTemplate,
]);
}
}

View File

@@ -74,11 +74,9 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
}
/**
* @param User $user
* @param string|null $timezone
* @return array<UserPreference>
*/
private function getUserPreferences(User $user, string $timezone = null): array
private function getUserPreferences(User $user, ?string $timezone = null): array
{
$preferences = [];

View File

@@ -77,7 +77,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[ORM\Column(name: 'company', type: Types::STRING, length: 100, nullable: true)]
#[Assert\Length(max: 100)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'company')]
private ?string $company = null;
#[ORM\Column(name: 'vat_id', type: Types::STRING, length: 50, nullable: true)]
@@ -102,7 +102,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Assert\Country]
#[Assert\Length(max: 2)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'country')]
private ?string $country = null;
#[ORM\Column(name: 'currency', type: Types::STRING, length: 3, nullable: false)]
@@ -110,25 +110,25 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Assert\Currency]
#[Assert\Length(max: 3)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer'])]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'currency')]
private ?string $currency = self::DEFAULT_CURRENCY;
#[ORM\Column(name: 'phone', type: Types::STRING, length: 30, nullable: true)]
#[Assert\Length(max: 30)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Serializer\Groups(['Customer'])]
#[Exporter\Expose(label: 'phone')]
private ?string $phone = null;
#[ORM\Column(name: 'fax', type: Types::STRING, length: 30, nullable: true)]
#[Assert\Length(max: 30)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Serializer\Groups(['Customer'])]
#[Exporter\Expose(label: 'fax')]
private ?string $fax = null;
#[ORM\Column(name: 'mobile', type: Types::STRING, length: 30, nullable: true)]
#[Assert\Length(max: 30)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Serializer\Groups(['Customer'])]
#[Exporter\Expose(label: 'mobile')]
private ?string $mobile = null;
/**
@@ -143,7 +143,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[ORM\Column(name: 'homepage', type: Types::STRING, length: 100, nullable: true)]
#[Assert\Length(max: 100)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Serializer\Groups(['Customer'])]
#[Exporter\Expose(label: 'homepage')]
private ?string $homepage = null;
/**
@@ -154,7 +154,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Assert\Timezone]
#[Assert\Length(max: 64)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Serializer\Groups(['Customer'])]
#[Exporter\Expose(label: 'timezone')]
private ?string $timezone = null;
/**

View File

@@ -60,6 +60,11 @@ class ExportTemplate
return $this->id;
}
public function isNew(): bool
{
return $this->id === null;
}
public function setTitle(?string $title): void
{
$this->title = $title;

View File

@@ -96,7 +96,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
*/
#[ORM\Column(name: 'start', type: Types::DATETIME_MUTABLE, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Project'])]
#[Serializer\Groups(['Default'])]
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
#[Serializer\Accessor(getter: 'getStart')]
private ?\DateTime $start = null;
@@ -107,7 +107,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
*/
#[ORM\Column(name: 'end', type: Types::DATETIME_MUTABLE, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Project'])]
#[Serializer\Groups(['Default'])]
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
#[Serializer\Accessor(getter: 'getEnd')]
private ?\DateTime $end = null;

View File

@@ -106,7 +106,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
#[ORM\Column(name: 'avatar', type: Types::STRING, length: 255, nullable: true)]
#[Assert\Length(max: 255, groups: ['Profile'])]
#[Serializer\Expose]
#[Serializer\Groups(['User_Entity'])]
#[Serializer\Groups(['Default'])]
private ?string $avatar = null;
/**
* API token (password) for this user
@@ -965,7 +965,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return $this;
}
public function setLastLogin(\DateTime $time = null): User
public function setLastLogin(?\DateTime $time = null): User
{
$this->lastLogin = $time;

View File

@@ -30,9 +30,9 @@ class WorkingTime
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private ?User $user = null;
#[ORM\Column(name: 'date', type: Types::DATE_MUTABLE, nullable: false)]
#[ORM\Column(name: 'date', type: Types::DATE_IMMUTABLE, nullable: false)]
#[Assert\NotNull]
private \DateTimeInterface $date;
private \DateTimeImmutable $date;
#[ORM\Column(name: 'expected', type: Types::INTEGER, nullable: false)]
#[Assert\NotNull]
private int $expectedTime = 0;
@@ -46,7 +46,7 @@ class WorkingTime
#[Assert\NotNull]
private ?\DateTimeImmutable $approvedAt = null;
public function __construct(User $user, \DateTimeInterface $date)
public function __construct(User $user, \DateTimeImmutable $date)
{
$this->user = $user;
$this->date = $date;
@@ -62,7 +62,7 @@ class WorkingTime
return $this->user;
}
public function getDate(): \DateTimeInterface
public function getDate(): \DateTimeImmutable
{
return $this->date;
}

View File

@@ -88,6 +88,14 @@ class Kernel extends BaseKernel
}
$plugins = [];
$finder = new Finder();
$finder->ignoreUnreadableDirs()->directories()->name('*Bundle-*');
foreach ($finder->in($pluginsDir) as $bundleDir) {
throw new \Exception(
\sprintf('Bundle "%s" has invalid directory name. Remove the version number, see https://www.kimai.org/documentation/plugin-management.html.', $bundleDir->getRelativePathname())
);
}
$finder = new Finder();
$finder->ignoreUnreadableDirs()->directories()->name('*Bundle');
/** @var SplFileInfo $bundleDir */

View File

@@ -74,7 +74,7 @@ final class LdapAuthenticator implements AuthenticationEntryPointInterface, Inte
return false;
}
public function start(Request $request, AuthenticationException $authException = null): Response
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
if (!$this->authenticator instanceof AuthenticationEntryPointInterface) {
throw new NotAnEntryPointException(\sprintf('Decorated authenticator "%s" does not implement interface "%s".', get_debug_type($this->authenticator), AuthenticationEntryPointInterface::class));

View File

@@ -721,6 +721,7 @@ class ProjectStatisticService
$qb = clone $tplQb;
$qb->addSelect('MAX(t.date) as lastRecord');
$result = $qb->getQuery()->getScalarResult();
/** @var array{id: int, lastRecord: string, duration: int, rate: float} $row */
foreach ($result as $row) {
if ($row['lastRecord'] !== null) {
// might be the wrong timezone
@@ -736,6 +737,7 @@ class ProjectStatisticService
;
$result = $qb->getQuery()->getScalarResult();
/** @var array{id: int, lastRecord: string, duration: int, rate: float} $row */
foreach ($result as $row) {
$projectViews[$row['id']]->setDurationDay($row['duration'] ?? 0);
}
@@ -749,6 +751,7 @@ class ProjectStatisticService
;
$result = $qb->getQuery()->getScalarResult();
/** @var array{id: int, lastRecord: string, duration: int, rate: float} $row */
foreach ($result as $row) {
$projectViews[$row['id']]->setDurationWeek($row['duration']);
}
@@ -762,6 +765,7 @@ class ProjectStatisticService
;
$result = $qb->getQuery()->getScalarResult();
/** @var array{id: int, lastRecord: string, duration: int, rate: float} $row */
foreach ($result as $row) {
$projectViews[$row['id']]->setDurationMonth($row['duration']);
}

View File

@@ -116,7 +116,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
* Overwritten to fetch preferences when using the Profile controller actions.
* Depends on the query, some magic mechanisms like the ParamConverter will use this method to fetch the user.
*/
public function findOneBy(array $criteria, array $orderBy = null): ?object
public function findOneBy(array $criteria, ?array $orderBy = null): ?object
{
if (\count($criteria) === 1 && isset($criteria['username']) && \is_string($criteria['username'])) {
return $this->loadUserByIdentifier($criteria['username']);

View File

@@ -24,6 +24,10 @@ final class DefaultRounding implements RoundingInterface
return;
}
if ($record->getBegin() === null) {
return;
}
$timestamp = $record->getBegin()->getTimestamp();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
@@ -43,6 +47,10 @@ final class DefaultRounding implements RoundingInterface
return;
}
if ($record->getEnd() === null) {
return;
}
$timestamp = $record->getEnd()->getTimestamp();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;

View File

@@ -29,7 +29,7 @@ final class FileHelper
$this->dataDir = $directory;
}
public function getDataDirectory(string $subDirectory = null): string
public function getDataDirectory(?string $subDirectory = null): string
{
$directory = $this->dataDir . '/';

View File

@@ -1,7 +1,4 @@
{
"behat/transliterator": {
"version": "v1.2.0"
},
"composer/pcre": {
"version": "1.0.1"
},

View File

@@ -3,7 +3,7 @@
{% block main %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = {
'title': 'create'|trans,
'title': (template.isNew() ? 'create'|trans : 'edit'|trans),
'form': form,
'back': path('export_template_create')
} %}

View File

@@ -219,46 +219,44 @@
{% block duration_widget %}
<div class="duration-widget">
<div class="input-group">
{% if (form.vars.duration_presets is defined and form.vars.duration_presets is not empty) and (form.vars.disabled is same as (false)) %}
<div class="input-group">
{% if form.vars.icon is defined and form.vars.icon is not null %}
{% if form.vars.icon is defined and form.vars.icon is not null %}
<div class="input-group-text">
{{ icon(form.vars.icon) }}
</div>
{% endif %}
{{ block('form_widget_simple') }}
{% if form.vars.toggle is defined and form.vars.toggle %}
<div class="input-group-text">
{{ icon(form.vars.icon) }}
<a href="#" id="{{ form.vars.id }}_toggle" class="text-success">{{ icon('link') }}</a>
</div>
{% endif %}
{{ block('form_widget_simple') }}
{% if form.vars.toggle is defined and form.vars.toggle %}
<div class="input-group-text">
<a href="#" id="{{ form.vars.id }}_toggle" class="text-success">{{ icon('link') }}</a>
</div>
{% endif %}
<button type="button" class="input-group-text dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div class="dropdown-menu dropdown-menu-end pre-scrollable">
<div class="dropdown-menu-columns">
{% for index in [0, 1, 2, 3] %}
<div class="dropdown-menu-column" style="min-width: 4rem">
{% for value in form.vars.duration_presets %}
{% if loop.index0 % 4 == index %}
<a class="dropdown-item justify-content-center" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ value }}" data-event="change">{{ value }}</a>
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %}
<button type="button" class="input-group-text dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div class="dropdown-menu dropdown-menu-end pre-scrollable">
<div class="dropdown-menu-columns">
{% for index in [0, 1, 2, 3] %}
<div class="dropdown-menu-column" style="min-width: 4rem">
{% for value in form.vars.duration_presets %}
{% if loop.index0 % 4 == index %}
<a class="dropdown-item justify-content-center" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ value }}" data-event="change">{{ value }}</a>
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div>
</div>
{% else %}
{% if icon is defined and icon is not empty %}
<div class="input-group">
<div class="input-group-text">
{{ icon(icon) }}
</div>
{{ block('form_widget_simple') }}
<div class="input-group-text">
{{ icon(icon) }}
</div>
{{ block('form_widget_simple') }}
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
{% endif %}
</div>
</div>
{% endblock duration_widget %}

View File

@@ -68,7 +68,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
$data = [
'message' => 'Unauthorized',
'code' => 401
'code' => Response::HTTP_UNAUTHORIZED
];
self::assertEquals(
@@ -100,7 +100,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
]);
}
public function request(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], string $content = null): Crawler
public function request(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], ?string $content = null): Crawler
{
$server = ['HTTP_CONTENT_TYPE' => 'application/json', 'CONTENT_TYPE' => 'application/json'];
@@ -276,6 +276,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
$fieldName = implode('.', $parts);
}
self::assertIsString($fieldName);
self::assertArrayHasKey($fieldName, $data, \sprintf('Could not find validation error for field "%s" in list: %s', $fieldName, implode(', ', $failedFields)));
self::assertArrayHasKey('errors', $data[$fieldName], \sprintf('Field %s has no validation problem', $fieldName));
foreach ($messages as $i => $message) {
@@ -356,6 +357,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'enabled' => 'bool',
'apiToken' => 'bool',
'color' => '@string',
'avatar' => '@string',
'alias' => '@string',
'accountNumber' => '@string',
'initials' => '@string',
@@ -431,6 +433,9 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'color' => '@string',
'number' => '@string',
'comment' => '@string',
'currency' => 'string', // since 2.40.0
'country' => 'string', // since 2.40.0
'company' => '@string', // since 2.40.0
];
// if a list of customers is loaded
@@ -446,6 +451,13 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
'teams' => ['result' => 'array', 'type' => 'Team'],
'currency' => 'string', // since 1.10
'country' => 'string',
'company' => '@string',
'homepage' => '@string',
'fax' => '@string',
'mobile' => '@string',
'phone' => '@string',
'timezone' => 'string',
];
// if a customer is loaded explicitly
@@ -490,6 +502,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'orderNumber' => '@string',
'globalActivities' => 'bool',
'comment' => '@string',
'start' => '@datetime',
'end' => '@datetime',
];
// if a project is embedded in an expanded collection (here timesheet)
@@ -505,6 +519,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'orderNumber' => '@string',
'globalActivities' => 'bool',
'comment' => '@string',
'start' => '@datetime',
'end' => '@datetime',
];
// if a collection of projects is loaded

View File

@@ -407,7 +407,7 @@ class ActivityControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingName(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/activities/1/meta', ['value' => 'X'], [
'code' => 400,
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
@@ -415,7 +415,7 @@ class ActivityControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingValue(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/activities/1/meta', ['name' => 'X'], [
'code' => 400,
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
@@ -423,7 +423,7 @@ class ActivityControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingMetafield(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/activities/1/meta', ['name' => 'X', 'value' => 'Y'], [
'code' => 404,
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Not Found'
]);
}

View File

@@ -105,6 +105,7 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase
'/api/users/{id}',
'/api/users/me',
'/api/users/api-token/{id}',
'/api/users/{id}/preferences',
];
self::assertArrayHasKey('openapi', $json);

View File

@@ -390,7 +390,7 @@ class CustomerControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingName(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/customers/1/meta', ['value' => 'X'], [
'code' => 400,
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
@@ -398,7 +398,7 @@ class CustomerControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingValue(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/customers/1/meta', ['name' => 'X'], [
'code' => 400,
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
@@ -406,7 +406,7 @@ class CustomerControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingMetafield(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/customers/1/meta', ['name' => 'X', 'value' => 'Y'], [
'code' => 404,
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Not Found'
]);
}

View File

@@ -566,7 +566,7 @@ class ProjectControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingName(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/projects/1/meta', ['value' => 'X'], [
'code' => 400,
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
@@ -574,7 +574,7 @@ class ProjectControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingValue(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/projects/1/meta', ['name' => 'X'], [
'code' => 400,
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
@@ -582,7 +582,7 @@ class ProjectControllerTest extends APIControllerBaseTestCase
public function testMetaActionThrowsExceptionOnMissingMetafield(): void
{
$this->assertExceptionForPatchAction(User::ROLE_ADMIN, '/api/projects/1/meta', ['name' => 'X', 'value' => 'Y'], [
'code' => 404,
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Not Found'
]);
}

View File

@@ -19,6 +19,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -54,7 +55,7 @@ class ValidationFailedExceptionErrorHandlerTest extends TestCase
$validations = new ValidationFailedException($constraints, 'Uuups, that is broken');
$expected = [
'code' => '400',
'code' => Response::HTTP_BAD_REQUEST,
'message' => null,
'errors' => [
'children' => []
@@ -98,7 +99,7 @@ class ValidationFailedExceptionErrorHandlerTest extends TestCase
$context->setAttribute('exception', $validations);
$expected = [
'code' => '400',
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Uuups, that is broken',
'errors' => [
'children' => [

View File

@@ -263,7 +263,7 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->request($client, '/api/timesheets', 'GET', $query);
$this->assertApiException($client->getResponse(), ['code' => 404, 'message' => 'Not Found']);
$this->assertApiException($client->getResponse(), ['code' => Response::HTTP_NOT_FOUND, 'message' => 'Not Found']);
}
public function testGetCollectionWithSingleParamsQuery(): void
@@ -1434,7 +1434,7 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
$id = $timesheets[0]->getId();
$this->assertExceptionForMethod($client, '/api/timesheets/' . $id . '/meta', 'PATCH', ['value' => 'X'], [
'code' => 400,
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
@@ -1446,7 +1446,7 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
$id = $timesheets[0]->getId();
$this->assertExceptionForMethod($client, '/api/timesheets/' . $id . '/meta', 'PATCH', ['name' => 'X'], [
'code' => 404,
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Not Found'
]);
}
@@ -1458,7 +1458,7 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
$id = $timesheets[0]->getId();
$this->assertExceptionForMethod($client, '/api/timesheets/' . $id . '/meta', 'PATCH', ['name' => 'X', 'value' => 'Y'], [
'code' => 404,
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Not Found'
]);
}

View File

@@ -10,8 +10,11 @@
namespace App\Tests\API;
use App\Entity\User;
use App\Tests\Mocks\PrepareUserEventSubscriberMock;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Response;
#[Group('integration')]
class UserControllerTest extends APIControllerBaseTestCase
@@ -178,7 +181,7 @@ class UserControllerTest extends APIControllerBaseTestCase
'ROLE_ADMIN'
],
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$this->request($client, '/api/users', 'POST', [], (string) json_encode($data));
self::assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent();
@@ -211,7 +214,7 @@ class UserControllerTest extends APIControllerBaseTestCase
'ROLE_ADMIN'
],
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$this->request($client, '/api/users', 'POST', [], (string) json_encode($data));
$response = $client->getResponse();
self::assertEquals(400, $response->getStatusCode());
@@ -231,7 +234,7 @@ class UserControllerTest extends APIControllerBaseTestCase
'ABC',
],
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$this->request($client, '/api/users', 'POST', [], (string) json_encode($data));
$response = $client->getResponse();
self::assertEquals(400, $response->getStatusCode());
@@ -249,7 +252,7 @@ class UserControllerTest extends APIControllerBaseTestCase
'language' => 'ru',
'timezone' => 'Europe/Paris',
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$this->request($client, '/api/users', 'POST', [], (string) json_encode($data));
$response = $client->getResponse();
$this->assertApiResponseAccessDenied($response, 'Access denied.');
}
@@ -269,11 +272,12 @@ class UserControllerTest extends APIControllerBaseTestCase
'ROLE_ADMIN'
],
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$this->request($client, '/api/users', 'POST', [], (string) json_encode($data));
self::assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertFalse($result['enabled']);
$data = [
@@ -287,7 +291,7 @@ class UserControllerTest extends APIControllerBaseTestCase
];
$id = $result['id'];
self::assertIsNumeric($id);
$this->request($client, '/api/users/' . $id, 'PATCH', [], json_encode($data));
$this->request($client, '/api/users/' . $id, 'PATCH', [], (string) json_encode($data));
self::assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent();
@@ -312,7 +316,7 @@ class UserControllerTest extends APIControllerBaseTestCase
public function testPatchActionWithInvalidUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->request($client, '/api/users/1', 'PATCH', [], json_encode(['language' => 'hu']));
$this->request($client, '/api/users/1', 'PATCH', [], (string) json_encode(['language' => 'hu']));
$this->assertApiResponseAccessDenied($client->getResponse(), 'Not allowed to edit user');
}
@@ -330,10 +334,73 @@ class UserControllerTest extends APIControllerBaseTestCase
'ABC',
],
];
$this->request($client, '/api/users/1', 'PATCH', [], json_encode($data));
$this->request($client, '/api/users/1', 'PATCH', [], (string) json_encode($data));
$response = $client->getResponse();
self::assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['email', 'language', 'timezone', 'roles'], true);
}
// ------------------------------------- [USER PREFERENCES] -------------------------------------
public function testUpdateUserPreferenceThrowsNotFound(): void
{
$this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/users/42/preferences', []);
}
public function testUpdateUserPreferenceThrowsExceptionOnWrongStructure(): void
{
$this->assertExceptionForPatchAction(User::ROLE_SUPER_ADMIN, '/api/users/1/preferences', ['name' => 'X', 'value' => 'X'], [
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
public function testUpdateUserPreferenceThrowsExceptionOnMissingName(): void
{
$this->assertExceptionForPatchAction(User::ROLE_SUPER_ADMIN, '/api/users/1/preferences', [['value' => 'X']], [
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
public function testUpdateUserPreferenceThrowsExceptionOnMissingValue(): void
{
$this->assertExceptionForPatchAction(User::ROLE_SUPER_ADMIN, '/api/users/1/preferences', [['name' => 'X']], [
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Bad Request'
]);
}
public function testUpdateUserPreferenceThrowsExceptionOnMissingMetafield(): void
{
$this->assertExceptionForPatchAction(User::ROLE_SUPER_ADMIN, '/api/users/1/preferences', [['name' => 'X', 'value' => 'Y']], [
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Not Found'
]);
}
public function testUpdateUserPreference(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
/** @var EventDispatcher $dispatcher */
$dispatcher = static::getContainer()->get('event_dispatcher');
$dispatcher->addSubscriber(new PrepareUserEventSubscriberMock());
$data = [
[
'name' => 'metatestmock',
'value' => 'another,testing,bar'
]
];
$this->request($client, '/api/users/1/preferences', 'PATCH', [], (string) json_encode($data));
self::assertTrue($client->getResponse()->isSuccessful());
$em = $this->getEntityManager();
/** @var User $user */
$user = $em->getRepository(User::class)->find(1);
self::assertEquals('another,testing,bar', $user->getPreferenceValue('metatestmock'));
self::assertEquals('another,testing,bar', $user->getPreferenceValue('metatestmock'));
}
}

View File

@@ -33,7 +33,9 @@ class BundleInstallerCommandTest extends KernelTestCase
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$this->application->add(new $className());
$command = new $className();
self::assertInstanceOf(AbstractBundleInstallerCommand::class, $command);
$this->application->add($command);
return $this->application->find('kimai:bundle:test:install');
}

View File

@@ -12,9 +12,6 @@ namespace App\Tests\Configuration;
use App\Configuration\ConfigLoaderInterface;
use App\Entity\Configuration;
/**
* @covers \App\Configuration\SystemConfiguration
*/
class TestConfigLoader implements ConfigLoaderInterface
{
/**

View File

@@ -169,12 +169,12 @@ abstract class AbstractControllerBaseTestCase extends WebTestCase
return $url;
}
public function request(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], string $content = null): Crawler
public function request(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], ?string $content = null): Crawler
{
return $client->request($method, $this->createUrl($url), $parameters, [], [], $content);
}
public function requestPure(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], string $content = null): Crawler
public function requestPure(HttpKernelBrowser $client, string $url, string $method = 'GET', array $parameters = [], ?string $content = null): Crawler
{
return $client->request($method, $url, $parameters, [], [], $content);
}
@@ -381,7 +381,7 @@ abstract class AbstractControllerBaseTestCase extends WebTestCase
* @param HttpKernelBrowser $client
* @param string|null $message
*/
protected function assertHasFlashSuccess(HttpKernelBrowser $client, string $message = null): void
protected function assertHasFlashSuccess(HttpKernelBrowser $client, ?string $message = null): void
{
$this->assertHasFlashMessage($client, 'success', $message);
}
@@ -390,12 +390,12 @@ abstract class AbstractControllerBaseTestCase extends WebTestCase
* @param HttpKernelBrowser $client
* @param string|null $message
*/
protected function assertHasFlashError(HttpKernelBrowser $client, string $message = null): void
protected function assertHasFlashError(HttpKernelBrowser $client, ?string $message = null): void
{
$this->assertHasFlashMessage($client, 'error', $message);
}
private function assertHasFlashMessage(HttpKernelBrowser $client, string $type, string $message = null): void
private function assertHasFlashMessage(HttpKernelBrowser $client, string $type, ?string $message = null): void
{
$content = $client->getResponse()->getContent();
self::assertStringContainsString('ALERT.' . $type . '(\'', $content, 'Could not find flash ' . $type . ' message');

View File

@@ -45,4 +45,27 @@ abstract class AbstractEntityTestCase extends TestCase
self::assertEquals('Unknown budget type: foo', $e->getMessage());
}
}
public function assertCloneResetsId(object $sut): void
{
if (!property_exists($sut, 'id')) {
$this->fail('Entity does not have an ID property');
}
if (!method_exists($sut, 'getId')) {
$this->fail('Entity does not have a getId() method');
}
self::assertNull($sut->getId());
$obj = new \ReflectionObject($sut);
$prop = $obj->getProperty('id');
$prop->setAccessible(true);
$prop->setValue($sut, 123);
self::assertEquals(123, $sut->getId());
$clone = clone $sut;
self::assertNull($clone->getId());
$prop->setValue($sut, null);
$prop->setAccessible(false);
}
}

View File

@@ -48,4 +48,11 @@ class AccessTokenTest extends AbstractEntityTestCase
$sut->setExpiresAt(new \DateTimeImmutable('-1 day'));
self::assertFalse($sut->isValid());
}
public function testClone(): void
{
$user = new User();
$sut = new AccessToken($user, 'foo');
$this->assertCloneResetsId($sut);
}
}

View File

@@ -11,13 +11,12 @@ namespace App\Tests\Entity;
use App\Entity\Activity;
use App\Entity\ActivityRate;
use App\Entity\Rate;
use App\Entity\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Entity\Rate
*/
#[CoversClass(Rate::class)]
#[CoversClass(ActivityRate::class)]
class ActivityRateTest extends TestCase
{

View File

@@ -175,6 +175,8 @@ class ActivityTest extends AbstractEntityTestCase
$sut->setName('activity1111');
$sut->setComment('DE-0123456789');
$this->assertCloneResetsId($sut);
$project = new Project();
$project->setName('foo');
$project->setOrderNumber('1234567890');

View File

@@ -12,10 +12,9 @@ namespace App\Tests\Entity;
use App\Entity\Bookmark;
use App\Entity\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(Bookmark::class)]
class BookmarkTest extends TestCase
class BookmarkTest extends AbstractEntityTestCase
{
public function testDefaultValues(): void
{
@@ -48,4 +47,10 @@ class BookmarkTest extends TestCase
self::assertEquals('sdsdsd', $sut2->getType());
self::assertEquals('foo-bar', $sut2->getName());
}
public function testClone(): void
{
$sut = new Bookmark();
$this->assertCloneResetsId($sut);
}
}

View File

@@ -31,5 +31,14 @@ class ConfigurationTest extends TestCase
self::assertEquals('foo-bar', $sut->getName());
self::assertInstanceOf(Configuration::class, $sut->setValue('hello world'));
self::assertEquals('hello world', $sut->getValue());
self::assertInstanceOf(Configuration::class, $sut->setValue(true));
self::assertEquals('1', $sut->getValue());
self::assertInstanceOf(Configuration::class, $sut->setValue(null));
self::assertNull($sut->getValue());
self::assertInstanceOf(Configuration::class, $sut->setValue(false));
self::assertEquals('0', $sut->getValue());
}
}

View File

@@ -9,13 +9,12 @@
namespace App\Tests\Entity;
use App\Entity\CommentTableTypeTrait;
use App\Entity\Customer;
use App\Entity\CustomerComment;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* @covers \App\Entity\CommentTableTypeTrait
*/
#[CoversClass(CommentTableTypeTrait::class)]
#[CoversClass(CustomerComment::class)]
class CustomerCommentTest extends AbstractCommentEntityTestCase
{

View File

@@ -11,13 +11,12 @@ namespace App\Tests\Entity;
use App\Entity\Customer;
use App\Entity\CustomerRate;
use App\Entity\Rate;
use App\Entity\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Entity\Rate
*/
#[CoversClass(Rate::class)]
#[CoversClass(CustomerRate::class)]
class CustomerRateTest extends TestCase
{

View File

@@ -12,6 +12,7 @@ namespace App\Tests\Entity;
use App\Constants;
use App\Entity\Customer;
use App\Entity\CustomerMeta;
use App\Entity\InvoiceTemplate;
use App\Entity\Team;
use App\Export\Spreadsheet\ColumnDefinition;
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
@@ -51,6 +52,26 @@ class CustomerTest extends AbstractEntityTestCase
self::assertNull($sut->getMetaField('foo'));
self::assertInstanceOf(Collection::class, $sut->getTeams());
self::assertEquals(0, $sut->getTeams()->count());
self::assertTrue($sut->isNew());
self::assertNull($sut->getInvoiceText());
self::assertNull($sut->getInvoiceTemplate());
}
public function testInvoiceText(): void
{
$sut = new Customer('foo');
self::assertNull($sut->getInvoiceText());
$sut->setInvoiceText('Some fancy long text to explain that tax should be handled by the receiving party');
self::assertEquals('Some fancy long text to explain that tax should be handled by the receiving party', $sut->getInvoiceText());
}
public function testInvoiceTemplate(): void
{
$tpl = new InvoiceTemplate();
$sut = new Customer('foo');
self::assertNull($sut->getInvoiceTemplate());
$sut->setInvoiceTemplate($tpl);
self::assertSame($tpl, $sut->getInvoiceTemplate());
}
public function testBudgets(): void
@@ -221,6 +242,9 @@ class CustomerTest extends AbstractEntityTestCase
public function testClone(): void
{
$sut = new Customer('mycustomer');
$this->assertCloneResetsId($sut);
$sut->setVatId('DE-0123456789');
$sut->setTimeBudget(123456);
$sut->setBudget(1234.56);

View File

@@ -19,6 +19,7 @@ class ExportTemplateTest extends AbstractEntityTestCase
{
$sut = new ExportTemplate();
self::assertNull($sut->getId());
self::assertTrue($sut->isNew());
self::assertNull($sut->getTitle());
self::assertEquals('csv', $sut->getRenderer());
self::assertNull($sut->getLanguage());
@@ -115,6 +116,7 @@ class ExportTemplateTest extends AbstractEntityTestCase
$p->setAccessible(true);
$p->setValue($sut, 13);
self::assertEquals(13, $sut->getId());
self::assertFalse($sut->isNew());
$sut2 = clone $sut;
self::assertNull($sut2->getId());

View File

@@ -34,6 +34,7 @@ class InvoiceTemplateTest extends TestCase
self::assertEquals('default', $sut->getNumberGenerator());
self::assertEquals('default', $sut->getRenderer());
self::assertEquals(30, $sut->getDueDays());
self::assertTrue($sut->isDecimalDuration()); // @phpstan-ignore method.deprecated
}
public function testSetNullForOptionalValues(): void

View File

@@ -29,10 +29,9 @@ use App\Repository\Query\InvoiceQuery;
use App\Tests\Invoice\DebugFormatter;
use App\Tests\Mocks\InvoiceModelFactoryFactory;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(Invoice::class)]
class InvoiceTest extends TestCase
class InvoiceTest extends AbstractEntityTestCase
{
public function testDefaultValues(): void
{
@@ -219,6 +218,8 @@ class InvoiceTest extends TestCase
public function testClone(): void
{
$sut = new Invoice();
$this->assertCloneResetsId($sut);
$sut->setComment('foo kajsdhgf aksjdhfg');
$sut->setFilename('1234567890');

View File

@@ -9,13 +9,12 @@
namespace App\Tests\Entity;
use App\Entity\CommentTableTypeTrait;
use App\Entity\Project;
use App\Entity\ProjectComment;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* @covers \App\Entity\CommentTableTypeTrait
*/
#[CoversClass(CommentTableTypeTrait::class)]
#[CoversClass(ProjectComment::class)]
class ProjectCommentTest extends AbstractCommentEntityTestCase
{

View File

@@ -11,13 +11,12 @@ namespace App\Tests\Entity;
use App\Entity\Project;
use App\Entity\ProjectRate;
use App\Entity\Rate;
use App\Entity\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Entity\Rate
*/
#[CoversClass(Rate::class)]
#[CoversClass(ProjectRate::class)]
class ProjectRateTest extends TestCase
{

View File

@@ -25,7 +25,7 @@ class ActivityTestMetaFieldSubscriberMock implements EventSubscriberInterface
];
}
public function loadMeta(ActivityMetaDefinitionEvent $event)
public function loadMeta(ActivityMetaDefinitionEvent $event): void
{
$definition = (new ActivityMeta())
->setName('metatestmock')

View File

@@ -25,7 +25,7 @@ class CustomerTestMetaFieldSubscriberMock implements EventSubscriberInterface
];
}
public function loadMeta(CustomerMetaDefinitionEvent $event)
public function loadMeta(CustomerMetaDefinitionEvent $event): void
{
$definition = (new CustomerMeta())
->setName('metatestmock')

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Mocks;
use App\Entity\UserPreference;
use App\Event\PrepareUserEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\Length;
class PrepareUserEventSubscriberMock implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
PrepareUserEvent::class => ['prepareUserEvent', 200],
];
}
public function prepareUserEvent(PrepareUserEvent $event): void
{
$definition = (new UserPreference('metatestmock'))
->setType(TextType::class)
->addConstraint(new Length(['max' => 200]))
->setEnabled(true);
$event->getUser()->addPreference($definition);
$definition = (new UserPreference('foobar'))
->setType(IntegerType::class)
->setEnabled(false);
$event->getUser()->addPreference($definition);
}
}

View File

@@ -25,7 +25,7 @@ class ProjectTestMetaFieldSubscriberMock implements EventSubscriberInterface
];
}
public function loadMeta(ProjectMetaDefinitionEvent $event)
public function loadMeta(ProjectMetaDefinitionEvent $event): void
{
$definition = (new ProjectMeta())
->setName('metatestmock')

View File

@@ -25,7 +25,7 @@ class TimesheetTestMetaFieldSubscriberMock implements EventSubscriberInterface
];
}
public function loadMeta(TimesheetMetaDefinitionEvent $event)
public function loadMeta(TimesheetMetaDefinitionEvent $event): void
{
$definition = (new TimesheetMeta())
->setName('metatestmock')

View File

@@ -129,7 +129,7 @@ parameters:
-
message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
count: 3
count: 1
path: API/APIControllerBaseTestCase.php
-
@@ -301,26 +301,6 @@ parameters:
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Cannot access offset 'enabled' on mixed\\.$#"
count: 1
path: API/UserControllerTest.php
-
message: "#^Cannot access offset 'id' on mixed\\.$#"
count: 1
path: API/UserControllerTest.php
-
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTestCase\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
count: 8
path: API/UserControllerTest.php
-
message: "#^Parameter \\#1 \\$command of method Symfony\\\\Bundle\\\\FrameworkBundle\\\\Console\\\\Application\\:\\:add\\(\\) expects Symfony\\\\Component\\\\Console\\\\Command\\\\Command, object given\\.$#"
count: 1
path: Command/BundleInstallerCommandTest.php
-
message: "#^Parameter \\#1 \\$hashedPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\PasswordHasherInterface\\:\\:verify\\(\\) expects string, string\\|null given\\.$#"
count: 1
@@ -1800,27 +1780,11 @@ parameters:
message: "#^Parameter \\#2 \\$type of class PHPUnit\\\\Framework\\\\MockObject\\\\MockBuilder constructor expects class\\-string\\<TMockedClass\\>, string given\\.$#"
count: 1
path: Mocks/AbstractMockFactory.php
-
message: "#^Method App\\\\Tests\\\\Mocks\\\\ActivityTestMetaFieldSubscriberMock\\:\\:loadMeta\\(\\) has no return type specified\\.$#"
count: 1
path: Mocks/ActivityTestMetaFieldSubscriberMock.php
-
message: "#^Method App\\\\Tests\\\\Mocks\\\\CustomerTestMetaFieldSubscriberMock\\:\\:loadMeta\\(\\) has no return type specified\\.$#"
count: 1
path: Mocks/CustomerTestMetaFieldSubscriberMock.php
-
message: "#^Parameter \\#1 \\$dataDir of class App\\\\Utils\\\\FileHelper constructor expects string, string\\|false given\\.$#"
count: 1
path: Mocks/FileHelperFactory.php
-
message: "#^Method App\\\\Tests\\\\Mocks\\\\ProjectTestMetaFieldSubscriberMock\\:\\:loadMeta\\(\\) has no return type specified\\.$#"
count: 1
path: Mocks/ProjectTestMetaFieldSubscriberMock.php
-
message: "#^Method App\\\\Tests\\\\Mocks\\\\RoundingServiceFactory\\:\\:create\\(\\) has parameter \\$rules with no value type specified in iterable type array\\.$#"
count: 1
@@ -1831,11 +1795,6 @@ parameters:
count: 1
path: Mocks/Saml/SamlAuthFactoryFactory.php
-
message: "#^Method App\\\\Tests\\\\Mocks\\\\TimesheetTestMetaFieldSubscriberMock\\:\\:loadMeta\\(\\) has no return type specified\\.$#"
count: 1
path: Mocks/TimesheetTestMetaFieldSubscriberMock.php
-
message: "#^Method App\\\\Tests\\\\Mocks\\\\TrackingModeServiceFactory\\:\\:create\\(\\) has parameter \\$modes with no value type specified in iterable type array\\.$#"
count: 1