From 7937fa281a4831737e3264f53466a7e4b4ce9768 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Fri, 26 Sep 2025 10:49:06 +0200 Subject: [PATCH] Release 2.40.0 (#5621) --- composer.lock | 333 ++++++++---------- phpstan.neon | 60 ---- src/API/NotFoundException.php | 2 +- src/API/UserController.php | 43 +++ src/Activity/ActivityStatisticService.php | 2 +- src/Command/TranslationCommand.php | 4 +- src/Constants.php | 4 +- src/Controller/ExportController.php | 3 +- src/DataFixtures/UserFixtures.php | 4 +- src/Entity/Customer.php | 16 +- src/Entity/ExportTemplate.php | 5 + src/Entity/Project.php | 4 +- src/Entity/User.php | 4 +- src/Entity/WorkingTime.php | 8 +- src/Kernel.php | 8 + src/Ldap/LdapAuthenticator.php | 2 +- src/Project/ProjectStatisticService.php | 4 + src/Repository/UserRepository.php | 2 +- src/Timesheet/Rounding/DefaultRounding.php | 8 + src/Utils/FileHelper.php | 2 +- symfony.lock | 3 - templates/export/template.html.twig | 2 +- templates/form/blocks.html.twig | 54 ++- tests/API/APIControllerBaseTestCase.php | 20 +- tests/API/ActivityControllerTest.php | 6 +- tests/API/ApiDocControllerTest.php | 1 + tests/API/CustomerControllerTest.php | 6 +- tests/API/ProjectControllerTest.php | 6 +- ...idationFailedExceptionErrorHandlerTest.php | 5 +- tests/API/TimesheetControllerTest.php | 8 +- tests/API/UserControllerTest.php | 83 ++++- tests/Command/BundleInstallerCommandTest.php | 4 +- tests/Configuration/TestConfigLoader.php | 3 - .../AbstractControllerBaseTestCase.php | 10 +- tests/Entity/AbstractEntityTestCase.php | 23 ++ tests/Entity/AccessTokenTest.php | 7 + tests/Entity/ActivityRateTest.php | 5 +- tests/Entity/ActivityTest.php | 2 + tests/Entity/BookmarkTest.php | 9 +- tests/Entity/ConfigurationTest.php | 9 + tests/Entity/CustomerCommentTest.php | 5 +- tests/Entity/CustomerRateTest.php | 5 +- tests/Entity/CustomerTest.php | 24 ++ tests/Entity/ExportTemplateTest.php | 2 + tests/Entity/InvoiceTemplateTest.php | 1 + tests/Entity/InvoiceTest.php | 5 +- tests/Entity/ProjectCommentTest.php | 5 +- tests/Entity/ProjectRateTest.php | 5 +- .../ActivityTestMetaFieldSubscriberMock.php | 2 +- .../CustomerTestMetaFieldSubscriberMock.php | 2 +- .../Mocks/PrepareUserEventSubscriberMock.php | 43 +++ .../ProjectTestMetaFieldSubscriberMock.php | 2 +- .../TimesheetTestMetaFieldSubscriberMock.php | 2 +- tests/phpstan.neon | 43 +-- 54 files changed, 531 insertions(+), 399 deletions(-) create mode 100644 tests/Mocks/PrepareUserEventSubscriberMock.php diff --git a/composer.lock b/composer.lock index 571d0ab6..f17b68a4 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/phpstan.neon b/phpstan.neon index d82fec5b..f8769f24 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -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\\\\|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 diff --git a/src/API/NotFoundException.php b/src/API/NotFoundException.php index 03b0c1ea..85ae40d7 100644 --- a/src/API/NotFoundException.php +++ b/src/API/NotFoundException.php @@ -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); } diff --git a/src/API/UserController.php b/src/API/UserController.php index 3d8b8593..ab6a9083 100644 --- a/src/API/UserController.php +++ b/src/API/UserController.php @@ -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); + } } diff --git a/src/Activity/ActivityStatisticService.php b/src/Activity/ActivityStatisticService.php index 21414f22..b41783d6 100644 --- a/src/Activity/ActivityStatisticService.php +++ b/src/Activity/ActivityStatisticService.php @@ -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 diff --git a/src/Command/TranslationCommand.php b/src/Command/TranslationCommand.php index f4bf375c..37c890c4 100644 --- a/src/Command/TranslationCommand.php +++ b/src/Command/TranslationCommand.php @@ -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; } diff --git a/src/Constants.php b/src/Constants.php index 88bb146b..cddb5ffd 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -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 */ diff --git a/src/Controller/ExportController.php b/src/Controller/ExportController.php index 396fbae6..2c8d98bb 100644 --- a/src/Controller/ExportController.php +++ b/src/Controller/ExportController.php @@ -239,7 +239,8 @@ final class ExportController extends AbstractController } return $this->render('export/template.html.twig', [ - 'form' => $form->createView() + 'form' => $form->createView(), + 'template' => $exportTemplate, ]); } } diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php index c60f685a..90b7caf1 100644 --- a/src/DataFixtures/UserFixtures.php +++ b/src/DataFixtures/UserFixtures.php @@ -74,11 +74,9 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface } /** - * @param User $user - * @param string|null $timezone * @return array */ - private function getUserPreferences(User $user, string $timezone = null): array + private function getUserPreferences(User $user, ?string $timezone = null): array { $preferences = []; diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 98f141da..30c06e45 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -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; /** diff --git a/src/Entity/ExportTemplate.php b/src/Entity/ExportTemplate.php index 96220a16..ff1c2f09 100644 --- a/src/Entity/ExportTemplate.php +++ b/src/Entity/ExportTemplate.php @@ -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; diff --git a/src/Entity/Project.php b/src/Entity/Project.php index d770bbd0..ceeb8f12 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -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; diff --git a/src/Entity/User.php b/src/Entity/User.php index 5cd5fc4a..77fc0b7e 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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; diff --git a/src/Entity/WorkingTime.php b/src/Entity/WorkingTime.php index b5df3ebd..00718b90 100644 --- a/src/Entity/WorkingTime.php +++ b/src/Entity/WorkingTime.php @@ -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; } diff --git a/src/Kernel.php b/src/Kernel.php index 9293f2b3..83e96d06 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -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 */ diff --git a/src/Ldap/LdapAuthenticator.php b/src/Ldap/LdapAuthenticator.php index 1dce3611..a84e6731 100644 --- a/src/Ldap/LdapAuthenticator.php +++ b/src/Ldap/LdapAuthenticator.php @@ -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)); diff --git a/src/Project/ProjectStatisticService.php b/src/Project/ProjectStatisticService.php index bc88546d..ce353369 100644 --- a/src/Project/ProjectStatisticService.php +++ b/src/Project/ProjectStatisticService.php @@ -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']); } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index c55110b3..0fbcebdc 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -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']); diff --git a/src/Timesheet/Rounding/DefaultRounding.php b/src/Timesheet/Rounding/DefaultRounding.php index 022fa18f..572785c4 100644 --- a/src/Timesheet/Rounding/DefaultRounding.php +++ b/src/Timesheet/Rounding/DefaultRounding.php @@ -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; diff --git a/src/Utils/FileHelper.php b/src/Utils/FileHelper.php index 9b6898a2..f7f6220d 100644 --- a/src/Utils/FileHelper.php +++ b/src/Utils/FileHelper.php @@ -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 . '/'; diff --git a/symfony.lock b/symfony.lock index 5645c1ab..9f35a6aa 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,7 +1,4 @@ { - "behat/transliterator": { - "version": "v1.2.0" - }, "composer/pcre": { "version": "1.0.1" }, diff --git a/templates/export/template.html.twig b/templates/export/template.html.twig index ebf2b30c..722acc4a 100644 --- a/templates/export/template.html.twig +++ b/templates/export/template.html.twig @@ -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') } %} diff --git a/templates/form/blocks.html.twig b/templates/form/blocks.html.twig index a2473262..a6d0352e 100644 --- a/templates/form/blocks.html.twig +++ b/templates/form/blocks.html.twig @@ -219,46 +219,44 @@ {% block duration_widget %}
+
{% if (form.vars.duration_presets is defined and form.vars.duration_presets is not empty) and (form.vars.disabled is same as (false)) %} -
- {% 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 %} +
+ {{ icon(form.vars.icon) }} +
+ {% endif %} + {{ block('form_widget_simple') }} + {% if form.vars.toggle is defined and form.vars.toggle %}
- {{ icon(form.vars.icon) }} + {{ icon('link') }}
- {% endif %} - {{ block('form_widget_simple') }} - {% if form.vars.toggle is defined and form.vars.toggle %} - - {% endif %} - - {% endblock duration_widget %} diff --git a/tests/API/APIControllerBaseTestCase.php b/tests/API/APIControllerBaseTestCase.php index 3e04af8f..b730486d 100644 --- a/tests/API/APIControllerBaseTestCase.php +++ b/tests/API/APIControllerBaseTestCase.php @@ -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 diff --git a/tests/API/ActivityControllerTest.php b/tests/API/ActivityControllerTest.php index a7f3f742..86351795 100644 --- a/tests/API/ActivityControllerTest.php +++ b/tests/API/ActivityControllerTest.php @@ -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' ]); } diff --git a/tests/API/ApiDocControllerTest.php b/tests/API/ApiDocControllerTest.php index 30a2cb2e..5b4a5b73 100644 --- a/tests/API/ApiDocControllerTest.php +++ b/tests/API/ApiDocControllerTest.php @@ -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); diff --git a/tests/API/CustomerControllerTest.php b/tests/API/CustomerControllerTest.php index 2535f40c..e66f9418 100644 --- a/tests/API/CustomerControllerTest.php +++ b/tests/API/CustomerControllerTest.php @@ -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' ]); } diff --git a/tests/API/ProjectControllerTest.php b/tests/API/ProjectControllerTest.php index 15eec1e8..ab06547b 100644 --- a/tests/API/ProjectControllerTest.php +++ b/tests/API/ProjectControllerTest.php @@ -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' ]); } diff --git a/tests/API/Serializer/ValidationFailedExceptionErrorHandlerTest.php b/tests/API/Serializer/ValidationFailedExceptionErrorHandlerTest.php index 519abe3e..176719b9 100644 --- a/tests/API/Serializer/ValidationFailedExceptionErrorHandlerTest.php +++ b/tests/API/Serializer/ValidationFailedExceptionErrorHandlerTest.php @@ -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' => [ diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php index 13028a3a..03803653 100644 --- a/tests/API/TimesheetControllerTest.php +++ b/tests/API/TimesheetControllerTest.php @@ -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' ]); } diff --git a/tests/API/UserControllerTest.php b/tests/API/UserControllerTest.php index 1817d244..9d63bcbb 100644 --- a/tests/API/UserControllerTest.php +++ b/tests/API/UserControllerTest.php @@ -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')); + } } diff --git a/tests/Command/BundleInstallerCommandTest.php b/tests/Command/BundleInstallerCommandTest.php index 64441f7d..ae991fad 100644 --- a/tests/Command/BundleInstallerCommandTest.php +++ b/tests/Command/BundleInstallerCommandTest.php @@ -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'); } diff --git a/tests/Configuration/TestConfigLoader.php b/tests/Configuration/TestConfigLoader.php index 17490483..338f8340 100644 --- a/tests/Configuration/TestConfigLoader.php +++ b/tests/Configuration/TestConfigLoader.php @@ -12,9 +12,6 @@ namespace App\Tests\Configuration; use App\Configuration\ConfigLoaderInterface; use App\Entity\Configuration; -/** - * @covers \App\Configuration\SystemConfiguration - */ class TestConfigLoader implements ConfigLoaderInterface { /** diff --git a/tests/Controller/AbstractControllerBaseTestCase.php b/tests/Controller/AbstractControllerBaseTestCase.php index f944a9fd..829e2306 100644 --- a/tests/Controller/AbstractControllerBaseTestCase.php +++ b/tests/Controller/AbstractControllerBaseTestCase.php @@ -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'); diff --git a/tests/Entity/AbstractEntityTestCase.php b/tests/Entity/AbstractEntityTestCase.php index 17eb16bb..04e5e45e 100644 --- a/tests/Entity/AbstractEntityTestCase.php +++ b/tests/Entity/AbstractEntityTestCase.php @@ -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); + } } diff --git a/tests/Entity/AccessTokenTest.php b/tests/Entity/AccessTokenTest.php index 7a90470c..ff52a84a 100644 --- a/tests/Entity/AccessTokenTest.php +++ b/tests/Entity/AccessTokenTest.php @@ -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); + } } diff --git a/tests/Entity/ActivityRateTest.php b/tests/Entity/ActivityRateTest.php index 26a09a3a..6bd2dee8 100644 --- a/tests/Entity/ActivityRateTest.php +++ b/tests/Entity/ActivityRateTest.php @@ -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 { diff --git a/tests/Entity/ActivityTest.php b/tests/Entity/ActivityTest.php index 74744a26..24925b5d 100644 --- a/tests/Entity/ActivityTest.php +++ b/tests/Entity/ActivityTest.php @@ -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'); diff --git a/tests/Entity/BookmarkTest.php b/tests/Entity/BookmarkTest.php index a29d4563..58d88b9b 100644 --- a/tests/Entity/BookmarkTest.php +++ b/tests/Entity/BookmarkTest.php @@ -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); + } } diff --git a/tests/Entity/ConfigurationTest.php b/tests/Entity/ConfigurationTest.php index 1f1da24d..a1bea872 100644 --- a/tests/Entity/ConfigurationTest.php +++ b/tests/Entity/ConfigurationTest.php @@ -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()); } } diff --git a/tests/Entity/CustomerCommentTest.php b/tests/Entity/CustomerCommentTest.php index 057503f9..5c3a231d 100644 --- a/tests/Entity/CustomerCommentTest.php +++ b/tests/Entity/CustomerCommentTest.php @@ -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 { diff --git a/tests/Entity/CustomerRateTest.php b/tests/Entity/CustomerRateTest.php index 140cb95d..725088f3 100644 --- a/tests/Entity/CustomerRateTest.php +++ b/tests/Entity/CustomerRateTest.php @@ -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 { diff --git a/tests/Entity/CustomerTest.php b/tests/Entity/CustomerTest.php index b1072907..892bb96d 100644 --- a/tests/Entity/CustomerTest.php +++ b/tests/Entity/CustomerTest.php @@ -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); diff --git a/tests/Entity/ExportTemplateTest.php b/tests/Entity/ExportTemplateTest.php index 073a623d..39229a81 100644 --- a/tests/Entity/ExportTemplateTest.php +++ b/tests/Entity/ExportTemplateTest.php @@ -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()); diff --git a/tests/Entity/InvoiceTemplateTest.php b/tests/Entity/InvoiceTemplateTest.php index 79651d40..7130bd67 100644 --- a/tests/Entity/InvoiceTemplateTest.php +++ b/tests/Entity/InvoiceTemplateTest.php @@ -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 diff --git a/tests/Entity/InvoiceTest.php b/tests/Entity/InvoiceTest.php index 30c1c165..98ce8d17 100644 --- a/tests/Entity/InvoiceTest.php +++ b/tests/Entity/InvoiceTest.php @@ -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'); diff --git a/tests/Entity/ProjectCommentTest.php b/tests/Entity/ProjectCommentTest.php index 7c9207ad..db7638a8 100644 --- a/tests/Entity/ProjectCommentTest.php +++ b/tests/Entity/ProjectCommentTest.php @@ -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 { diff --git a/tests/Entity/ProjectRateTest.php b/tests/Entity/ProjectRateTest.php index ec554bc4..02608c25 100644 --- a/tests/Entity/ProjectRateTest.php +++ b/tests/Entity/ProjectRateTest.php @@ -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 { diff --git a/tests/Mocks/ActivityTestMetaFieldSubscriberMock.php b/tests/Mocks/ActivityTestMetaFieldSubscriberMock.php index 91101f04..b9f790ec 100644 --- a/tests/Mocks/ActivityTestMetaFieldSubscriberMock.php +++ b/tests/Mocks/ActivityTestMetaFieldSubscriberMock.php @@ -25,7 +25,7 @@ class ActivityTestMetaFieldSubscriberMock implements EventSubscriberInterface ]; } - public function loadMeta(ActivityMetaDefinitionEvent $event) + public function loadMeta(ActivityMetaDefinitionEvent $event): void { $definition = (new ActivityMeta()) ->setName('metatestmock') diff --git a/tests/Mocks/CustomerTestMetaFieldSubscriberMock.php b/tests/Mocks/CustomerTestMetaFieldSubscriberMock.php index 1f18e8d6..848b42b0 100644 --- a/tests/Mocks/CustomerTestMetaFieldSubscriberMock.php +++ b/tests/Mocks/CustomerTestMetaFieldSubscriberMock.php @@ -25,7 +25,7 @@ class CustomerTestMetaFieldSubscriberMock implements EventSubscriberInterface ]; } - public function loadMeta(CustomerMetaDefinitionEvent $event) + public function loadMeta(CustomerMetaDefinitionEvent $event): void { $definition = (new CustomerMeta()) ->setName('metatestmock') diff --git a/tests/Mocks/PrepareUserEventSubscriberMock.php b/tests/Mocks/PrepareUserEventSubscriberMock.php new file mode 100644 index 00000000..5295ffce --- /dev/null +++ b/tests/Mocks/PrepareUserEventSubscriberMock.php @@ -0,0 +1,43 @@ + ['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); + } +} diff --git a/tests/Mocks/ProjectTestMetaFieldSubscriberMock.php b/tests/Mocks/ProjectTestMetaFieldSubscriberMock.php index 7684d157..e55f232b 100644 --- a/tests/Mocks/ProjectTestMetaFieldSubscriberMock.php +++ b/tests/Mocks/ProjectTestMetaFieldSubscriberMock.php @@ -25,7 +25,7 @@ class ProjectTestMetaFieldSubscriberMock implements EventSubscriberInterface ]; } - public function loadMeta(ProjectMetaDefinitionEvent $event) + public function loadMeta(ProjectMetaDefinitionEvent $event): void { $definition = (new ProjectMeta()) ->setName('metatestmock') diff --git a/tests/Mocks/TimesheetTestMetaFieldSubscriberMock.php b/tests/Mocks/TimesheetTestMetaFieldSubscriberMock.php index 0571f4cd..76156397 100644 --- a/tests/Mocks/TimesheetTestMetaFieldSubscriberMock.php +++ b/tests/Mocks/TimesheetTestMetaFieldSubscriberMock.php @@ -25,7 +25,7 @@ class TimesheetTestMetaFieldSubscriberMock implements EventSubscriberInterface ]; } - public function loadMeta(TimesheetMetaDefinitionEvent $event) + public function loadMeta(TimesheetMetaDefinitionEvent $event): void { $definition = (new TimesheetMeta()) ->setName('metatestmock') diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 15c61b5e..776b0d87 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -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\\, 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