From 2e6b700b43961f2d700be83842a5d1aff575368d Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sun, 6 Apr 2025 09:53:48 +0200 Subject: [PATCH] Release 2.32 (#5411) * bump packages * dynamic invoice options * make sure that invoice previews can be detected * support for mpdf associated files * do not include any future times in work contract calculation * re-add username column in Excel spreadsheet * deactivate internal rate editing * show if plugin update exists * shorten name to Kimai only, without Time-Tracking * remove check for existing id in work contract * fix metafield already defined in search * helper methods to unlock months * new translation * send event on unlock month --- composer.json | 1 + composer.lock | 630 +++++++++++------- phpstan.neon | 20 - public/manifest.json | 2 +- .../Authentication/ApiTokenUpgradeBadge.php | 10 +- src/Command/TranslationCommand.php | 8 +- src/Constants.php | 4 +- src/Controller/InvoiceController.php | 1 + src/Controller/PluginController.php | 11 +- src/Event/InvoiceCreatedEvent.php | 2 +- src/Event/InvoiceDocumentsEvent.php | 3 + src/Event/InvoicePreRenderEvent.php | 6 +- .../ThemeJavascriptTranslationsEvent.php | 3 + src/Event/WorkingTimeApproveMonthEvent.php | 19 +- src/Event/WorkingTimeUnlockMonthEvent.php | 34 + src/Export/Base/SpreadsheetRenderer.php | 3 +- src/Form/TimesheetEditForm.php | 15 +- src/Invoice/InvoiceModel.php | 31 + src/Invoice/Renderer/PdfRenderer.php | 4 +- src/Pdf/HtmlToPdfConverter.php | 4 +- src/Pdf/MPdfConverter.php | 47 +- src/Repository/ApiUserRepository.php | 5 +- src/Repository/RepositorySearchTrait.php | 5 +- src/Repository/UserRepository.php | 1 - src/Repository/WorkingTimeRepository.php | 14 +- src/Security/KimaiUserProvider.php | 3 +- src/Timesheet/DateTimeFactory.php | 7 +- src/Twig/Runtime/ThemeExtension.php | 14 +- src/WorkingTime/WorkingTimeService.php | 47 +- templates/contract/status.html.twig | 10 +- templates/partials/logo_login.html.twig | 2 +- templates/plugin/index.html.twig | 3 + .../Security/PasswordResetControllerTest.php | 2 +- .../Security/SecurityControllerTest.php | 2 +- .../SelfRegistrationControllerTest.php | 4 +- .../ThemeJavascriptTranslationsEventTest.php | 4 +- .../WorkingTimeApproveMonthEventTest.php | 34 + .../Event/WorkingTimeUnlockMonthEventTest.php | 33 + .../AbstractActionsSubscriberTestCase.php | 2 +- tests/EventSubscriber/EmailSubscriberTest.php | 1 + .../LastLoginSubscriberTest.php | 2 + tests/EventSubscriber/MenuSubscriberTest.php | 1 + .../PagerfantaExceptionSubscriberTest.php | 1 + .../EventSubscriber/ProfileSubscriberTest.php | 1 + .../UserDetailsSubscriberTest.php | 1 + .../UserPreferenceSubscriberTest.php | 1 + .../EventSubscriber/WizardSubscriberTest.php | 1 + tests/Export/Base/CsvRendererTest.php | 4 +- .../Renderer/AbstractRendererTestCase.php | 10 +- tests/Invoice/InvoiceModelTest.php | 25 + .../Twig/Runtime/ThemeEventExtensionTest.php | 16 +- tests/phpstan.neon | 40 -- translations/messages.de.xlf | 8 + translations/messages.en.xlf | 8 + translations/plugins.de.xlf | 4 + translations/plugins.en.xlf | 4 + translations/validators.de.xlf | 4 + translations/validators.en.xlf | 4 + 58 files changed, 749 insertions(+), 437 deletions(-) create mode 100644 src/Event/WorkingTimeUnlockMonthEvent.php create mode 100644 tests/Event/WorkingTimeApproveMonthEventTest.php create mode 100644 tests/Event/WorkingTimeUnlockMonthEventTest.php diff --git a/composer.json b/composer.json index 99538931..d04c3d18 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "doctrine/doctrine-bundle": "^2.7", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^2.8", + "easybill/zugferd-php": "^2.1", "endroid/qr-code": "^4.8", "erusev/parsedown": "^1.6", "friendsofsymfony/rest-bundle": "^3.0", diff --git a/composer.lock b/composer.lock index 7fba77aa..3b03a332 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "41bb07bb455c3336e8da2e906c33d08f", + "content-hash": "583b9b2747672ce16a337f137a0fe507", "packages": [ { "name": "azuyalabs/yasumi", @@ -487,29 +487,29 @@ }, { "name": "doctrine/collections", - "version": "2.2.2", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "d8af7f248c74f195f7347424600fd9e17b57af59" + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59", - "reference": "d8af7f248c74f195f7347424600fd9e17b57af59", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", "shasum": "" }, "require": { "doctrine/deprecations": "^1", - "php": "^8.1" + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" }, "require-dev": { "doctrine/coding-standard": "^12", "ext-json": "*", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.11" + "phpunit/phpunit": "^10.5" }, "type": "library", "autoload": { @@ -553,7 +553,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.2.2" + "source": "https://github.com/doctrine/collections/tree/2.3.0" }, "funding": [ { @@ -569,7 +569,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T06:56:21+00:00" + "time": "2025-03-22T10:17:19+00:00" }, { "name": "doctrine/common", @@ -820,41 +820,42 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.13.2", + "version": "2.14.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "2363c43d9815a11657e452625cd64172d5587486" + "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/2363c43d9815a11657e452625cd64172d5587486", - "reference": "2363c43d9815a11657e452625cd64172d5587486", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca6a7350b421baf7fbdefbf9f4993292ed18effb", + "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb", "shasum": "" }, "require": { - "doctrine/cache": "^1.11 || ^2.0", "doctrine/dbal": "^3.7.0 || ^4.0", - "doctrine/persistence": "^2.2 || ^3", + "doctrine/persistence": "^3.1 || ^4", "doctrine/sql-formatter": "^1.0.1", - "php": "^7.4 || ^8.0", - "symfony/cache": "^5.4 || ^6.0 || ^7.0", - "symfony/config": "^5.4 || ^6.0 || ^7.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "php": "^8.1", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/doctrine-bridge": "^5.4.46 || ~6.3.12 || ^6.4.3 || ^7.0.3", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1.1 || ^2.0 || ^3" + "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/service-contracts": "^2.5 || ^3" }, "conflict": { "doctrine/annotations": ">=3.0", + "doctrine/cache": "< 1.11", "doctrine/orm": "<2.17 || >=4.0", - "twig/twig": "<1.34 || >=2.0 <2.4" + "symfony/var-exporter": "< 6.4.1 || 7.0.0", + "twig/twig": "<2.13 || >=3.0 <3.0.4" }, "require-dev": { "doctrine/annotations": "^1 || ^2", + "doctrine/cache": "^1.11 || ^2.0", "doctrine/coding-standard": "^12", "doctrine/deprecations": "^1.0", "doctrine/orm": "^2.17 || ^3.0", @@ -862,20 +863,21 @@ "phpstan/phpstan": "2.1.1", "phpstan/phpstan-phpunit": "2.0.3", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "^9.5.26", + "phpunit/phpunit": "^9.6.22", "psr/log": "^1.1.4 || ^2.0 || ^3.0", - "symfony/phpunit-bridge": "^6.1 || ^7.0", - "symfony/property-info": "^5.4 || ^6.0 || ^7.0", - "symfony/proxy-manager-bridge": "^5.4 || ^6.0", - "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0", - "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", - "symfony/string": "^5.4 || ^6.0 || ^7.0", - "symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0", - "symfony/validator": "^5.4 || ^6.0 || ^7.0", - "symfony/var-exporter": "^5.4 || ^6.2 || ^7.0", - "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0", - "twig/twig": "^1.34 || ^2.12 || ^3.0" + "symfony/doctrine-messenger": "^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", + "symfony/string": "^6.4 || ^7.0", + "symfony/twig-bridge": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/var-exporter": "^6.4.1 || ^7.0.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", + "twig/twig": "^2.13 || ^3.0.4" }, "suggest": { "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", @@ -920,7 +922,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.2" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.14.0" }, "funding": [ { @@ -936,7 +938,7 @@ "type": "tidelift" } ], - "time": "2025-01-15T11:12:38+00:00" + "time": "2025-03-22T17:28:21+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -1355,16 +1357,16 @@ }, { "name": "doctrine/migrations", - "version": "3.8.2", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "5007eb1168691225ac305fe16856755c20860842" + "reference": "325b61e41d032f5f7d7e2d11cbefff656eadc9ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/5007eb1168691225ac305fe16856755c20860842", - "reference": "5007eb1168691225ac305fe16856755c20860842", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/325b61e41d032f5f7d7e2d11cbefff656eadc9ab", + "reference": "325b61e41d032f5f7d7e2d11cbefff656eadc9ab", "shasum": "" }, "require": { @@ -1384,7 +1386,7 @@ "require-dev": { "doctrine/coding-standard": "^12", "doctrine/orm": "^2.13 || ^3", - "doctrine/persistence": "^2 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", "doctrine/sql-formatter": "^1.0", "ext-pdo_sqlite": "*", "fig/log-test": "^1", @@ -1438,7 +1440,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.8.2" + "source": "https://github.com/doctrine/migrations/tree/3.9.0" }, "funding": [ { @@ -1454,7 +1456,7 @@ "type": "tidelift" } ], - "time": "2024-10-10T21:35:27+00:00" + "time": "2025-03-26T06:48:45+00:00" }, { "name": "doctrine/orm", @@ -1710,17 +1712,81 @@ "time": "2025-01-24T11:45:48+00:00" }, { - "name": "egulias/email-validator", - "version": "4.0.3", + "name": "easybill/zugferd-php", + "version": "v2.1.1", "source": { "type": "git", - "url": "https://github.com/egulias/EmailValidator.git", - "reference": "b115554301161fa21467629f1e1391c1936de517" + "url": "https://github.com/easybill/zugferd-php.git", + "reference": "376749138f5ecd216b73b5cdd5d4473f2a2d3252" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", - "reference": "b115554301161fa21467629f1e1391c1936de517", + "url": "https://api.github.com/repos/easybill/zugferd-php/zipball/376749138f5ecd216b73b5cdd5d4473f2a2d3252", + "reference": "376749138f5ecd216b73b5cdd5d4473f2a2d3252", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "jms/serializer": "^3.16", + "php": "^8.1" + }, + "require-dev": { + "ext-libxml": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "^9.3", + "rector/rector": "^1.0", + "symfony/finder": "^5.4" + }, + "suggest": { + "ext-libxml": "Needed for XSD validation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Easybill\\ZUGFeRD\\": "src/zugferd10", + "Easybill\\ZUGFeRD2\\": "src/zugferd2" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Romowicz", + "email": "patrick@romowicz.de" + }, + { + "name": "Jan Nöhles", + "email": "jan.noehles@easybill.de" + } + ], + "description": "ZUGFeRD PHP SDK (Factur-X, XRechnung) - Convert PHP Objects to XML and back.", + "keywords": [ + "ZUGFeRD", + "factur-x", + "xrechnung" + ], + "support": { + "issues": "https://github.com/easybill/zugferd-php/issues", + "source": "https://github.com/easybill/zugferd-php/tree/v2.1.1" + }, + "time": "2024-11-07T08:28:44+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { @@ -1766,7 +1832,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -1774,7 +1840,7 @@ "type": "github" } ], - "time": "2024-12-27T00:36:43+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { "name": "endroid/qr-code", @@ -2009,16 +2075,16 @@ }, { "name": "gedmo/doctrine-extensions", - "version": "v3.19.0", + "version": "v3.20.0", "source": { "type": "git", "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", - "reference": "5b0b8a442d19e6701ae64535dc08f7944e2895d2" + "reference": "ea1d37586b8e4bae2a815feb38b177894b12c44c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/5b0b8a442d19e6701ae64535dc08f7944e2895d2", - "reference": "5b0b8a442d19e6701ae64535dc08f7944e2895d2", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/ea1d37586b8e4bae2a815feb38b177894b12c44c", + "reference": "ea1d37586b8e4bae2a815feb38b177894b12c44c", "shasum": "" }, "require": { @@ -2113,10 +2179,9 @@ "uploadable" ], "support": { - "email": "gediminas.morkevicius@gmail.com", + "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.19.0", - "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" + "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.20.0" }, "funding": [ { @@ -2136,7 +2201,7 @@ "type": "github" } ], - "time": "2025-02-24T22:12:57+00:00" + "time": "2025-04-04T17:19:27+00:00" }, { "name": "jms/metadata", @@ -2466,16 +2531,16 @@ }, { "name": "league/csv", - "version": "9.21.0", + "version": "9.23.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "72196d11ebba22d868954cb39c0c7346207430cc" + "reference": "774008ad8a634448e4f8e288905e070e8b317ff3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/72196d11ebba22d868954cb39c0c7346207430cc", - "reference": "72196d11ebba22d868954cb39c0c7346207430cc", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/774008ad8a634448e4f8e288905e070e8b317ff3", + "reference": "774008ad8a634448e4f8e288905e070e8b317ff3", "shasum": "" }, "require": { @@ -2485,19 +2550,23 @@ "require-dev": { "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^3.64.0", - "phpbench/phpbench": "^1.3.1", - "phpstan/phpstan": "^1.12.11", + "friendsofphp/php-cs-fixer": "^3.69.0", + "phpbench/phpbench": "^1.4.0", + "phpstan/phpstan": "^1.12.18", "phpstan/phpstan-deprecation-rules": "^1.2.1", - "phpstan/phpstan-phpunit": "^1.4.1", - "phpstan/phpstan-strict-rules": "^1.6.1", - "phpunit/phpunit": "^10.5.16 || ^11.4.3", - "symfony/var-dumper": "^6.4.8 || ^7.1.8" + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-strict-rules": "^1.6.2", + "phpunit/phpunit": "^10.5.16 || ^11.5.7", + "symfony/var-dumper": "^6.4.8 || ^7.2.3" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters", - "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters" + "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters", + "ext-mysqli": "Requiered to use the package with the MySQLi extension", + "ext-pdo": "Required to use the package with the PDO extension", + "ext-pgsql": "Requiered to use the package with the PgSQL extension", + "ext-sqlite3": "Required to use the package with the SQLite3 extension" }, "type": "library", "extra": { @@ -2510,7 +2579,7 @@ "src/functions_include.php" ], "psr-4": { - "League\\Csv\\": "src/" + "League\\Csv\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2549,7 +2618,7 @@ "type": "github" } ], - "time": "2025-01-08T19:27:58+00:00" + "time": "2025-03-28T06:52:04+00:00" }, { "name": "lorenzo/pinky", @@ -2790,16 +2859,16 @@ }, { "name": "monolog/monolog", - "version": "3.8.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", "shasum": "" }, "require": { @@ -2877,7 +2946,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.8.1" + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" }, "funding": [ { @@ -2889,7 +2958,7 @@ "type": "tidelift" } ], - "time": "2024-12-05T17:15:07+00:00" + "time": "2025-03-24T10:02:05+00:00" }, { "name": "mpdf/mpdf", @@ -3126,16 +3195,16 @@ }, { "name": "nelmio/api-doc-bundle", - "version": "v4.37.1", + "version": "v4.38.2", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "1497977f82d396f1dda8120434c6d29f3de683e6" + "reference": "fdc1cf5bc57287787db59f205a8e77485bd22072" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/1497977f82d396f1dda8120434c6d29f3de683e6", - "reference": "1497977f82d396f1dda8120434c6d29f3de683e6", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/fdc1cf5bc57287787db59f205a8e77485bd22072", + "reference": "fdc1cf5bc57287787db59f205a8e77485bd22072", "shasum": "" }, "require": { @@ -3209,7 +3278,7 @@ "type": "symfony-bundle", "extra": { "branch-alias": { - "dev-master": "4.x-dev" + "dev-4.x": "4.x-dev" } }, "autoload": { @@ -3236,7 +3305,7 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.37.1" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.38.2" }, "funding": [ { @@ -3244,7 +3313,7 @@ "type": "github" } ], - "time": "2025-02-14T12:47:20+00:00" + "time": "2025-03-24T15:00:53+00:00" }, { "name": "nelmio/cors-bundle", @@ -5127,16 +5196,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "342e87b15ac02e4b4f0924ddc368e75d5262aab3" + "reference": "95af448bb7c3d8db02f7b4f5cbf3cb7a6ff1e432" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/342e87b15ac02e4b4f0924ddc368e75d5262aab3", - "reference": "342e87b15ac02e4b4f0924ddc368e75d5262aab3", + "url": "https://api.github.com/repos/symfony/cache/zipball/95af448bb7c3d8db02f7b4f5cbf3cb7a6ff1e432", + "reference": "95af448bb7c3d8db02f7b4f5cbf3cb7a6ff1e432", "shasum": "" }, "require": { @@ -5203,7 +5272,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.19" + "source": "https://github.com/symfony/cache/tree/v6.4.20" }, "funding": [ { @@ -5219,7 +5288,7 @@ "type": "tidelift" } ], - "time": "2025-02-26T09:12:57+00:00" + "time": "2025-03-08T15:51:34+00:00" }, { "name": "symfony/cache-contracts", @@ -5448,16 +5517,16 @@ }, { "name": "symfony/console", - "version": "v6.4.17", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", - "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "url": "https://api.github.com/repos/symfony/console/zipball/2e4af9c952617cc3f9559ff706aee420a8464c36", + "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36", "shasum": "" }, "require": { @@ -5522,7 +5591,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.17" + "source": "https://github.com/symfony/console/tree/v6.4.20" }, "funding": [ { @@ -5538,7 +5607,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T12:07:30+00:00" + "time": "2025-03-03T17:16:38+00:00" }, { "name": "symfony/css-selector", @@ -5607,16 +5676,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "b343c3b2f1539fe41331657b37d5c96c1d1ea842" + "reference": "c49796a9184a532843e78e50df9e55708b92543a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b343c3b2f1539fe41331657b37d5c96c1d1ea842", - "reference": "b343c3b2f1539fe41331657b37d5c96c1d1ea842", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c49796a9184a532843e78e50df9e55708b92543a", + "reference": "c49796a9184a532843e78e50df9e55708b92543a", "shasum": "" }, "require": { @@ -5624,7 +5693,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.2.10|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -5668,7 +5737,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.19" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.20" }, "funding": [ { @@ -5684,7 +5753,7 @@ "type": "tidelift" } ], - "time": "2025-02-20T10:02:49+00:00" + "time": "2025-03-13T09:55:08+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5755,16 +5824,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "22df1e858b8b00647c67fc201dfbea65c0fdab15" + "reference": "7205dbc642bac2ecbf108fadbf9a04aa08290a2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/22df1e858b8b00647c67fc201dfbea65c0fdab15", - "reference": "22df1e858b8b00647c67fc201dfbea65c0fdab15", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/7205dbc642bac2ecbf108fadbf9a04aa08290a2a", + "reference": "7205dbc642bac2ecbf108fadbf9a04aa08290a2a", "shasum": "" }, "require": { @@ -5843,7 +5912,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.19" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.20" }, "funding": [ { @@ -5859,7 +5928,7 @@ "type": "tidelift" } ], - "time": "2025-02-18T08:43:25+00:00" + "time": "2025-02-28T20:55:44+00:00" }, { "name": "symfony/dotenv", @@ -5937,16 +6006,16 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "3d4e55cd2b8f1979a65eba9ab749d6466c316f71" + "reference": "aa3bcf4f7674719df078e61cc8062e5b7f752031" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/3d4e55cd2b8f1979a65eba9ab749d6466c316f71", - "reference": "3d4e55cd2b8f1979a65eba9ab749d6466c316f71", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/aa3bcf4f7674719df078e61cc8062e5b7f752031", + "reference": "aa3bcf4f7674719df078e61cc8062e5b7f752031", "shasum": "" }, "require": { @@ -5992,7 +6061,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.19" + "source": "https://github.com/symfony/error-handler/tree/v6.4.20" }, "funding": [ { @@ -6008,7 +6077,7 @@ "type": "tidelift" } ], - "time": "2025-02-02T20:16:33+00:00" + "time": "2025-03-01T13:00:38+00:00" }, { "name": "symfony/event-dispatcher", @@ -6362,16 +6431,16 @@ }, { "name": "symfony/flex", - "version": "v2.4.7", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402" + "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/92f4fba342161ff36072bd3b8e0b3c6c23160402", - "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402", + "url": "https://api.github.com/repos/symfony/flex/zipball/8ce1acd9842abe0e9b4c4a0bd3f259859516c018", + "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018", "shasum": "" }, "require": { @@ -6410,7 +6479,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.4.7" + "source": "https://github.com/symfony/flex/tree/v2.5.0" }, "funding": [ { @@ -6426,20 +6495,20 @@ "type": "tidelift" } ], - "time": "2024-10-07T08:51:54+00:00" + "time": "2025-03-03T07:50:46+00:00" }, { "name": "symfony/form", - "version": "v6.4.13", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "0fe17f90af23908ddc11dc23507db98e66572046" + "reference": "3929e2a60a828f39df6765fb49d224cc629fa529" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/0fe17f90af23908ddc11dc23507db98e66572046", - "reference": "0fe17f90af23908ddc11dc23507db98e66572046", + "url": "https://api.github.com/repos/symfony/form/zipball/3929e2a60a828f39df6765fb49d224cc629fa529", + "reference": "3929e2a60a828f39df6765fb49d224cc629fa529", "shasum": "" }, "require": { @@ -6507,7 +6576,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v6.4.13" + "source": "https://github.com/symfony/form/tree/v6.4.20" }, "funding": [ { @@ -6523,20 +6592,20 @@ "type": "tidelift" } ], - "time": "2024-10-09T08:40:40+00:00" + "time": "2025-03-27T10:21:45+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "078a6f11cb34d208d6efc74003d77f66a09fa3c2" + "reference": "51418a20079cb25af3fcb8fa8ae1ed82f7fdd1ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/078a6f11cb34d208d6efc74003d77f66a09fa3c2", - "reference": "078a6f11cb34d208d6efc74003d77f66a09fa3c2", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/51418a20079cb25af3fcb8fa8ae1ed82f7fdd1ce", + "reference": "51418a20079cb25af3fcb8fa8ae1ed82f7fdd1ce", "shasum": "" }, "require": { @@ -6656,7 +6725,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.19" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.20" }, "funding": [ { @@ -6672,7 +6741,7 @@ "type": "tidelift" } ], - "time": "2025-02-26T07:27:07+00:00" + "time": "2025-03-23T16:46:24+00:00" }, { "name": "symfony/http-client", @@ -6924,16 +6993,16 @@ }, { "name": "symfony/http-kernel", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "88f2c9f7feff86bb7b9105c5151bc2c1404cd64c" + "reference": "6be6db31bc74693ce5516e1fd5e5ff1171005e37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/88f2c9f7feff86bb7b9105c5151bc2c1404cd64c", - "reference": "88f2c9f7feff86bb7b9105c5151bc2c1404cd64c", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6be6db31bc74693ce5516e1fd5e5ff1171005e37", + "reference": "6be6db31bc74693ce5516e1fd5e5ff1171005e37", "shasum": "" }, "require": { @@ -7018,7 +7087,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.19" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.20" }, "funding": [ { @@ -7034,7 +7103,7 @@ "type": "tidelift" } ], - "time": "2025-02-26T10:51:37+00:00" + "time": "2025-03-28T13:27:10+00:00" }, { "name": "symfony/intl", @@ -7660,17 +7729,93 @@ "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/process", - "version": "v6.4.19", + "name": "symfony/polyfill-php84", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", - "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T12:04:04+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", "shasum": "" }, "require": { @@ -7702,7 +7847,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.19" + "source": "https://github.com/symfony/process/tree/v6.4.20" }, "funding": [ { @@ -7718,7 +7863,7 @@ "type": "tidelift" } ], - "time": "2025-02-04T13:35:48+00:00" + "time": "2025-03-10T17:11:00+00:00" }, { "name": "symfony/property-access", @@ -8974,16 +9119,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "d6aecb7196bf610e63ebb64f937c33878d5d03b1" + "reference": "bb423dfaa51b6d88b1d64197ae695a0c8ac73778" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/d6aecb7196bf610e63ebb64f937c33878d5d03b1", - "reference": "d6aecb7196bf610e63ebb64f937c33878d5d03b1", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/bb423dfaa51b6d88b1d64197ae695a0c8ac73778", + "reference": "bb423dfaa51b6d88b1d64197ae695a0c8ac73778", "shasum": "" }, "require": { @@ -9014,7 +9159,7 @@ "symfony/dependency-injection": "^5.4|^6.0|^7.0", "symfony/expression-language": "^5.4|^6.0|^7.0", "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/form": "^6.4|^7.0", + "symfony/form": "^6.4.20|^7.2.5", "symfony/html-sanitizer": "^6.1|^7.0", "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/http-kernel": "^6.4|^7.0", @@ -9063,7 +9208,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v6.4.19" + "source": "https://github.com/symfony/twig-bridge/tree/v6.4.20" }, "funding": [ { @@ -9079,7 +9224,7 @@ "type": "tidelift" } ], - "time": "2025-02-14T09:54:06+00:00" + "time": "2025-03-28T13:08:36+00:00" }, { "name": "symfony/twig-bundle", @@ -9167,16 +9312,16 @@ }, { "name": "symfony/validator", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "f3e853dffe7c5db675686b8216d6d890dad8c885" + "reference": "9314555aceb8d8ce8abda81e1e47e439258d9309" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/f3e853dffe7c5db675686b8216d6d890dad8c885", - "reference": "f3e853dffe7c5db675686b8216d6d890dad8c885", + "url": "https://api.github.com/repos/symfony/validator/zipball/9314555aceb8d8ce8abda81e1e47e439258d9309", + "reference": "9314555aceb8d8ce8abda81e1e47e439258d9309", "shasum": "" }, "require": { @@ -9244,7 +9389,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.19" + "source": "https://github.com/symfony/validator/tree/v6.4.20" }, "funding": [ { @@ -9260,7 +9405,7 @@ "type": "tidelift" } ], - "time": "2025-02-19T13:12:02+00:00" + "time": "2025-03-14T14:22:58+00:00" }, { "name": "symfony/var-dumper", @@ -9349,16 +9494,16 @@ }, { "name": "symfony/var-exporter", - "version": "v6.4.19", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "be6e71b0c257884c1107313de5d247741cfea172" + "reference": "998df255e9e6a15a36ae35e9c6cd818c17cf92a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/be6e71b0c257884c1107313de5d247741cfea172", - "reference": "be6e71b0c257884c1107313de5d247741cfea172", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/998df255e9e6a15a36ae35e9c6cd818c17cf92a2", + "reference": "998df255e9e6a15a36ae35e9c6cd818c17cf92a2", "shasum": "" }, "require": { @@ -9406,7 +9551,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.19" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.20" }, "funding": [ { @@ -9422,7 +9567,7 @@ "type": "tidelift" } ], - "time": "2025-02-13T09:33:32+00:00" + "time": "2025-03-13T09:55:08+00:00" }, { "name": "symfony/webpack-encore-bundle", @@ -9498,16 +9643,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.18", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5" + "reference": "28ee818fce4a73ac1474346b94e4b966f665c53f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5", - "reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5", + "url": "https://api.github.com/repos/symfony/yaml/zipball/28ee818fce4a73ac1474346b94e4b966f665c53f", + "reference": "28ee818fce4a73ac1474346b94e4b966f665c53f", "shasum": "" }, "require": { @@ -9550,7 +9695,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.18" + "source": "https://github.com/symfony/yaml/tree/v6.4.20" }, "funding": [ { @@ -9566,7 +9711,7 @@ "type": "tidelift" } ], - "time": "2025-01-07T09:44:41+00:00" + "time": "2025-02-27T20:15:30+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -10205,16 +10350,16 @@ }, { "name": "zircote/swagger-php", - "version": "5.0.5", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "2eb4005840058d8844a0bcc14403932331331068" + "reference": "18457fa71f753cfd4a2b21916baf329864fdfaa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/2eb4005840058d8844a0bcc14403932331331068", - "reference": "2eb4005840058d8844a0bcc14403932331331068", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/18457fa71f753cfd4a2b21916baf329864fdfaa6", + "reference": "18457fa71f753cfd4a2b21916baf329864fdfaa6", "shasum": "" }, "require": { @@ -10285,9 +10430,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/5.0.5" + "source": "https://github.com/zircote/swagger-php/tree/5.0.7" }, - "time": "2025-02-24T00:48:00+00:00" + "time": "2025-03-19T03:31:11+00:00" } ], "packages-dev": [ @@ -10423,34 +10568,36 @@ }, { "name": "dama/doctrine-test-bundle", - "version": "v8.2.2", + "version": "v8.3.0", "source": { "type": "git", "url": "https://github.com/dmaicher/doctrine-test-bundle.git", - "reference": "eefe54fdf00d910f808efea9cfce9cc261064a0a" + "reference": "11846789ca2a86f6277316b42448f7fccb3965ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/eefe54fdf00d910f808efea9cfce9cc261064a0a", - "reference": "eefe54fdf00d910f808efea9cfce9cc261064a0a", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/11846789ca2a86f6277316b42448f7fccb3965ff", + "reference": "11846789ca2a86f6277316b42448f7fccb3965ff", "shasum": "" }, "require": { "doctrine/dbal": "^3.3 || ^4.0", "doctrine/doctrine-bundle": "^2.11.0", - "php": "^7.4 || ^8.0", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^5.4 || ^6.3 || ^7.0", - "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0" + "php": ">= 8.1", + "psr/cache": "^2.0 || ^3.0", + "symfony/cache": "^6.4 || ^7.2", + "symfony/framework-bundle": "^6.4 || ^7.2" + }, + "conflict": { + "phpunit/phpunit": "<10.0" }, "require-dev": { "behat/behat": "^3.0", "friendsofphp/php-cs-fixer": "^3.27", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0 || ^11.0", - "symfony/phpunit-bridge": "^7.2", - "symfony/process": "^5.4 || ^6.3 || ^7.0", - "symfony/yaml": "^5.4 || ^6.3 || ^7.0" + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "symfony/process": "^6.4 || ^7.2", + "symfony/yaml": "^6.4 || ^7.2" }, "type": "symfony-bundle", "extra": { @@ -10484,9 +10631,9 @@ ], "support": { "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", - "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.2.2" + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.3.0" }, - "time": "2025-02-04T14:37:36+00:00" + "time": "2025-03-04T10:07:03+00:00" }, { "name": "doctrine/data-fixtures", @@ -10831,16 +10978,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.70.0", + "version": "v3.75.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e" + "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e", - "reference": "2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/399a128ff2fdaf4281e4e79b755693286cdf325c", + "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c", "shasum": "" }, "require": { @@ -10848,6 +10995,7 @@ "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", "ext-filter": "*", + "ext-hash": "*", "ext-json": "*", "ext-tokenizer": "*", "fidry/cpu-core-counter": "^1.2", @@ -10870,18 +11018,18 @@ "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.5", - "infection/infection": "^0.29.10", - "justinrainbow/json-schema": "^5.3 || ^6.0", + "facile-it/paraunit": "^1.3.1 || ^2.6", + "infection/infection": "^0.29.14", + "justinrainbow/json-schema": "^5.3 || ^6.2", "keradus/cli-executor": "^2.1", "mikey179/vfsstream": "^1.6.12", "php-coveralls/php-coveralls": "^2.7", "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.22 || ^10.5.45 || ^11.5.7", - "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.0", - "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.0" + "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12", + "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3", + "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -10922,7 +11070,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.70.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.75.0" }, "funding": [ { @@ -10930,7 +11078,7 @@ "type": "github" } ], - "time": "2025-02-22T23:30:51+00:00" + "time": "2025-03-31T18:40:42+00:00" }, { "name": "masterminds/html5", @@ -11119,16 +11267,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.6", + "version": "2.1.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c" + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", - "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", "shasum": "" }, "require": { @@ -11173,7 +11321,7 @@ "type": "github" } ], - "time": "2025-02-19T15:46:42+00:00" + "time": "2025-03-24T13:45:00+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -11224,16 +11372,16 @@ }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "bdb6a835c5aa9725979694ae9b70591e180f4853" + "reference": "a61a04a361b60014ec04881ccb87252d3bf02e94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/bdb6a835c5aa9725979694ae9b70591e180f4853", - "reference": "bdb6a835c5aa9725979694ae9b70591e180f4853", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/a61a04a361b60014ec04881ccb87252d3bf02e94", + "reference": "a61a04a361b60014ec04881ccb87252d3bf02e94", "shasum": "" }, "require": { @@ -11289,22 +11437,22 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.1" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.2" }, - "time": "2024-12-02T16:48:00+00:00" + "time": "2025-03-03T09:29:16+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "2.0.4", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "d09e152f403c843998d7a52b5d87040c937525dd" + "reference": "6b92469f8a7995e626da3aa487099617b8dfa260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d09e152f403c843998d7a52b5d87040c937525dd", - "reference": "d09e152f403c843998d7a52b5d87040c937525dd", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6b92469f8a7995e626da3aa487099617b8dfa260", + "reference": "6b92469f8a7995e626da3aa487099617b8dfa260", "shasum": "" }, "require": { @@ -11315,7 +11463,9 @@ "phpunit/phpunit": "<7.0" }, "require-dev": { + "nikic/php-parser": "^5", "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6" }, @@ -11340,22 +11490,22 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.4" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.6" }, - "time": "2025-01-22T13:07:38+00:00" + "time": "2025-03-26T12:47:06+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "8b88b5f818bfa301e0c99154ab622dace071c3ba" + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/8b88b5f818bfa301e0c99154ab622dace071c3ba", - "reference": "8b88b5f818bfa301e0c99154ab622dace071c3ba", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", "shasum": "" }, "require": { @@ -11388,22 +11538,22 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.3" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.4" }, - "time": "2025-01-21T10:52:14+00:00" + "time": "2025-03-18T11:42:40+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "2.0.2", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "65f02c7e585f3c7372e42e14d3d87da034031553" + "reference": "648087fb4dd865a09b1828a3b0396eb447665f2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/65f02c7e585f3c7372e42e14d3d87da034031553", - "reference": "65f02c7e585f3c7372e42e14d3d87da034031553", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/648087fb4dd865a09b1828a3b0396eb447665f2e", + "reference": "648087fb4dd865a09b1828a3b0396eb447665f2e", "shasum": "" }, "require": { @@ -11459,9 +11609,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.2" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.4" }, - "time": "2025-01-21T18:57:07+00:00" + "time": "2025-03-28T12:02:03+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/phpstan.neon b/phpstan.neon index ba853bef..76a3f5ac 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3432,26 +3432,6 @@ parameters: count: 1 path: src/Model/Statistic/Month.php - - - message: "#^Method App\\\\Pdf\\\\HtmlToPdfConverter\\:\\:convertToPdf\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Pdf/HtmlToPdfConverter.php - - - - message: "#^Method App\\\\Pdf\\\\MPdfConverter\\:\\:convertToPdf\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Pdf/MPdfConverter.php - - - - message: "#^Method App\\\\Pdf\\\\MPdfConverter\\:\\:sanitizeOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Pdf/MPdfConverter.php - - - - message: "#^Method App\\\\Pdf\\\\MPdfConverter\\:\\:sanitizeOptions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Pdf/MPdfConverter.php - - message: "#^Method App\\\\Pdf\\\\PdfContext\\:\\:getOption\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 diff --git a/public/manifest.json b/public/manifest.json index dae53dba..f7faaa0c 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,5 +1,5 @@ { - "name": "Kimai Time-Tracker", + "name": "Kimai", "short_name": "Kimai", "icons": [ { diff --git a/src/API/Authentication/ApiTokenUpgradeBadge.php b/src/API/Authentication/ApiTokenUpgradeBadge.php index ec6bcf29..6af0e238 100644 --- a/src/API/Authentication/ApiTokenUpgradeBadge.php +++ b/src/API/Authentication/ApiTokenUpgradeBadge.php @@ -9,18 +9,13 @@ namespace App\API\Authentication; -use App\Entity\User; use Symfony\Component\Security\Core\Exception\LogicException; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; final class ApiTokenUpgradeBadge implements BadgeInterface { - /** - * @param string|null $plaintextApiToken - * @param PasswordUpgraderInterface $passwordUpgrader - */ - public function __construct(private ?string $plaintextApiToken, private PasswordUpgraderInterface $passwordUpgrader) + public function __construct(private ?string $plaintextApiToken, private readonly PasswordUpgraderInterface $passwordUpgrader) { } @@ -36,9 +31,6 @@ final class ApiTokenUpgradeBadge implements BadgeInterface return $password; } - /** - * @return PasswordUpgraderInterface - */ public function getPasswordUpgrader(): PasswordUpgraderInterface { return $this->passwordUpgrader; diff --git a/src/Command/TranslationCommand.php b/src/Command/TranslationCommand.php index ab5ce297..1031ae1d 100644 --- a/src/Command/TranslationCommand.php +++ b/src/Command/TranslationCommand.php @@ -454,10 +454,12 @@ final class TranslationCommand extends Command foreach ($xml->file->body->{'trans-unit'} as $unit) { $source = $unit->source; - if (!isset($unit['resname'])) { + if (!isset($unit['resname']) && $source !== null) { $unit['resname'] = $source; } - $unit['id'] = $this->generateId($unit['resname']); + if ($unit['resname'] !== null) { + $unit['id'] = $this->generateId($unit['resname']); // @phpstan-ignore offsetAssign.valueType + } } $xmlDocument = new \DOMDocument('1.0'); @@ -491,7 +493,7 @@ final class TranslationCommand extends Command ); } $unit->target[0] = $translations[$key]; - $unit->target['state'] = 'needs-translation'; + $unit->target['state'] = 'needs-translation'; // @phpstan-ignore assign.propertyType $foundEmpty = true; } diff --git a/src/Constants.php b/src/Constants.php index 3970ee93..b95f953e 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ final class Constants /** * The current release version */ - public const VERSION = '2.31.0'; + public const VERSION = '2.32.0'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 23100; + public const VERSION_ID = 23200; /** * The software name */ diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index f9a2ff1b..bfc0682f 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -164,6 +164,7 @@ final class InvoiceController extends AbstractController try { $query->setCustomers([$customer]); $model = $this->service->createModel($query); + $model->setPreview(true); return $this->service->renderInvoice($model, $this->dispatcher, true); } catch (Exception $ex) { diff --git a/src/Controller/PluginController.php b/src/Controller/PluginController.php index e1b6b26f..ace5ea10 100644 --- a/src/Controller/PluginController.php +++ b/src/Controller/PluginController.php @@ -34,11 +34,20 @@ final class PluginController extends AbstractController $page = new PageSetup('menu.plugin'); $page->setHelp('plugins.html'); + $all = $this->getPluginInformation($client, $cache); + $bundles = []; + foreach ($all as $item) { + if ($item['bundle'] !== null) { + $bundles[$item['bundle']] = $item; + } + } + return $this->render('plugin/index.html.twig', [ 'page_setup' => $page, 'plugins' => $plugins, 'installed' => $installed, - 'extensions' => $this->getPluginInformation($client, $cache) + 'extensions' => $all, + 'bundles' => $bundles, ]); } diff --git a/src/Event/InvoiceCreatedEvent.php b/src/Event/InvoiceCreatedEvent.php index 7ceb0140..1ee3d460 100644 --- a/src/Event/InvoiceCreatedEvent.php +++ b/src/Event/InvoiceCreatedEvent.php @@ -15,7 +15,7 @@ use Symfony\Contracts\EventDispatcher\Event; final class InvoiceCreatedEvent extends Event { - public function __construct(private Invoice $invoice, private InvoiceModel $model) + public function __construct(private readonly Invoice $invoice, private readonly InvoiceModel $model) { } diff --git a/src/Event/InvoiceDocumentsEvent.php b/src/Event/InvoiceDocumentsEvent.php index ac0cf4ae..efd6603c 100644 --- a/src/Event/InvoiceDocumentsEvent.php +++ b/src/Event/InvoiceDocumentsEvent.php @@ -47,6 +47,9 @@ final class InvoiceDocumentsEvent extends Event $this->documents = $documents; } + /** + * @CloudRequired + */ public function setMaximumAllowedDocuments(int $max): void { $this->maximum = $max; diff --git a/src/Event/InvoicePreRenderEvent.php b/src/Event/InvoicePreRenderEvent.php index e966a607..4838c51f 100644 --- a/src/Event/InvoicePreRenderEvent.php +++ b/src/Event/InvoicePreRenderEvent.php @@ -16,7 +16,11 @@ use Symfony\Contracts\EventDispatcher\Event; final class InvoicePreRenderEvent extends Event { - public function __construct(private InvoiceModel $model, private InvoiceDocument $document, private RendererInterface $renderer) + public function __construct( + private readonly InvoiceModel $model, + private readonly InvoiceDocument $document, + private readonly RendererInterface $renderer + ) { } diff --git a/src/Event/ThemeJavascriptTranslationsEvent.php b/src/Event/ThemeJavascriptTranslationsEvent.php index 15825ef0..b7f4da7a 100644 --- a/src/Event/ThemeJavascriptTranslationsEvent.php +++ b/src/Event/ThemeJavascriptTranslationsEvent.php @@ -11,6 +11,9 @@ namespace App\Event; use Symfony\Contracts\EventDispatcher\Event; +/** + * @deprecated since 2.32 - use TranslatorInterface directly + */ final class ThemeJavascriptTranslationsEvent extends Event { /** diff --git a/src/Event/WorkingTimeApproveMonthEvent.php b/src/Event/WorkingTimeApproveMonthEvent.php index f38f2707..530c8124 100644 --- a/src/Event/WorkingTimeApproveMonthEvent.php +++ b/src/Event/WorkingTimeApproveMonthEvent.php @@ -15,13 +15,19 @@ use Symfony\Contracts\EventDispatcher\Event; final class WorkingTimeApproveMonthEvent extends Event { - public function __construct(private User $user, private Month $month, private \DateTimeInterface $approvalDate, private User $approver) + public function __construct( + private readonly Month $month, + private readonly User $approvedBy + ) { } + /** + * @deprecated use getMonth()->getUser() instead) + */ public function getUser(): User { - return $this->user; + return $this->getMonth()->getUser(); } public function getMonth(): Month @@ -29,13 +35,8 @@ final class WorkingTimeApproveMonthEvent extends Event return $this->month; } - public function getApprovalDate(): \DateTimeInterface + public function getApprovedBy(): User { - return $this->approvalDate; - } - - public function getApprover(): User - { - return $this->approver; + return $this->approvedBy; } } diff --git a/src/Event/WorkingTimeUnlockMonthEvent.php b/src/Event/WorkingTimeUnlockMonthEvent.php new file mode 100644 index 00000000..a42a4a05 --- /dev/null +++ b/src/Event/WorkingTimeUnlockMonthEvent.php @@ -0,0 +1,34 @@ +month; + } + + public function getUnlockedBy(): User + { + return $this->unlockedBy; + } +} diff --git a/src/Export/Base/SpreadsheetRenderer.php b/src/Export/Base/SpreadsheetRenderer.php index 50bb0b4f..27143ed3 100644 --- a/src/Export/Base/SpreadsheetRenderer.php +++ b/src/Export/Base/SpreadsheetRenderer.php @@ -164,7 +164,8 @@ final class SpreadsheetRenderer $columns[] = (new Column('fixedRate', new RateFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getFixedRate()); } - $columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName())->withColumnWidth(ColumnWidth::MEDIUM); + $columns[] = (new Column('alias', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName())->withColumnWidth(ColumnWidth::MEDIUM); + $columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getUserIdentifier())->withColumnWidth(ColumnWidth::MEDIUM); $columns[] = (new Column('account_number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getAccountNumber()); $columns[] = (new Column('customer', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getName())->withColumnWidth(ColumnWidth::MEDIUM); $columns[] = (new Column('project', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getName())->withColumnWidth(ColumnWidth::MEDIUM); diff --git a/src/Form/TimesheetEditForm.php b/src/Form/TimesheetEditForm.php index aff4012d..901d9007 100644 --- a/src/Form/TimesheetEditForm.php +++ b/src/Form/TimesheetEditForm.php @@ -360,16 +360,13 @@ class TimesheetEditForm extends AbstractType return; } + $moneyOptions = ['currency' => $currency]; + $builder - ->add('fixedRate', FixedRateType::class, [ - 'currency' => $currency, - ]) - ->add('hourlyRate', HourlyRateType::class, [ - 'currency' => $currency, - ]) - ->add('internalRate', InternalRateType::class, [ - 'currency' => $currency, - ]); + ->add('fixedRate', FixedRateType::class, $moneyOptions) + ->add('hourlyRate', HourlyRateType::class, $moneyOptions) + //->add('internalRate', InternalRateType::class, $moneyOptions) + ; } protected function addUser(FormBuilderInterface $builder, array $options): void diff --git a/src/Invoice/InvoiceModel.php b/src/Invoice/InvoiceModel.php index 630c8280..625dbf4b 100644 --- a/src/Invoice/InvoiceModel.php +++ b/src/Invoice/InvoiceModel.php @@ -52,6 +52,11 @@ final class InvoiceModel private array $itemHydrator = []; private ?string $invoiceNumber = null; private bool $hideZeroTax = false; + private bool $isPreview = false; + /** + * @var array|null|bool|int|float> + */ + private array $options = []; /** * @internal use InvoiceModelFactory @@ -68,6 +73,22 @@ final class InvoiceModel $this->addItemHydrator(new InvoiceItemDefaultHydrator()); } + /** + * @param string|array|null|bool|int|float $value + */ + public function setOption(string $key, string|array|null|bool|int|float $value): void + { + $this->options[$key] = $value; + } + + /** + * @return array|null|bool|int|float> + */ + public function getOptions(): array + { + return $this->options; + } + public function getQuery(): ?InvoiceQuery { return $this->query; @@ -265,4 +286,14 @@ final class InvoiceModel { $this->hideZeroTax = $hideZeroTax; } + + public function isPreview(): bool + { + return $this->isPreview; + } + + public function setPreview(bool $preview): void + { + $this->isPreview = $preview; + } } diff --git a/src/Invoice/Renderer/PdfRenderer.php b/src/Invoice/Renderer/PdfRenderer.php index b3030dbf..17ec78f5 100644 --- a/src/Invoice/Renderer/PdfRenderer.php +++ b/src/Invoice/Renderer/PdfRenderer.php @@ -23,7 +23,7 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin { use PDFRendererTrait; - public function __construct(Environment $twig, private HtmlToPdfConverter $converter) + public function __construct(Environment $twig, private readonly HtmlToPdfConverter $converter) { parent::__construct($twig); } @@ -45,7 +45,7 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin $context->setOption('margin_bottom', '8'); $content = $this->renderTwigTemplate($document, $model, ['pdfContext' => $context]); - $content = $this->converter->convertToPdf($content, $context->getOptions()); + $content = $this->converter->convertToPdf($content, array_merge($model->getOptions(), $context->getOptions())); return $this->createPdfResponse($content, $context); } diff --git a/src/Pdf/HtmlToPdfConverter.php b/src/Pdf/HtmlToPdfConverter.php index ca83caf0..a8f7f1ea 100644 --- a/src/Pdf/HtmlToPdfConverter.php +++ b/src/Pdf/HtmlToPdfConverter.php @@ -15,9 +15,7 @@ interface HtmlToPdfConverter * Returns the binary content of the PDF, which can be saved as file. * Throws an exception if conversion fails. * - * @param string $html - * @param array $options - * @return string + * @param array> $options * @throws \Exception */ public function convertToPdf(string $html, array $options = []): string; diff --git a/src/Pdf/MPdfConverter.php b/src/Pdf/MPdfConverter.php index 6e2ef7a2..e9b93d92 100644 --- a/src/Pdf/MPdfConverter.php +++ b/src/Pdf/MPdfConverter.php @@ -18,22 +18,29 @@ use Mpdf\Output\Destination; final class MPdfConverter implements HtmlToPdfConverter { - public function __construct(private FileHelper $fileHelper, private string $cacheDirectory) + public function __construct( + private readonly FileHelper $fileHelper, + private readonly string $cacheDirectory + ) { } + /** + * @param array> $options + * @return array> + */ private function sanitizeOptions(array $options): array { - $configs = new ConfigVariables(); - $fonts = new FontVariables(); - $allowed = [ - 'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top', - 'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts', - ]; - - $filtered = array_filter($options, function ($key) use ($allowed, $configs, $fonts): bool { + $filtered = array_filter($options, function ($key): bool { + $allowed = [ + 'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top', + 'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts', 'associated_files' + ]; if (!\in_array($key, $allowed)) { + $configs = new ConfigVariables(); if (!\array_key_exists($key, $configs->getDefaults())) { + $fonts = new FontVariables(); + return \array_key_exists($key, $fonts->getDefaults()); } } @@ -49,10 +56,7 @@ final class MPdfConverter implements HtmlToPdfConverter } /** - * @param string $html - * @param array $options - * @return string - * @throws \Mpdf\MpdfException + * @param array> $options */ public function convertToPdf(string $html, array $options = []): string { @@ -93,17 +97,26 @@ final class MPdfConverter implements HtmlToPdfConverter } /** - * @param array> $options - * @return Mpdf + * @param array> $options */ private function initMpdf(array $options): Mpdf { $options['fontDir'] = $this->getFontDirectories(); $options['fontdata'] = $this->mergeFontData($options); + $associatedFiles = []; + if (\array_key_exists('associated_files', $options) && \is_array($options['associated_files'])) { + $associatedFiles = $options['associated_files']; + unset($options['associated_files']); + } + $mpdf = new Mpdf($options); $mpdf->creator = Constants::SOFTWARE; + if (\count($associatedFiles) > 0) { + $mpdf->SetAssociatedFiles($associatedFiles); + } + return $mpdf; } @@ -120,8 +133,8 @@ final class MPdfConverter implements HtmlToPdfConverter } /** - * @param array> $options - * @return array> + * @param array> $options + * @return array> */ private function mergeFontData(array $options): array { diff --git a/src/Repository/ApiUserRepository.php b/src/Repository/ApiUserRepository.php index cb8dab42..c5f56331 100644 --- a/src/Repository/ApiUserRepository.php +++ b/src/Repository/ApiUserRepository.php @@ -15,12 +15,9 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; -/** - * @template-implements PasswordUpgraderInterface - */ class ApiUserRepository implements UserLoaderInterface, PasswordUpgraderInterface { - public function __construct(private UserRepository $userRepository) + public function __construct(private readonly UserRepository $userRepository) { } diff --git a/src/Repository/RepositorySearchTrait.php b/src/Repository/RepositorySearchTrait.php index 947e8e7e..230979f5 100644 --- a/src/Repository/RepositorySearchTrait.php +++ b/src/Repository/RepositorySearchTrait.php @@ -68,6 +68,7 @@ trait RepositorySearchTrait $alias = 'meta' . $a++; $paramName = 'metaName' . $i++; $paramValue = 'metaValue' . $c++; + $subqueryName = 'metaNotExists' . $metaName; if ($metaValue === '*') { $qb->leftJoin($rootAlias . '.meta', $alias); @@ -76,7 +77,7 @@ trait RepositorySearchTrait $and->add($qb->expr()->isNotNull($alias . '.value')); } elseif ($metaValue === '~') { $and->add( - \sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias) + \sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id)', $subqueryName, $this->getMetaFieldClass(), $subqueryName, $subqueryName, $this->getMetaFieldName(), $rootAlias) ); } elseif ($metaValue === '' || $metaValue === null) { $qb->leftJoin($rootAlias . '.meta', $alias); @@ -86,7 +87,7 @@ trait RepositorySearchTrait $qb->expr()->eq($alias . '.name', ':' . $paramName), $qb->expr()->isNull($alias . '.value') ), - \sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias) + \sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id)', $subqueryName, $this->getMetaFieldClass(), $subqueryName, $subqueryName, $this->getMetaFieldName(), $rootAlias) ) ); $qb->setParameter($paramName, $metaName); diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index c6773408..189e5e71 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -35,7 +35,6 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; /** * @extends EntityRepository - * @template-implements PasswordUpgraderInterface * @template-implements UserProviderInterface */ class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface diff --git a/src/Repository/WorkingTimeRepository.php b/src/Repository/WorkingTimeRepository.php index 4f4031f3..57495bb1 100644 --- a/src/Repository/WorkingTimeRepository.php +++ b/src/Repository/WorkingTimeRepository.php @@ -21,18 +21,10 @@ class WorkingTimeRepository extends EntityRepository { private bool $pendingUpdate = false; - public function deleteWorkingTime(WorkingTime $workingTime): void + public function scheduleWorkingTimeDelete(WorkingTime $workingTime): void { - $entityManager = $this->getEntityManager(); - $entityManager->remove($workingTime); - $entityManager->flush(); - } - - public function saveWorkingTime(WorkingTime $workingTime): void - { - $entityManager = $this->getEntityManager(); - $entityManager->persist($workingTime); - $entityManager->flush(); + $this->pendingUpdate = true; + $this->getEntityManager()->remove($workingTime); } public function scheduleWorkingTimeUpdate(WorkingTime $workingTime): void diff --git a/src/Security/KimaiUserProvider.php b/src/Security/KimaiUserProvider.php index 3f292c5f..f0a672f9 100644 --- a/src/Security/KimaiUserProvider.php +++ b/src/Security/KimaiUserProvider.php @@ -19,7 +19,6 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; /** - * @template-implements PasswordUpgraderInterface * @template-implements UserProviderInterface */ final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface @@ -29,7 +28,7 @@ final class KimaiUserProvider implements UserProviderInterface, PasswordUpgrader /** * @param iterable> $providers */ - public function __construct(private iterable $providers, private SystemConfiguration $configuration) + public function __construct(private readonly iterable $providers, private readonly SystemConfiguration $configuration) { } diff --git a/src/Timesheet/DateTimeFactory.php b/src/Timesheet/DateTimeFactory.php index 339cf0a8..c1178458 100644 --- a/src/Timesheet/DateTimeFactory.php +++ b/src/Timesheet/DateTimeFactory.php @@ -166,12 +166,7 @@ final class DateTimeFactory return $date->modify('23:59:59'); } - /** - * @param string $format - * @param null|string $datetime - * @return bool|DateTime - */ - public function createDateTimeFromFormat(string $format, ?string $datetime = 'now'): bool|DateTime + public function createDateTimeFromFormat(string $format, ?string $datetime = 'now'): false|DateTime { return DateTime::createFromFormat($format, $datetime ?? 'now', $this->getTimezone()); } diff --git a/src/Twig/Runtime/ThemeExtension.php b/src/Twig/Runtime/ThemeExtension.php index fb64360f..a8331565 100644 --- a/src/Twig/Runtime/ThemeExtension.php +++ b/src/Twig/Runtime/ThemeExtension.php @@ -24,15 +24,17 @@ use Twig\Extension\RuntimeExtensionInterface; final class ThemeExtension implements RuntimeExtensionInterface { - public function __construct(private EventDispatcherInterface $eventDispatcher, private TranslatorInterface $translator, private SystemConfiguration $configuration, private Security $security) + public function __construct( + private readonly EventDispatcherInterface $eventDispatcher, + private readonly TranslatorInterface $translator, + private readonly SystemConfiguration $configuration, + private readonly Security $security + ) { } /** - * @param Environment $environment - * @param string $eventName * @param array $payload - * @return ThemeEvent */ public function trigger(Environment $environment, string $eventName, array $payload = []): ThemeEvent { @@ -61,7 +63,7 @@ final class ThemeExtension implements RuntimeExtensionInterface public function getJavascriptTranslations(): array { - $event = new ThemeJavascriptTranslationsEvent(); + $event = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated $this->eventDispatcher->dispatch($event); @@ -97,7 +99,7 @@ final class ThemeExtension implements RuntimeExtensionInterface public function generateTitle(?string $prefix = null, string $delimiter = ' – '): string { - return ($prefix ?? '') . Constants::SOFTWARE . $delimiter . $this->translator->trans('time_tracking', [], 'messages'); + return ($prefix ?? '') . Constants::SOFTWARE; } public function colorize(?string $color, ?string $identifier = null): string diff --git a/src/WorkingTime/WorkingTimeService.php b/src/WorkingTime/WorkingTimeService.php index 5937503e..831f5972 100644 --- a/src/WorkingTime/WorkingTimeService.php +++ b/src/WorkingTime/WorkingTimeService.php @@ -13,6 +13,7 @@ use App\Entity\User; use App\Entity\WorkingTime; use App\Event\WorkingTimeApproveMonthEvent; use App\Event\WorkingTimeQueryStatsEvent; +use App\Event\WorkingTimeUnlockMonthEvent; use App\Event\WorkingTimeYearEvent; use App\Event\WorkingTimeYearSummaryEvent; use App\Repository\TimesheetRepository; @@ -155,12 +156,14 @@ final class WorkingTimeService $dayDate = $day->getDay(); $result = new WorkingTime($user, $dayDate); - if (($firstDay === null || $firstDay <= $dayDate) && ($lastDay === null || $lastDay >= $dayDate)) { - $result->setExpectedTime($calculator->getWorkHoursForDay($dayDate)); - } + if ($dayDate <= $until) { + if (($firstDay === null || $firstDay <= $dayDate) && ($lastDay === null || $lastDay >= $dayDate)) { + $result->setExpectedTime($calculator->getWorkHoursForDay($dayDate)); + } - if (\array_key_exists($key, $stats)) { - $result->setActualTime($stats[$key]); + if (\array_key_exists($key, $stats)) { + $result->setActualTime($stats[$key]); + } } $day->setWorkingTime($result); @@ -181,6 +184,7 @@ final class WorkingTimeService return $year->getMonth($monthDate); } + // deprecated 3.0 remove $user, fetch from $month->getUser() instead public function approveMonth(User $user, Month $month, \DateTimeInterface $approvalDate, User $approvedBy): void { foreach ($month->getDays() as $day) { @@ -189,10 +193,6 @@ final class WorkingTimeService continue; } - if ($workingTime->getId() !== null) { - continue; - } - if ($month->isLocked() || $workingTime->isApproved()) { continue; } @@ -205,15 +205,38 @@ final class WorkingTimeService $this->workingTimeRepository->persistScheduledWorkingTimes(); + // $user = $month->getUser(); $user->setPreferenceValue(self::LATEST_APPROVAL_PREF, $this->workingTimeRepository->getLatestApprovalDate($user)?->format(self::LATEST_APPROVAL_FORMAT)); $this->userRepository->saveUser($user); - $this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($user, $month, $approvalDate, $approvedBy)); + $this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($month, $approvedBy)); + } + + public function unlockMonth(Month $month, User $unlockedBy): void + { + foreach ($month->getDays() as $day) { + $workingTime = $day->getWorkingTime(); + if ($workingTime === null || $workingTime->getId() === null) { + continue; + } + + if (!$workingTime->isApproved()) { + continue; + } + + $this->workingTimeRepository->scheduleWorkingTimeDelete($workingTime); + } + + $this->workingTimeRepository->persistScheduledWorkingTimes(); + + $user = $month->getUser(); + $user->setPreferenceValue(self::LATEST_APPROVAL_PREF, $this->workingTimeRepository->getLatestApprovalDate($user)?->format(self::LATEST_APPROVAL_FORMAT)); + $this->userRepository->saveUser($user); + + $this->eventDispatcher->dispatch(new WorkingTimeUnlockMonthEvent($month, $unlockedBy)); } /** - * @param \DateTimeInterface $year - * @param User $user * @return array */ private function getYearStatistics(\DateTimeInterface $year, User $user): array diff --git a/templates/contract/status.html.twig b/templates/contract/status.html.twig index b788e4f5..2960cf57 100644 --- a/templates/contract/status.html.twig +++ b/templates/contract/status.html.twig @@ -98,10 +98,7 @@ {% for month in year.months %} {% set monthShould = month.expectedTime(now) %} {% set monthIs = month.actualTime %} - {% set showStats = now > month.month or monthIs > 0 %} - {% if showStats %} - {% set yearShould = yearShould + monthShould %} - {% endif %} + {% set yearShould = yearShould + monthShould %} {% set yearIs = yearIs + monthIs %} {% if month.locked %} {% set approved = approved + 1 %} @@ -137,7 +134,6 @@ {% for month in year.months %} {% set monthShould = month.expectedTime(now) %} {% set monthIs = month.actualTime %} - {% set showStats = now > month.month or monthIs > 0 %} {% if withWorkHourConfiguration %} @@ -152,9 +148,7 @@ {{ month.month|month_name }} - {% if showStats %} - {{ work_times_result(monthShould, monthIs, decimal) }} - {% endif %} + {{ work_times_result(monthShould, monthIs, decimal) }} {{ monthShould|duration(decimal) }} diff --git a/templates/partials/logo_login.html.twig b/templates/partials/logo_login.html.twig index feb21375..afb190a3 100644 --- a/templates/partials/logo_login.html.twig +++ b/templates/partials/logo_login.html.twig @@ -8,5 +8,5 @@ {{ get_title() }} {% else %} -

{{ get_title('', '
')|raw }}

+

{{ get_title()|raw }}

{% endif %} diff --git a/templates/plugin/index.html.twig b/templates/plugin/index.html.twig index 52f4557e..989bf02e 100644 --- a/templates/plugin/index.html.twig +++ b/templates/plugin/index.html.twig @@ -29,6 +29,9 @@ {{ plugin.metadata.description }} + {% if bundles[plugin.id] is defined and bundles[plugin.id]['latest_release'] is defined and bundles[plugin.id]['latest_release'] != plugin.metadata.version %} + {{ widgets.alert('warning', 'update_available'|trans({'%version%': bundles[plugin.id]['latest_release']}, 'plugins')) }} + {% endif %} {{ widgets.label(plugin.metadata.version, 'primary', plugin.id) }} diff --git a/tests/Controller/Security/PasswordResetControllerTest.php b/tests/Controller/Security/PasswordResetControllerTest.php index 27fd76fd..87ca0f40 100644 --- a/tests/Controller/Security/PasswordResetControllerTest.php +++ b/tests/Controller/Security/PasswordResetControllerTest.php @@ -51,7 +51,7 @@ class PasswordResetControllerTest extends AbstractControllerBaseTestCase $content = $response->getContent(); self::assertNotFalse($content); - self::assertStringContainsString('Kimai – Time Tracking', $content); + self::assertStringContainsString('Kimai', $content); self::assertStringContainsString('Reset your password', $content); self::assertStringContainsString('
', $content); self::assertStringContainsString('getResponse()->isSuccessful()); $content = $response->getContent(); - self::assertStringContainsString('Kimai – Time Tracking', $content); + self::assertStringContainsString('Kimai', $content); self::assertStringContainsString('isSuccessful()); $content = $response->getContent(); - self::assertStringContainsString('Kimai – Time Tracking', $content); + self::assertStringContainsString('Kimai', $content); self::assertStringContainsString('Register a new account', $content); self::assertStringContainsString('createUser($client, 'example', 'register@example.com', 'test1234'); $content = $client->getResponse()->getContent(); - self::assertStringContainsString('Kimai – Time Tracking', $content); + self::assertStringContainsString('Kimai', $content); self::assertStringContainsString('An e-mail has been sent to register@example.com. It contains a link you must click to activate your account.', $content); self::assertStringContainsString('', $content); } diff --git a/tests/Event/ThemeJavascriptTranslationsEventTest.php b/tests/Event/ThemeJavascriptTranslationsEventTest.php index eb663fd7..02d63156 100644 --- a/tests/Event/ThemeJavascriptTranslationsEventTest.php +++ b/tests/Event/ThemeJavascriptTranslationsEventTest.php @@ -21,14 +21,14 @@ class ThemeJavascriptTranslationsEventTest extends TestCase public function testDefaultValues(): void { - $sut = new ThemeJavascriptTranslationsEvent(); + $sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated self::assertCount(self::COUNTER, $sut->getTranslations()); } public function testGetterAndSetter(): void { - $sut = new ThemeJavascriptTranslationsEvent(); + $sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated $sut->setTranslation('foo', 'bar'); $sut->setTranslation('hello', 'world', 'testing'); diff --git a/tests/Event/WorkingTimeApproveMonthEventTest.php b/tests/Event/WorkingTimeApproveMonthEventTest.php new file mode 100644 index 00000000..f44c2f05 --- /dev/null +++ b/tests/Event/WorkingTimeApproveMonthEventTest.php @@ -0,0 +1,34 @@ +getUser()); // @phpstan-ignore method.deprecated + self::assertSame($approvalMonth, $sut->getMonth()); + self::assertSame($approver, $sut->getApprovedBy()); + } +} diff --git a/tests/Event/WorkingTimeUnlockMonthEventTest.php b/tests/Event/WorkingTimeUnlockMonthEventTest.php new file mode 100644 index 00000000..be4f9e2b --- /dev/null +++ b/tests/Event/WorkingTimeUnlockMonthEventTest.php @@ -0,0 +1,33 @@ +getMonth()); + self::assertSame($approver, $sut->getUnlockedBy()); + } +} diff --git a/tests/EventSubscriber/Actions/AbstractActionsSubscriberTestCase.php b/tests/EventSubscriber/Actions/AbstractActionsSubscriberTestCase.php index fce554b6..a3df0918 100644 --- a/tests/EventSubscriber/Actions/AbstractActionsSubscriberTestCase.php +++ b/tests/EventSubscriber/Actions/AbstractActionsSubscriberTestCase.php @@ -22,7 +22,7 @@ abstract class AbstractActionsSubscriberTestCase extends TestCase protected function createSubscriber(string $className, ...$grants): AbstractActionsSubscriber { $auth = $this->createMock(AuthorizationCheckerInterface::class); - $auth->method('isGranted')->willReturnOnConsecutiveCalls(...$grants); + $auth->method('isGranted')->willReturnOnConsecutiveCalls(...$grants); // @phpstan-ignore argument.named $router = $this->createMock(UrlGeneratorInterface::class); $router->method('generate')->willReturnArgument(0); diff --git a/tests/EventSubscriber/EmailSubscriberTest.php b/tests/EventSubscriber/EmailSubscriberTest.php index c1ddf528..011ebed2 100644 --- a/tests/EventSubscriber/EmailSubscriberTest.php +++ b/tests/EventSubscriber/EmailSubscriberTest.php @@ -27,6 +27,7 @@ class EmailSubscriberTest extends TestCase $events = EmailSubscriber::getSubscribedEvents(); self::assertArrayHasKey(EmailEvent::class, $events); $methodName = $events[EmailEvent::class][0]; + self::assertIsString($methodName); self::assertTrue(method_exists(EmailSubscriber::class, $methodName)); } diff --git a/tests/EventSubscriber/LastLoginSubscriberTest.php b/tests/EventSubscriber/LastLoginSubscriberTest.php index 251d77f6..edb21a5c 100644 --- a/tests/EventSubscriber/LastLoginSubscriberTest.php +++ b/tests/EventSubscriber/LastLoginSubscriberTest.php @@ -31,10 +31,12 @@ class LastLoginSubscriberTest extends TestCase self::assertArrayHasKey(UserInteractiveLoginEvent::class, $events); $methodName = $events[UserInteractiveLoginEvent::class]; + self::assertIsString($methodName); self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName)); self::assertArrayHasKey(LoginSuccessEvent::class, $events); $methodName = $events[LoginSuccessEvent::class]; + self::assertIsString($methodName); self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName)); } diff --git a/tests/EventSubscriber/MenuSubscriberTest.php b/tests/EventSubscriber/MenuSubscriberTest.php index 08914e7b..62f2895d 100644 --- a/tests/EventSubscriber/MenuSubscriberTest.php +++ b/tests/EventSubscriber/MenuSubscriberTest.php @@ -23,6 +23,7 @@ class MenuSubscriberTest extends TestCase $events = MenuSubscriber::getSubscribedEvents(); self::assertArrayHasKey(ConfigureMainMenuEvent::class, $events); $methodName = $events[ConfigureMainMenuEvent::class][0]; + self::assertIsString($methodName); self::assertTrue(method_exists(MenuSubscriber::class, $methodName)); } } diff --git a/tests/EventSubscriber/PagerfantaExceptionSubscriberTest.php b/tests/EventSubscriber/PagerfantaExceptionSubscriberTest.php index 2286e490..330467ae 100644 --- a/tests/EventSubscriber/PagerfantaExceptionSubscriberTest.php +++ b/tests/EventSubscriber/PagerfantaExceptionSubscriberTest.php @@ -29,6 +29,7 @@ class PagerfantaExceptionSubscriberTest extends TestCase $events = PagerfantaExceptionSubscriber::getSubscribedEvents(); self::assertArrayHasKey(KernelEvents::EXCEPTION, $events); $methodName = $events[KernelEvents::EXCEPTION][0]; + self::assertIsString($methodName); self::assertTrue(method_exists(PagerfantaExceptionSubscriber::class, $methodName)); } diff --git a/tests/EventSubscriber/ProfileSubscriberTest.php b/tests/EventSubscriber/ProfileSubscriberTest.php index 61a5cdc7..17e4db93 100644 --- a/tests/EventSubscriber/ProfileSubscriberTest.php +++ b/tests/EventSubscriber/ProfileSubscriberTest.php @@ -33,6 +33,7 @@ class ProfileSubscriberTest extends TestCase self::assertArrayHasKey(LoginSuccessEvent::class, $events); $methodName = $events[LoginSuccessEvent::class]; + self::assertIsString($methodName); self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName)); } diff --git a/tests/EventSubscriber/UserDetailsSubscriberTest.php b/tests/EventSubscriber/UserDetailsSubscriberTest.php index d0993091..fe0e3381 100644 --- a/tests/EventSubscriber/UserDetailsSubscriberTest.php +++ b/tests/EventSubscriber/UserDetailsSubscriberTest.php @@ -23,6 +23,7 @@ class UserDetailsSubscriberTest extends TestCase $events = UserDetailsSubscriber::getSubscribedEvents(); self::assertArrayHasKey(UserDetailsEvent::class, $events); $methodName = $events[UserDetailsEvent::class][0]; + self::assertIsString($methodName); self::assertTrue(method_exists(UserDetailsSubscriber::class, $methodName)); } } diff --git a/tests/EventSubscriber/UserPreferenceSubscriberTest.php b/tests/EventSubscriber/UserPreferenceSubscriberTest.php index 4d9de4f2..5815dc1d 100644 --- a/tests/EventSubscriber/UserPreferenceSubscriberTest.php +++ b/tests/EventSubscriber/UserPreferenceSubscriberTest.php @@ -44,6 +44,7 @@ class UserPreferenceSubscriberTest extends TestCase $events = UserPreferenceSubscriber::getSubscribedEvents(); self::assertArrayHasKey(PrepareUserEvent::class, $events); $methodName = $events[PrepareUserEvent::class][0]; + self::assertIsString($methodName); self::assertTrue(method_exists(UserPreferenceSubscriber::class, $methodName)); } diff --git a/tests/EventSubscriber/WizardSubscriberTest.php b/tests/EventSubscriber/WizardSubscriberTest.php index 3a50195b..e3e4b349 100644 --- a/tests/EventSubscriber/WizardSubscriberTest.php +++ b/tests/EventSubscriber/WizardSubscriberTest.php @@ -23,6 +23,7 @@ class WizardSubscriberTest extends TestCase $events = WizardSubscriber::getSubscribedEvents(); self::assertArrayHasKey(KernelEvents::REQUEST, $events); $methodName = $events[KernelEvents::REQUEST][0]; + self::assertIsString($methodName); self::assertTrue(method_exists(WizardSubscriber::class, $methodName)); } } diff --git a/tests/Export/Base/CsvRendererTest.php b/tests/Export/Base/CsvRendererTest.php index 0efbee48..58dbdadc 100644 --- a/tests/Export/Base/CsvRendererTest.php +++ b/tests/Export/Base/CsvRendererTest.php @@ -109,6 +109,7 @@ class CsvRendererTest extends AbstractRendererTestCase '0', '0', '84', + 'Kevin', 'kevin', '', 'Customer Name', @@ -142,6 +143,7 @@ class CsvRendererTest extends AbstractRendererTestCase '0', '0', '-100.92', + 'niveK', 'nivek', '', 'Customer Name', @@ -168,6 +170,6 @@ class CsvRendererTest extends AbstractRendererTestCase self::assertEquals($expected, $all[5]); self::assertEquals($expected2, $all[6]); self::assertEquals(\count($expected), \count($all[0])); - self::assertEquals('foo', $all[4][16]); + self::assertEquals('foo', $all[4][17]); } } diff --git a/tests/Export/Renderer/AbstractRendererTestCase.php b/tests/Export/Renderer/AbstractRendererTestCase.php index ad74791c..cd7fe920 100644 --- a/tests/Export/Renderer/AbstractRendererTestCase.php +++ b/tests/Export/Renderer/AbstractRendererTestCase.php @@ -52,17 +52,19 @@ abstract class AbstractRendererTestCase extends KernelTestCase $activity->setProject($project); $activity->setMetaField((new ActivityMeta())->setName('activity-foo')->setValue('activity-bar')->setIsVisible(true)); - $userMethods = ['getId', 'getPreferenceValue', 'getUsername', 'getUserIdentifier']; + $userMethods = ['getId', 'getPreferenceValue', 'getUsername', 'getUserIdentifier', 'getAlias']; $user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock(); $user1->method('getId')->willReturn(1); $user1->method('getPreferenceValue')->willReturn('50'); - $user1->method('getUsername')->willReturn('foo-bar'); + $user1->method('getAlias')->willReturn('Foo Bar'); $user1->method('getUserIdentifier')->willReturn('foo-bar'); + $user1->method('getUsername')->willReturn('foo-bar'); $user2 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock(); $user2->method('getId')->willReturn(2); - $user2->method('getUsername')->willReturn('hello-world'); + $user2->method('getAlias')->willReturn('Hello World'); $user2->method('getUserIdentifier')->willReturn('hello-world'); + $user2->method('getUsername')->willReturn('hello-world'); $timesheet = new Timesheet(); $timesheet @@ -111,6 +113,7 @@ abstract class AbstractRendererTestCase extends KernelTestCase ; $userKevin = new User(); + $userKevin->setAlias('Kevin'); $userKevin->setUserIdentifier('kevin'); $timesheet5 = new Timesheet(); @@ -129,6 +132,7 @@ abstract class AbstractRendererTestCase extends KernelTestCase ; $userNivek = new User(); + $userNivek->setAlias('niveK'); $userNivek->setUserIdentifier('nivek'); $timesheet6 = new Timesheet(); diff --git a/tests/Invoice/InvoiceModelTest.php b/tests/Invoice/InvoiceModelTest.php index 9083ed7d..b8e2eecb 100644 --- a/tests/Invoice/InvoiceModelTest.php +++ b/tests/Invoice/InvoiceModelTest.php @@ -32,6 +32,8 @@ class InvoiceModelTest extends TestCase self::assertNotNull($sut->getCustomer()); self::assertNotNull($sut->getTemplate()); + self::assertFalse($sut->isPreview()); + self::assertFalse($sut->isHideZeroTax()); self::assertNull($sut->getCalculator()); self::assertEmpty($sut->getEntries()); self::assertIsArray($sut->getEntries()); @@ -43,6 +45,12 @@ class InvoiceModelTest extends TestCase $sut->setFormatter($newFormatter); self::assertNotSame($formatter, $sut->getFormatter()); self::assertSame($newFormatter, $sut->getFormatter()); + self::assertEquals([], $sut->getOptions()); + + $sut->setPreview(true); + $sut->setHideZeroTax(true); + self::assertTrue($sut->isPreview()); + self::assertTrue($sut->isHideZeroTax()); } public function testEmptyObjectThrowsExceptionOnNumberGenerator(): void @@ -75,6 +83,23 @@ class InvoiceModelTest extends TestCase self::assertSame($template, $sut->getTemplate()); self::assertInstanceOf(\DateTimeInterface::class, $sut->getDueDate()); + + $sut->setOption('foo-int', 123); + $sut->setOption('foo-float', 123.45); + $sut->setOption('foo-string', '12345'); + $sut->setOption('foo-null', null); + $sut->setOption('foo-bool', true); + $sut->setOption('foo-array', ['foo' => 'bar']); + $sut->setOption('foo-array2', ['foo', 'bar']); + self::assertEquals([ + 'foo-int' => 123, + 'foo-float' => 123.45, + 'foo-string' => '12345', + 'foo-null' => null, + 'foo-bool' => true, + 'foo-array' => ['foo' => 'bar'], + 'foo-array2' => ['foo', 'bar'] + ], $sut->getOptions()); } public function testDueDate(): void diff --git a/tests/Twig/Runtime/ThemeEventExtensionTest.php b/tests/Twig/Runtime/ThemeEventExtensionTest.php index 37c59b9b..0440699c 100644 --- a/tests/Twig/Runtime/ThemeEventExtensionTest.php +++ b/tests/Twig/Runtime/ThemeEventExtensionTest.php @@ -50,7 +50,7 @@ class ThemeEventExtensionTest extends TestCase ]; } - protected function getSut(bool $hasListener = true, string $title = null): ThemeExtension + protected function getSut(bool $hasListener = true): ThemeExtension { $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher->method('hasListeners')->willReturn($hasListener); @@ -155,18 +155,6 @@ class ThemeEventExtensionTest extends TestCase public function testGetTitle(): void { $sut = $this->getSut(false); - self::assertEquals('Kimai – foo', $sut->generateTitle()); - self::assertEquals('sdfsdf | Kimai – foo', $sut->generateTitle('sdfsdf | ')); - self::assertEquals('Kimai ... foo', $sut->generateTitle('', ' ... ')); - self::assertEquals('Kimai | foo', $sut->generateTitle(null, ' | ')); - } - - public function testGetBrandedTitle(): void - { - $sut = $this->getSut(false, 'MyCompany'); - self::assertEquals('Kimai – foo', $sut->generateTitle()); - self::assertEquals('sdfsdf | Kimai – foo', $sut->generateTitle('sdfsdf | ')); - self::assertEquals('Kimai ... foo', $sut->generateTitle('', ' ... ')); - self::assertEquals('Kimai | foo', $sut->generateTitle(null, ' | ')); + self::assertEquals('Kimai', $sut->generateTitle()); } } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index cf8db67d..24910436 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -1311,51 +1311,11 @@ parameters: count: 1 path: EventSubscriber/Actions/AbstractActionsSubscriberTestCase.php - - - message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\\\|int\\|string given\\.$#" - count: 1 - path: EventSubscriber/EmailSubscriberTest.php - - - - message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\\\|int\\|string\\>\\|string given\\.$#" - count: 2 - path: EventSubscriber/LastLoginSubscriberTest.php - - - - message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\\\|int\\|string given\\.$#" - count: 1 - path: EventSubscriber/MenuSubscriberTest.php - - - - message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\\\|int\\|string given\\.$#" - count: 1 - path: EventSubscriber/PagerfantaExceptionSubscriberTest.php - - message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\ProfileSubscriberTest\\:\\:getInvalidCookies\\(\\) has no return type specified\\.$#" count: 1 path: EventSubscriber/ProfileSubscriberTest.php - - - message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\\\|int\\|string\\>\\|string given\\.$#" - count: 1 - path: EventSubscriber/ProfileSubscriberTest.php - - - - message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\\\|int\\|string given\\.$#" - count: 1 - path: EventSubscriber/UserDetailsSubscriberTest.php - - - - message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\\\|int\\|string given\\.$#" - count: 1 - path: EventSubscriber/UserPreferenceSubscriberTest.php - - - - message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\\\|int\\|string given\\.$#" - count: 1 - path: EventSubscriber/WizardSubscriberTest.php - - message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#" count: 6 diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 647b3804..c5b17ade 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -614,6 +614,14 @@ complete_month.help Dies wird alle Tage des Jahres vor dem gewählten Datum abschliessen. Der Benutzer wird für den abgeschlossenen Zeitraum keine Zeiten mehr anlegen oder bearbeiten können. + + Unlock month + Monat entsperren + + + unlock_month.help + Dadurch werden alle Tage des Jahres nach dem ausgewählten Datum entsperrt. + completed_month_pdf Monats-Abschluss PDF diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index b80c3d2e..db86ccda 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -614,6 +614,14 @@ complete_month.help This will lock all days of the year before the chosen date. The user will no longer be able to create or edit times for the locked period. + + Unlock month + Unlock month + + + unlock_month.help + This unlocks all days of the year after the selected date. + completed_month_pdf Completed month PDF diff --git a/translations/plugins.de.xlf b/translations/plugins.de.xlf index c7253736..7ac17feb 100644 --- a/translations/plugins.de.xlf +++ b/translations/plugins.de.xlf @@ -18,6 +18,10 @@ Plugin Shop Mehr Erweiterungen + + Update available + Update verfügbar: %version% + diff --git a/translations/plugins.en.xlf b/translations/plugins.en.xlf index 4b00430c..a87e80e2 100644 --- a/translations/plugins.en.xlf +++ b/translations/plugins.en.xlf @@ -18,6 +18,10 @@ Plugin Shop More extensions + + Update available + Update available: %version% + diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 98560a57..3b6a6f68 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -158,6 +158,10 @@ An absence cannot be booked after the last working day. Eine Abwesenheit kann nicht nach dem letzten Arbeitstag gebucht werden. + + Selected period cannot be locked: unconfirmed absence requests are pending. + Ausgewählter Zeitraum kann nicht gesperrt werden: unbestätigte Abwesenheitsanträge stehen an. + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 6cf7043e..7d5cf84d 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -158,6 +158,10 @@ An absence cannot be booked after the last working day. An absence cannot be booked after the last working day. + + Selected period cannot be locked: unconfirmed absence requests are pending. + Selected period cannot be locked: unconfirmed absence requests are pending. +