From 6531e7fe52ce362636d4e7903575e211c25564a8 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 10 Jan 2024 12:43:07 +0100 Subject: [PATCH] Release 2.9.0 (#4526) * added fix to work around bc break in new phpword version * fix phpoffice deprecations * mark unused option as deprecated * support for DateTimeInterface and DateTimeImmutable where possible * use TRUSTED_PROXIES setting - fixes #4533 * re-enable Kimai test in docker test build script (#4541) * bump dependencies --- .docker/test-lite.sh | 5 +- .github/workflows/docker.yaml | 5 +- composer.json | 2 +- composer.lock | 250 +++++++++--------- config/packages/framework.yaml | 14 +- config/services.yaml | 4 +- phpstan.neon | 125 --------- src/Activity/ActivityStatisticService.php | 58 ++-- src/Command/ExportCreateCommand.php | 11 +- src/Command/InvoiceCreateCommand.php | 17 +- src/Constants.php | 4 +- .../Reporting/ProjectDateRangeController.php | 12 +- src/Customer/CustomerStatisticService.php | 53 ++-- src/DependencyInjection/AppExtension.php | 3 +- src/Event/ActivityBudgetStatisticEvent.php | 20 +- src/Event/ActivityStatisticEvent.php | 26 +- src/Event/ProjectBudgetStatisticEvent.php | 26 +- src/Event/ProjectStatisticEvent.php | 11 +- .../Base/AbstractSpreadsheetRenderer.php | 2 +- .../CellFormatter/DateFormatter.php | 6 +- .../CellFormatter/DateTimeFormatter.php | 6 +- .../CellFormatter/TimeFormatter.php | 4 +- src/Form/Model/DateRange.php | 8 +- src/Form/Type/DatePickerType.php | 4 +- .../Hydrator/InvoiceModelCustomerHydrator.php | 4 +- src/Invoice/Renderer/DocxRenderer.php | 7 +- src/Model/TimesheetCountedStatistic.php | 70 +++++ src/Project/ProjectStatisticService.php | 44 ++- src/Repository/Query/DateRangeInterface.php | 30 +++ src/Repository/Query/DateRangeTrait.php | 8 +- src/Repository/Query/InvoiceArchiveQuery.php | 2 +- src/Repository/Query/TimesheetQuery.php | 2 +- src/Twig/LocaleFormatExtensions.php | 4 +- .../TimesheetBudgetUsedValidator.php | 12 +- .../TimesheetLockdownValidator.php | 2 +- symfony.lock | 20 +- .../ActivityBudgetStatisticEventTest.php | 6 +- tests/Event/ActivityStatisticEventTest.php | 6 +- .../Event/ProjectBudgetStatisticEventTest.php | 6 +- tests/Event/ProjectStatisticEventTest.php | 6 +- .../CellFormatter/DateFormatterTest.php | 2 +- .../CellFormatter/DateTimeFormatterTest.php | 2 +- .../CellFormatter/TimeFormatterTest.php | 2 +- tests/Invoice/Renderer/DocxRendererTest.php | 6 +- tests/Repository/Query/BaseQueryTest.php | 20 +- tests/phpstan.neon | 35 --- 46 files changed, 487 insertions(+), 485 deletions(-) create mode 100644 src/Repository/Query/DateRangeInterface.php diff --git a/.docker/test-lite.sh b/.docker/test-lite.sh index 1ba6699e..6aa38f09 100755 --- a/.docker/test-lite.sh +++ b/.docker/test-lite.sh @@ -1,6 +1,9 @@ #!/bin/sh -e -# Test PHP/Kimai +if [ -z "$DATABASE_URL" ]; then + DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=5.7.40" +fi + /opt/kimai/bin/console kimai:version if [ $? != 0 ]; then echo "PHP/Kimai not responding" diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 155f211a..2c933797 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -49,9 +49,8 @@ jobs: kimai/kimai2:${{ matrix.server }}-${{ matrix.type }} kimai/kimai2:${{ matrix.server }}-${{ env.kimai_version }}-${{ matrix.type }} push: true -# see https://github.com/kimai/kimai/issues/4503 -# - name: Test Lite -# run: docker run --rm --entrypoint /assets/test-lite.sh kimai/kimai2:${{ matrix.server }}-${{ matrix.type }} + - name: Test Lite + run: docker run --rm --entrypoint /assets/test-lite.sh kimai/kimai2:${{ matrix.server }}-${{ matrix.type }} tag: needs: build diff --git a/composer.json b/composer.json index a12207bb..070a91a1 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "onelogin/php-saml": "^4.0", "pagerfanta/pagerfanta": "^3.0", "phpoffice/phpspreadsheet": "^1.16", - "phpoffice/phpword": "1.1.*", + "phpoffice/phpword": "^1.0", "psr/container": "^2.0", "psr/log": "^3.0", "scheb/2fa-backup-code": "^6.2", diff --git a/composer.lock b/composer.lock index 804572e7..b5d53ec8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,35 +4,34 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7b0ed62d4f9b4d06a6748c700fd04238", + "content-hash": "a307b9a36c22711439283a3e5bc3249c", "packages": [ { "name": "azuyalabs/yasumi", - "version": "2.6.0", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/azuyalabs/yasumi.git", - "reference": "a73f198d796100c237adf94f3fca1d15eb82b22b" + "reference": "37d1215d4f4012d3185bb9990c76ca17a4ff1c30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/azuyalabs/yasumi/zipball/a73f198d796100c237adf94f3fca1d15eb82b22b", - "reference": "a73f198d796100c237adf94f3fca1d15eb82b22b", + "url": "https://api.github.com/repos/azuyalabs/yasumi/zipball/37d1215d4f4012d3185bb9990c76ca17a4ff1c30", + "reference": "37d1215d4f4012d3185bb9990c76ca17a4ff1c30", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.4" + "php": ">=8.0" }, "require-dev": { "ext-intl": "*", - "friendsofphp/php-cs-fixer": "^2.19 || ^3.16", - "infection/infection": "^0.17 || ^0.26", + "friendsofphp/php-cs-fixer": "^2.19 || ^3.40", "mikey179/vfsstream": "^1.6", "phan/phan": "^5.4", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^8.5 || ^9.6", - "vimeo/psalm": "^5.9" + "vimeo/psalm": "^5.16" }, "suggest": { "ext-calendar": "For calculating the date of Easter" @@ -78,7 +77,7 @@ "type": "other" } ], - "time": "2023-04-26T15:35:45+00:00" + "time": "2024-01-07T14:12:44+00:00" }, { "name": "bacon/bacon-qr-code", @@ -2519,68 +2518,6 @@ ], "time": "2023-12-01T15:39:05+00:00" }, - { - "name": "laminas/laminas-escaper", - "version": "2.13.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-escaper.git", - "reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/af459883f4018d0f8a0c69c7a209daef3bf973ba", - "reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-mbstring": "*", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0" - }, - "conflict": { - "zendframework/zend-escaper": "*" - }, - "require-dev": { - "infection/infection": "^0.27.0", - "laminas/laminas-coding-standard": "~2.5.0", - "maglnet/composer-require-checker": "^3.8.0", - "phpunit/phpunit": "^9.6.7", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.9" - }, - "type": "library", - "autoload": { - "psr-4": { - "Laminas\\Escaper\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", - "homepage": "https://laminas.dev", - "keywords": [ - "escaper", - "laminas" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-escaper/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-escaper/issues", - "rss": "https://github.com/laminas/laminas-escaper/releases.atom", - "source": "https://github.com/laminas/laminas-escaper" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2023-10-10T08:35:13+00:00" - }, { "name": "league/csv", "version": "9.14.0", @@ -3246,16 +3183,16 @@ }, { "name": "nelmio/api-doc-bundle", - "version": "v4.15.1", + "version": "v4.16.2", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "bef8b654ed410d16bf1086e2319ab215e5f36193" + "reference": "31da761b6c9d275fb3bbee87c4c6888b17aec4ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/bef8b654ed410d16bf1086e2319ab215e5f36193", - "reference": "bef8b654ed410d16bf1086e2319ab215e5f36193", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/31da761b6c9d275fb3bbee87c4c6888b17aec4ad", + "reference": "31da761b6c9d275fb3bbee87c4c6888b17aec4ad", "shasum": "" }, "require": { @@ -3352,9 +3289,9 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.15.1" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.16.2" }, - "time": "2024-01-02T14:56:33+00:00" + "time": "2024-01-06T21:33:48+00:00" }, { "name": "nelmio/cors-bundle", @@ -3862,6 +3799,59 @@ }, "time": "2023-08-12T11:01:26+00:00" }, + { + "name": "phpoffice/math", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/Math.git", + "reference": "f0f8cad98624459c540cdd61d2a174d834471773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/f0f8cad98624459c540cdd61d2a174d834471773", + "reference": "f0f8cad98624459c540cdd61d2a174d834471773", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^7.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\Math\\": "src/Math/", + "Tests\\PhpOffice\\Math\\": "tests/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "Math - Manipulate Math Formula", + "homepage": "https://phpoffice.github.io/Math/", + "keywords": [ + "MathML", + "officemathml", + "php" + ], + "support": { + "issues": "https://github.com/PHPOffice/Math/issues", + "source": "https://github.com/PHPOffice/Math/tree/0.1.0" + }, + "time": "2023-09-25T12:08:20+00:00" + }, { "name": "phpoffice/phpspreadsheet", "version": "1.29.0", @@ -3969,24 +3959,24 @@ }, { "name": "phpoffice/phpword", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PHPWord.git", - "reference": "90a55955e6a772bb4cd9b1ef6a7e88c8976c2561" + "reference": "e76b701ef538cb749641514fcbc31a68078550fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/90a55955e6a772bb4cd9b1ef6a7e88c8976c2561", - "reference": "90a55955e6a772bb4cd9b1ef6a7e88c8976c2561", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/e76b701ef538cb749641514fcbc31a68078550fa", + "reference": "e76b701ef538cb749641514fcbc31a68078550fa", "shasum": "" }, "require": { "ext-dom": "*", "ext-json": "*", "ext-xml": "*", - "laminas/laminas-escaper": ">=2.6", - "php": "^7.1|^8.0" + "php": "^7.1|^8.0", + "phpoffice/math": "^0.1" }, "require-dev": { "dompdf/dompdf": "^2.0", @@ -3996,8 +3986,9 @@ "friendsofphp/php-cs-fixer": "^3.3", "mpdf/mpdf": "^8.1", "phpmd/phpmd": "^2.13", + "phpstan/phpstan-phpunit": "@stable", "phpunit/phpunit": ">=7.0", - "symfony/process": "^4.4", + "symfony/process": "^4.4 || ^5.0", "tecnickcom/tcpdf": "^6.5" }, "suggest": { @@ -4043,7 +4034,7 @@ } ], "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", - "homepage": "https://phpword.readthedocs.io/", + "homepage": "https://phpoffice.github.io/PHPWord/", "keywords": [ "ISO IEC 29500", "OOXML", @@ -4071,22 +4062,22 @@ ], "support": { "issues": "https://github.com/PHPOffice/PHPWord/issues", - "source": "https://github.com/PHPOffice/PHPWord/tree/1.1.0" + "source": "https://github.com/PHPOffice/PHPWord/tree/1.2.0" }, - "time": "2023-05-30T07:59:14+00:00" + "time": "2023-11-30T11:22:23+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.5", + "version": "1.25.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc" + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fedf211ff14ec8381c9bf5714e33a7a552dd1acc", - "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", "shasum": "" }, "require": { @@ -4118,9 +4109,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.5" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" }, - "time": "2023-12-16T09:33:33+00:00" + "time": "2024-01-04T17:06:16+00:00" }, { "name": "psr/cache", @@ -10340,16 +10331,16 @@ }, { "name": "zircote/swagger-php", - "version": "4.8.2", + "version": "4.8.3", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "a70a5dc5db26183c86f589ff3d76fb11e141fc58" + "reference": "598958d8a83cfbd44ba36388b2f9ed69e8b86ed4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/a70a5dc5db26183c86f589ff3d76fb11e141fc58", - "reference": "a70a5dc5db26183c86f589ff3d76fb11e141fc58", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/598958d8a83cfbd44ba36388b2f9ed69e8b86ed4", + "reference": "598958d8a83cfbd44ba36388b2f9ed69e8b86ed4", "shasum": "" }, "require": { @@ -10415,9 +10406,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.8.2" + "source": "https://github.com/zircote/swagger-php/tree/4.8.3" }, - "time": "2023-12-19T19:59:07+00:00" + "time": "2024-01-07T22:33:09+00:00" } ], "packages-dev": [ @@ -10861,21 +10852,22 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.45.0", + "version": "v3.46.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "c0daa33cb2533cd73f48dde1c70c2afa3e7953b5" + "reference": "be6831c9af1740470d2a773119b9273f8ac1c3d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c0daa33cb2533cd73f48dde1c70c2afa3e7953b5", - "reference": "c0daa33cb2533cd73f48dde1c70c2afa3e7953b5", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/be6831c9af1740470d2a773119b9273f8ac1c3d2", + "reference": "be6831c9af1740470d2a773119b9273f8ac1c3d2", "shasum": "" }, "require": { "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", "ext-json": "*", "ext-tokenizer": "*", "php": "^7.4 || ^8.0", @@ -10939,7 +10931,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.45.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.46.0" }, "funding": [ { @@ -10947,7 +10939,7 @@ "type": "github" } ], - "time": "2023-12-30T02:07:07+00:00" + "time": "2024-01-03T21:38:46+00:00" }, { "name": "masterminds/html5", @@ -11018,25 +11010,27 @@ }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -11044,7 +11038,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -11068,9 +11062,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" }, - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-01-07T17:17:35+00:00" }, { "name": "phar-io/manifest", @@ -11185,16 +11179,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.50", + "version": "1.10.55", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" + "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", + "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", "shasum": "" }, "require": { @@ -11243,25 +11237,25 @@ "type": "tidelift" } ], - "time": "2023-12-13T10:59:42+00:00" + "time": "2024-01-08T12:32:40+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.3.53", + "version": "1.3.54", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "85def57e5db6ac6c8a512200c0cfadf7b6621b10" + "reference": "f9555a2d54d685efd7003ae33c15e3d19d7d0c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/85def57e5db6ac6c8a512200c0cfadf7b6621b10", - "reference": "85def57e5db6ac6c8a512200c0cfadf7b6621b10", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/f9555a2d54d685efd7003ae33c15e3d19d7d0c36", + "reference": "f9555a2d54d685efd7003ae33c15e3d19d7d0c36", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.12" + "phpstan/phpstan": "^1.10.48" }, "conflict": { "doctrine/collections": "<1.0", @@ -11278,7 +11272,7 @@ "doctrine/dbal": "^2.13.8 || ^3.3.3", "doctrine/lexer": "^1.2.1", "doctrine/mongodb-odm": "^1.3 || ^2.1", - "doctrine/orm": "^2.11.0", + "doctrine/orm": "^2.14.0", "doctrine/persistence": "^1.3.8 || ^2.2.1", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", @@ -11311,9 +11305,9 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.53" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.54" }, - "time": "2023-11-21T10:31:58+00:00" + "time": "2024-01-05T15:44:44+00:00" }, { "name": "phpstan/phpstan-phpunit", diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 9848d766..f90908e0 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -1,21 +1,23 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +parameters: + env(TRUSTED_PROXIES): '' + framework: - # Deprecations for 7.0 + secret: '%env(APP_SECRET)%' + csrf_protection: true + annotations: false handle_all_throwables: true - annotations: - enabled: false serializer: enable_attributes: true - # --------------------------- + trusted_proxies: '%env(string:TRUSTED_PROXIES)%' exceptions: App\Validator\ValidationFailedException: log_level: debug - secret: '%env(APP_SECRET)%' default_locale: en - csrf_protection: true http_method_override: false # Enables session support. Note that the session will ONLY be started if you read or write from it. diff --git a/config/services.yaml b/config/services.yaml index 44e030ab..1bdb80b0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -16,8 +16,8 @@ services: $projectDirectory: '%kernel.project_dir%' $kernelEnvironment: '%kernel.environment%' - # make classes in src/ available to be used as services - # creates one service per class whose id is the fully-qualified class name + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/*' exclude: diff --git a/phpstan.neon b/phpstan.neon index f763cc4b..f2a4415a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -151,46 +151,6 @@ parameters: count: 1 path: src/API/UserController.php - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/Activity/ActivityStatisticService.php - - - - message: "#^Cannot access offset 'billable' on mixed\\.$#" - count: 1 - path: src/Activity/ActivityStatisticService.php - - - - message: "#^Cannot access offset 'counter' on mixed\\.$#" - count: 3 - path: src/Activity/ActivityStatisticService.php - - - - message: "#^Cannot access offset 'duration' on mixed\\.$#" - count: 4 - path: src/Activity/ActivityStatisticService.php - - - - message: "#^Cannot access offset 'exported' on mixed\\.$#" - count: 2 - path: src/Activity/ActivityStatisticService.php - - - - message: "#^Cannot access offset 'id' on mixed\\.$#" - count: 1 - path: src/Activity/ActivityStatisticService.php - - - - message: "#^Cannot access offset 'internalRate' on mixed\\.$#" - count: 3 - path: src/Activity/ActivityStatisticService.php - - - - message: "#^Cannot access offset 'rate' on mixed\\.$#" - count: 4 - path: src/Activity/ActivityStatisticService.php - - message: "#^Method App\\\\Activity\\\\ActivityStatisticService\\:\\:createStatisticQueryBuilder\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#" count: 1 @@ -1106,21 +1066,6 @@ parameters: count: 1 path: src/Controller/Reporting/CustomerMonthlyProjectsController.php - - - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" - count: 3 - path: src/Controller/Reporting/ProjectDateRangeController.php - - - - message: "#^Parameter \\#2 \\$begin of method App\\\\Project\\\\ProjectStatisticService\\:\\:getBudgetStatisticModelForProjectsByDateRange\\(\\) expects DateTime, DateTime\\|null given\\.$#" - count: 1 - path: src/Controller/Reporting/ProjectDateRangeController.php - - - - message: "#^Parameter \\#3 \\$end of method App\\\\Project\\\\ProjectStatisticService\\:\\:getBudgetStatisticModelForProjectsByDateRange\\(\\) expects DateTime, DateTime\\|null given\\.$#" - count: 1 - path: src/Controller/Reporting/ProjectDateRangeController.php - - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" count: 3 @@ -1291,46 +1236,6 @@ parameters: count: 1 path: src/Controller/WidgetController.php - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/Customer/CustomerStatisticService.php - - - - message: "#^Cannot access offset 'billable' on mixed\\.$#" - count: 1 - path: src/Customer/CustomerStatisticService.php - - - - message: "#^Cannot access offset 'counter' on mixed\\.$#" - count: 3 - path: src/Customer/CustomerStatisticService.php - - - - message: "#^Cannot access offset 'duration' on mixed\\.$#" - count: 4 - path: src/Customer/CustomerStatisticService.php - - - - message: "#^Cannot access offset 'exported' on mixed\\.$#" - count: 2 - path: src/Customer/CustomerStatisticService.php - - - - message: "#^Cannot access offset 'id' on mixed\\.$#" - count: 1 - path: src/Customer/CustomerStatisticService.php - - - - message: "#^Cannot access offset 'internalRate' on mixed\\.$#" - count: 3 - path: src/Customer/CustomerStatisticService.php - - - - message: "#^Cannot access offset 'rate' on mixed\\.$#" - count: 4 - path: src/Customer/CustomerStatisticService.php - - message: "#^Method App\\\\Customer\\\\CustomerStatisticService\\:\\:createStatisticQueryBuilder\\(\\) has parameter \\$customers with no value type specified in iterable type array\\.$#" count: 1 @@ -3686,11 +3591,6 @@ parameters: count: 1 path: src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php - - - message: "#^Parameter \\#2 \\$today of method App\\\\Customer\\\\CustomerStatisticService\\:\\:getBudgetStatisticModel\\(\\) expects DateTime, DateTime\\|null given\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php - - message: "#^Cannot call method getAddress\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" count: 1 @@ -5746,11 +5646,6 @@ parameters: count: 2 path: src/Validator/Constraints/TeamValidator.php - - - message: "#^Cannot call method format\\(\\) on DateTime\\|null\\.$#" - count: 2 - path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - message: "#^Cannot call method getCurrency\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" count: 1 @@ -5771,31 +5666,11 @@ parameters: count: 1 path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - - message: "#^Cannot call method getTimezone\\(\\) on DateTime\\|null\\.$#" - count: 1 - path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetBudgetUsedValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#" count: 2 path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - - message: "#^Parameter \\#2 \\$today of method App\\\\Activity\\\\ActivityStatisticService\\:\\:getBudgetStatisticModel\\(\\) expects DateTime, DateTime\\|null given\\.$#" - count: 1 - path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - - - message: "#^Parameter \\#2 \\$today of method App\\\\Customer\\\\CustomerStatisticService\\:\\:getBudgetStatisticModel\\(\\) expects DateTime, DateTime\\|null given\\.$#" - count: 1 - path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - - - message: "#^Parameter \\#2 \\$today of method App\\\\Project\\\\ProjectStatisticService\\:\\:getBudgetStatisticModel\\(\\) expects DateTime, DateTime\\|null given\\.$#" - count: 1 - path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - message: "#^Parameter \\#4 \\$duration of method App\\\\Validator\\\\Constraints\\\\TimesheetBudgetUsedValidator\\:\\:checkBudgets\\(\\) expects int, float\\|int\\|null given\\.$#" count: 3 diff --git a/src/Activity/ActivityStatisticService.php b/src/Activity/ActivityStatisticService.php index d6479385..cf88d546 100644 --- a/src/Activity/ActivityStatisticService.php +++ b/src/Activity/ActivityStatisticService.php @@ -16,7 +16,8 @@ use App\Model\ActivityBudgetStatisticModel; use App\Model\ActivityStatistic; use App\Repository\TimesheetRepository; use App\Timesheet\DateTimeFactory; -use DateTime; +use DateTimeImmutable; +use DateTimeInterface; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\QueryBuilder; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -26,19 +27,15 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; */ class ActivityStatisticService { - public function __construct(private TimesheetRepository $timesheetRepository, private EventDispatcherInterface $dispatcher) + public function __construct(private readonly TimesheetRepository $timesheetRepository, private readonly EventDispatcherInterface $dispatcher) { } /** - * WARNING: this method does not respect the budget type. Your results will always be wither the "full lifetime data" or the "selected date-range". - * - * @param Activity $activity - * @param DateTime|null $begin - * @param DateTime|null $end - * @return ActivityStatistic + * WARNING: this method does not respect the budget type. + * Your results will always be with the "full lifetime data" or the "selected date-range". */ - public function getActivityStatistics(Activity $activity, ?DateTime $begin = null, ?DateTime $end = null): ActivityStatistic + public function getActivityStatistics(Activity $activity, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): ActivityStatistic { $statistics = $this->getBudgetStatistic([$activity], $begin, $end); $event = new ActivityStatisticEvent($activity, array_pop($statistics), $begin, $end); @@ -47,7 +44,7 @@ class ActivityStatisticService return $event->getStatistic(); } - public function getBudgetStatisticModel(Activity $activity, DateTime $today): ActivityBudgetStatisticModel + public function getBudgetStatisticModel(Activity $activity, DateTimeInterface $today): ActivityBudgetStatisticModel { $stats = new ActivityBudgetStatisticModel($activity); $stats->setStatisticTotal($this->getActivityStatistics($activity)); @@ -68,10 +65,9 @@ class ActivityStatisticService /** * @param Activity[] $activities - * @param DateTime $today * @return ActivityBudgetStatisticModel[] */ - public function getBudgetStatisticModelForActivities(array $activities, DateTime $today): array + public function getBudgetStatisticModelForActivities(array $activities, DateTimeInterface $today): array { $models = []; $monthly = []; @@ -121,11 +117,9 @@ class ActivityStatisticService /** * @param Activity[] $activities - * @param DateTime|null $begin - * @param DateTime|null $end * @return array */ - private function getBudgetStatistic(array $activities, ?DateTime $begin = null, ?DateTime $end = null): array + private function getBudgetStatistic(array $activities, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): array { $statistics = []; foreach ($activities as $activity) { @@ -139,25 +133,25 @@ class ActivityStatisticService if (null !== $result) { foreach ($result as $resultRow) { $statistic = $statistics[$resultRow['id']]; - $statistic->setDuration($statistic->getDuration() + $resultRow['duration']); - $statistic->setRate($statistic->getRate() + $resultRow['rate']); - $statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']); - $statistic->setCounter($statistic->getCounter() + $resultRow['counter']); + $statistic->addDuration((int) $resultRow['duration']); + $statistic->addRate((float) $resultRow['rate']); + $statistic->addInternalRate((float) $resultRow['internalRate']); + $statistic->addCounter((int) $resultRow['counter']); if ($resultRow['billable']) { - $statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']); - $statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']); - $statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']); - $statistic->setCounterBillable($statistic->getCounterBillable() + $resultRow['counter']); + $statistic->addDurationBillable((int) $resultRow['duration']); + $statistic->addRateBillable((float) $resultRow['rate']); + $statistic->addInternalRateBillable((float) $resultRow['internalRate']); + $statistic->addCounterBillable((int) $resultRow['counter']); if ($resultRow['exported']) { - $statistic->setDurationBillableExported($statistic->getDurationBillableExported() + $resultRow['duration']); - $statistic->setRateBillableExported($statistic->getRateBillableExported() + $resultRow['rate']); + $statistic->addDurationBillableExported((int) $resultRow['duration']); + $statistic->addRateBillableExported((float) $resultRow['rate']); } } if ($resultRow['exported']) { - $statistic->setDurationExported($statistic->getDurationExported() + $resultRow['duration']); - $statistic->setRateExported($statistic->getRateExported() + $resultRow['rate']); - $statistic->setInternalRateExported($statistic->getInternalRateExported() + $resultRow['internalRate']); - $statistic->setCounterExported($statistic->getCounterExported() + $resultRow['counter']); + $statistic->addDurationExported((int) $resultRow['duration']); + $statistic->addRateExported((float) $resultRow['rate']); + $statistic->addInternalRateExported((float) $resultRow['internalRate']); + $statistic->addCounterExported((int) $resultRow['counter']); } } } @@ -165,7 +159,7 @@ class ActivityStatisticService return $statistics; } - private function createStatisticQueryBuilder(array $activities, DateTime $begin = null, ?DateTime $end = null): QueryBuilder + private function createStatisticQueryBuilder(array $activities, \DateTimeInterface $begin = null, ?\DateTimeInterface $end = null): QueryBuilder { $qb = $this->timesheetRepository->createQueryBuilder('t'); $qb @@ -187,14 +181,14 @@ class ActivityStatisticService if ($begin !== null) { $qb ->andWhere($qb->expr()->gte('t.begin', ':begin')) - ->setParameter('begin', $begin, Types::DATETIME_MUTABLE) + ->setParameter('begin', DateTimeImmutable::createFromInterface($begin), Types::DATETIME_IMMUTABLE) ; } if ($end !== null) { $qb ->andWhere($qb->expr()->lte('t.begin', ':end')) - ->setParameter('end', $end, Types::DATETIME_MUTABLE) + ->setParameter('end', DateTimeImmutable::createFromInterface($end), Types::DATETIME_IMMUTABLE) ; } diff --git a/src/Command/ExportCreateCommand.php b/src/Command/ExportCreateCommand.php index 41eea9a2..0970cb68 100644 --- a/src/Command/ExportCreateCommand.php +++ b/src/Command/ExportCreateCommand.php @@ -158,10 +158,12 @@ final class ExportCreateCommand extends Command return Command::FAILURE; } } - if (!$start instanceof \DateTime) { + if (!$start instanceof \DateTimeInterface) { $start = $dateFactory->getStartOfMonth(); } - $start->setTime(0, 0, 0); + + $start = \DateTimeImmutable::createFromInterface($start); + $start = $start->setTime(0, 0, 0); $end = $input->getOption('end'); if (!empty($end)) { @@ -174,11 +176,12 @@ final class ExportCreateCommand extends Command } } - if (empty($end)) { + if (!$end instanceof \DateTimeInterface) { $end = $dateFactory->getEndOfMonth($start); } - $end->setTime(23, 59, 59); + $end = \DateTimeImmutable::createFromInterface($end); + $end = $end->setTime(23, 59, 59); $directory = rtrim(sys_get_temp_dir(), '/') . '/'; if ($input->getOption('directory') !== null) { diff --git a/src/Command/InvoiceCreateCommand.php b/src/Command/InvoiceCreateCommand.php index a88bbe16..c14231a3 100644 --- a/src/Command/InvoiceCreateCommand.php +++ b/src/Command/InvoiceCreateCommand.php @@ -64,7 +64,7 @@ final class InvoiceCreateCommand extends Command ->addOption('project', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of project IDs', null) ->addOption('by-customer', null, InputOption::VALUE_NONE, 'If set, one invoice for each active customer in the given timerange is created') ->addOption('by-project', null, InputOption::VALUE_NONE, 'If set, one invoice for each active project in the given timerange is created') - ->addOption('set-exported', null, InputOption::VALUE_NONE, 'Whether the invoice items should be marked as exported') + ->addOption('set-exported', null, InputOption::VALUE_NONE, '[DEPRECATED] this flag has no meaning any more: invoiced items are always exported') ->addOption('template', null, InputOption::VALUE_OPTIONAL, 'Invoice template', null) ->addOption('search', null, InputOption::VALUE_OPTIONAL, 'Search term to filter invoice entries', null) ->addOption('exported', null, InputOption::VALUE_OPTIONAL, 'Exported filter for invoice entries (possible values: exported, all), by default only "not exported" items are fetched', null) @@ -156,10 +156,12 @@ final class InvoiceCreateCommand extends Command return Command::FAILURE; } } - if (!$start instanceof \DateTime) { + if (!$start instanceof \DateTimeInterface) { $start = $dateFactory->getStartOfMonth(); } - $start->setTime(0, 0, 0); + + $start = \DateTimeImmutable::createFromInterface($start); + $start = $start->setTime(0, 0, 0); $end = $input->getOption('end'); if (!empty($end)) { @@ -171,17 +173,18 @@ final class InvoiceCreateCommand extends Command return Command::FAILURE; } } - if (!$end instanceof \DateTime) { + if (!$end instanceof \DateTimeInterface) { $end = $dateFactory->getEndOfMonth(); } - $end->setTime(23, 59, 59); + + $end = \DateTimeImmutable::createFromInterface($end); + $end = $end->setTime(23, 59, 59); $searchTerm = null; if (null !== $input->getOption('search')) { $searchTerm = new SearchTerm($input->getOption('search')); } - $markAsExported = false; if ($input->getOption('preview') !== null) { $this->previewUniqueFile = (bool) $input->getOption('preview-unique'); $this->previewDirectory = rtrim($input->getOption('preview'), '/') . '/'; @@ -191,7 +194,7 @@ final class InvoiceCreateCommand extends Command return Command::FAILURE; } } elseif ($input->getOption('set-exported')) { - $markAsExported = true; + @trigger_error('The "set-exported" option of kimai:invoice:create command has no meaning anymore, it will be removed soon', E_USER_DEPRECATED); } // =============== VALIDATION END =============== diff --git a/src/Constants.php b/src/Constants.php index a1394d2d..9394220c 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ class Constants /** * The current release version */ - public const VERSION = '2.8.0'; + public const VERSION = '2.9.0'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 20800; + public const VERSION_ID = 20900; /** * The software name */ diff --git a/src/Controller/Reporting/ProjectDateRangeController.php b/src/Controller/Reporting/ProjectDateRangeController.php index d39d8d48..97f796db 100644 --- a/src/Controller/Reporting/ProjectDateRangeController.php +++ b/src/Controller/Reporting/ProjectDateRangeController.php @@ -10,6 +10,7 @@ namespace App\Controller\Reporting; use App\Controller\AbstractController; +use App\Entity\Customer; use App\Form\Model\DateRange; use App\Project\ProjectStatisticService; use App\Reporting\ProjectDateRange\ProjectDateRangeForm; @@ -37,15 +38,20 @@ final class ProjectDateRangeController extends AbstractController ]); $form->submit($request->query->all(), false); + $begin = $query->getMonth() ?? $defaultStart; + $dateRange = new DateRange(true); - $dateRange->setBegin($query->getMonth() ?? $defaultStart); - $dateRange->setEnd($dateFactory->getEndOfMonth($dateRange->getBegin())); + $dateRange->setBegin($begin); + $end = $dateFactory->getEndOfMonth($dateRange->getBegin()); // this resets the time + + $dateRange->setEnd($end); $projects = $service->findProjectsForDateRange($query, $dateRange); - $entries = $service->getBudgetStatisticModelForProjectsByDateRange($projects, $dateRange->getBegin(), $dateRange->getEnd(), $dateRange->getEnd()); + $entries = $service->getBudgetStatisticModelForProjectsByDateRange($projects, $begin, $end, $end); $byCustomer = []; foreach ($entries as $entry) { + /** @var Customer $customer */ $customer = $entry->getProject()->getCustomer(); if (!isset($byCustomer[$customer->getId()])) { $byCustomer[$customer->getId()] = ['customer' => $customer, 'projects' => []]; diff --git a/src/Customer/CustomerStatisticService.php b/src/Customer/CustomerStatisticService.php index 5274e6fb..073e18c0 100644 --- a/src/Customer/CustomerStatisticService.php +++ b/src/Customer/CustomerStatisticService.php @@ -17,6 +17,8 @@ use App\Model\CustomerStatistic; use App\Repository\TimesheetRepository; use App\Timesheet\DateTimeFactory; use DateTime; +use DateTimeImmutable; +use DateTimeInterface; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; @@ -27,17 +29,12 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; */ class CustomerStatisticService { - public function __construct(private TimesheetRepository $timesheetRepository, private EventDispatcherInterface $dispatcher) + public function __construct(private readonly TimesheetRepository $timesheetRepository, private readonly EventDispatcherInterface $dispatcher) { } /** - * WARNING: this method does not respect the budget type. Your results will always be wither the "full lifetime data" or the "selected date-range". - * - * @param Customer $customer - * @param DateTime|null $begin - * @param DateTime|null $end - * @return CustomerStatistic + * WARNING: this method does not respect the budget type. Your results will always be with the "full lifetime data" or the "selected date-range". */ public function getCustomerStatistics(Customer $customer, ?DateTime $begin = null, ?DateTime $end = null): CustomerStatistic { @@ -48,13 +45,13 @@ class CustomerStatisticService return $event->getStatistic(); } - public function getBudgetStatisticModel(Customer $customer, DateTime $today): CustomerBudgetStatisticModel + public function getBudgetStatisticModel(Customer $customer, DateTimeInterface $today): CustomerBudgetStatisticModel { $stats = new CustomerBudgetStatisticModel($customer); $stats->setStatisticTotal($this->getCustomerStatistics($customer)); $begin = null; - $end = $today; + $end = DateTime::createFromInterface($today); if ($customer->isMonthlyBudget()) { $dateFactory = new DateTimeFactory($today->getTimezone()); @@ -69,11 +66,9 @@ class CustomerStatisticService /** * @param Customer[] $customers - * @param DateTime|null $begin - * @param DateTime|null $end * @return array */ - private function getBudgetStatistic(array $customers, ?DateTime $begin = null, ?DateTime $end = null): array + private function getBudgetStatistic(array $customers, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): array { $statistics = []; foreach ($customers as $customer) { @@ -87,25 +82,25 @@ class CustomerStatisticService if (null !== $result) { foreach ($result as $resultRow) { $statistic = $statistics[$resultRow['id']]; - $statistic->setDuration($statistic->getDuration() + $resultRow['duration']); - $statistic->setRate($statistic->getRate() + $resultRow['rate']); - $statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']); - $statistic->setCounter($statistic->getCounter() + $resultRow['counter']); + $statistic->addDuration((int) $resultRow['duration']); + $statistic->addRate((float) $resultRow['rate']); + $statistic->addInternalRate((float) $resultRow['internalRate']); + $statistic->addCounter((int) $resultRow['counter']); if ($resultRow['billable']) { - $statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']); - $statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']); - $statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']); - $statistic->setCounterBillable($statistic->getCounterBillable() + $resultRow['counter']); + $statistic->addDurationBillable((int) $resultRow['duration']); + $statistic->addRateBillable((float) $resultRow['rate']); + $statistic->addInternalRateBillable((float) $resultRow['internalRate']); + $statistic->addCounterBillable((int) $resultRow['counter']); if ($resultRow['exported']) { - $statistic->setDurationBillableExported($statistic->getDurationBillableExported() + $resultRow['duration']); - $statistic->setRateBillableExported($statistic->getRateBillableExported() + $resultRow['rate']); + $statistic->addDurationBillableExported((int) $resultRow['duration']); + $statistic->addRateBillableExported((float) $resultRow['rate']); } } if ($resultRow['exported']) { - $statistic->setDurationExported($statistic->getDurationExported() + $resultRow['duration']); - $statistic->setRateExported($statistic->getRateExported() + $resultRow['rate']); - $statistic->setInternalRateExported($statistic->getInternalRateExported() + $resultRow['internalRate']); - $statistic->setCounterExported($statistic->getCounterExported() + $resultRow['counter']); + $statistic->addDurationExported((int) $resultRow['duration']); + $statistic->addRateExported((float) $resultRow['rate']); + $statistic->addInternalRateExported((float) $resultRow['internalRate']); + $statistic->addCounterExported((int) $resultRow['counter']); } } } @@ -113,7 +108,7 @@ class CustomerStatisticService return $statistics; } - private function createStatisticQueryBuilder(array $customers, DateTime $begin = null, ?DateTime $end = null): QueryBuilder + private function createStatisticQueryBuilder(array $customers, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): QueryBuilder { $qb = $this->timesheetRepository->createQueryBuilder('t'); $qb @@ -136,14 +131,14 @@ class CustomerStatisticService if ($begin !== null) { $qb ->andWhere($qb->expr()->gte('t.begin', ':begin')) - ->setParameter('begin', $begin, Types::DATETIME_MUTABLE) + ->setParameter('begin', DateTimeImmutable::createFromInterface($begin), Types::DATETIME_IMMUTABLE) ; } if ($end !== null) { $qb ->andWhere($qb->expr()->lte('t.begin', ':end')) - ->setParameter('end', $end, Types::DATETIME_MUTABLE) + ->setParameter('end', DateTimeImmutable::createFromInterface($end), Types::DATETIME_IMMUTABLE) ; } diff --git a/src/DependencyInjection/AppExtension.php b/src/DependencyInjection/AppExtension.php index eadcb8c4..53780030 100644 --- a/src/DependencyInjection/AppExtension.php +++ b/src/DependencyInjection/AppExtension.php @@ -109,8 +109,7 @@ final class AppExtension extends Extension $locales = explode('|', $container->getParameter('app_locales')); $directory = $container->getParameter('kernel.project_dir'); - $config = $directory . DIRECTORY_SEPARATOR . 'config/locales.php'; - $settings = include $config; + $settings = include $directory . DIRECTORY_SEPARATOR . 'config/locales.php'; $appLocales = []; $defaults = [ diff --git a/src/Event/ActivityBudgetStatisticEvent.php b/src/Event/ActivityBudgetStatisticEvent.php index 445faeee..e99e33a7 100644 --- a/src/Event/ActivityBudgetStatisticEvent.php +++ b/src/Event/ActivityBudgetStatisticEvent.php @@ -13,13 +13,27 @@ use App\Model\ActivityBudgetStatisticModel; final class ActivityBudgetStatisticEvent { + private readonly ?\DateTime $begin; + private readonly ?\DateTime $end; + /** * @param ActivityBudgetStatisticModel[] $models - * @param \DateTime|null $begin - * @param \DateTime|null $end */ - public function __construct(private array $models, private ?\DateTime $begin = null, private ?\DateTime $end = null) + public function __construct( + private readonly array $models, + ?\DateTimeInterface $begin = null, + ?\DateTimeInterface $end = null + ) { + if ($begin !== null) { + $begin = \DateTime::createFromInterface($begin); + } + $this->begin = $begin; + + if ($end !== null) { + $end = \DateTime::createFromInterface($end); + } + $this->end = $end; } public function getModel(int $activityId): ?ActivityBudgetStatisticModel diff --git a/src/Event/ActivityStatisticEvent.php b/src/Event/ActivityStatisticEvent.php index 8fe929df..9dad0a35 100644 --- a/src/Event/ActivityStatisticEvent.php +++ b/src/Event/ActivityStatisticEvent.php @@ -11,12 +11,32 @@ namespace App\Event; use App\Entity\Activity; use App\Model\ActivityStatistic; +use DateTime; +use DateTimeInterface; final class ActivityStatisticEvent extends AbstractActivityEvent { - public function __construct(Activity $activity, private ActivityStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null) + private readonly ?DateTime $begin; + private readonly ?DateTime $end; + + public function __construct( + Activity $activity, + private readonly ActivityStatistic $statistic, + ?DateTimeInterface $begin = null, + ?DateTimeInterface $end = null + ) { parent::__construct($activity); + + if ($begin !== null) { + $begin = DateTime::createFromInterface($begin); + } + $this->begin = $begin; + + if ($end !== null) { + $end = DateTime::createFromInterface($end); + } + $this->end = $end; } public function getStatistic(): ActivityStatistic @@ -24,12 +44,12 @@ final class ActivityStatisticEvent extends AbstractActivityEvent return $this->statistic; } - public function getBegin(): ?\DateTime + public function getBegin(): ?DateTime { return $this->begin; } - public function getEnd(): ?\DateTime + public function getEnd(): ?DateTime { return $this->end; } diff --git a/src/Event/ProjectBudgetStatisticEvent.php b/src/Event/ProjectBudgetStatisticEvent.php index c6dd57ee..f945b620 100644 --- a/src/Event/ProjectBudgetStatisticEvent.php +++ b/src/Event/ProjectBudgetStatisticEvent.php @@ -10,16 +10,32 @@ namespace App\Event; use App\Model\ProjectBudgetStatisticModel; +use DateTime; +use DateTimeInterface; final class ProjectBudgetStatisticEvent { + private readonly ?DateTime $begin; + private readonly ?DateTime $end; + /** * @param ProjectBudgetStatisticModel[] $models - * @param \DateTime|null $begin - * @param \DateTime|null $end */ - public function __construct(private array $models, private ?\DateTime $begin = null, private ?\DateTime $end = null) + public function __construct( + private readonly array $models, + ?DateTimeInterface $begin = null, + ?DateTimeInterface $end = null + ) { + if ($begin !== null) { + $begin = \DateTime::createFromInterface($begin); + } + $this->begin = $begin; + + if ($end !== null) { + $end = \DateTime::createFromInterface($end); + } + $this->end = $end; } public function getModel(int $projectId): ?ProjectBudgetStatisticModel @@ -45,12 +61,12 @@ final class ProjectBudgetStatisticEvent return $this->models; } - public function getBegin(): ?\DateTime + public function getBegin(): ?DateTime { return $this->begin; } - public function getEnd(): ?\DateTime + public function getEnd(): ?DateTime { return $this->end; } diff --git a/src/Event/ProjectStatisticEvent.php b/src/Event/ProjectStatisticEvent.php index 25539e1e..e407541a 100644 --- a/src/Event/ProjectStatisticEvent.php +++ b/src/Event/ProjectStatisticEvent.php @@ -14,7 +14,12 @@ use App\Model\ProjectStatistic; final class ProjectStatisticEvent extends AbstractProjectEvent { - public function __construct(Project $project, private ProjectStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null) + public function __construct( + Project $project, + private readonly ProjectStatistic $statistic, + private readonly ?\DateTimeInterface $begin = null, + private readonly ?\DateTimeInterface $end = null + ) { parent::__construct($project); } @@ -24,12 +29,12 @@ final class ProjectStatisticEvent extends AbstractProjectEvent return $this->statistic; } - public function getBegin(): ?\DateTime + public function getBegin(): ?\DateTimeInterface { return $this->begin; } - public function getEnd(): ?\DateTime + public function getEnd(): ?\DateTimeInterface { return $this->end; } diff --git a/src/Export/Base/AbstractSpreadsheetRenderer.php b/src/Export/Base/AbstractSpreadsheetRenderer.php index a61a35b0..449fc646 100644 --- a/src/Export/Base/AbstractSpreadsheetRenderer.php +++ b/src/Export/Base/AbstractSpreadsheetRenderer.php @@ -186,7 +186,7 @@ abstract class AbstractSpreadsheetRenderer $sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate); // TODO why is that format hardcoded and does not depend on the users locale? - $sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2); + $sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD); } protected function setDurationTotal(Worksheet $sheet, int $column, int $row, string $startCoordinate, string $endCoordinate): void diff --git a/src/Export/Spreadsheet/CellFormatter/DateFormatter.php b/src/Export/Spreadsheet/CellFormatter/DateFormatter.php index 65c9886b..ee0c4c81 100644 --- a/src/Export/Spreadsheet/CellFormatter/DateFormatter.php +++ b/src/Export/Spreadsheet/CellFormatter/DateFormatter.php @@ -24,11 +24,11 @@ final class DateFormatter implements CellFormatterInterface return; } - if (!$value instanceof \DateTime) { - throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported'); + if (!$value instanceof \DateTimeInterface) { + throw new \InvalidArgumentException('Unsupported value given, only DateTimeInterface is supported'); } $sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value)); - $sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2); + $sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD); } } diff --git a/src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php b/src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php index 75a0a401..f8a9e51d 100644 --- a/src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php +++ b/src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php @@ -25,11 +25,11 @@ final class DateTimeFormatter implements CellFormatterInterface return; } - if (!$value instanceof \DateTime) { - throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported'); + if (!$value instanceof \DateTimeInterface) { + throw new \InvalidArgumentException('Unsupported value given, only DateTimeInterface is supported'); } $sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value)); - $sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT); + $sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT); } } diff --git a/src/Export/Spreadsheet/CellFormatter/TimeFormatter.php b/src/Export/Spreadsheet/CellFormatter/TimeFormatter.php index cdbc93ad..d9cb0f19 100644 --- a/src/Export/Spreadsheet/CellFormatter/TimeFormatter.php +++ b/src/Export/Spreadsheet/CellFormatter/TimeFormatter.php @@ -25,8 +25,8 @@ final class TimeFormatter implements CellFormatterInterface return; } - if (!$value instanceof \DateTime) { - throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported'); + if (!$value instanceof \DateTimeInterface) { + throw new \InvalidArgumentException('Unsupported value given, only DateTimeInterface is supported'); } $sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value)); diff --git a/src/Form/Model/DateRange.php b/src/Form/Model/DateRange.php index 41e7cfce..6478852d 100644 --- a/src/Form/Model/DateRange.php +++ b/src/Form/Model/DateRange.php @@ -26,9 +26,9 @@ final class DateRange implements EquatableInterface return $this->begin; } - public function setBegin(DateTime $begin): DateRange + public function setBegin(\DateTimeInterface $begin): DateRange { - $this->begin = $begin; + $this->begin = DateTime::createFromInterface($begin); if ($this->resetTimes) { $this->begin->setTime(0, 0, 0); } @@ -41,9 +41,9 @@ final class DateRange implements EquatableInterface return $this->end; } - public function setEnd(DateTime $end): DateRange + public function setEnd(\DateTimeInterface $end): DateRange { - $this->end = $end; + $this->end = DateTime::createFromInterface($end); if ($this->resetTimes) { $this->end->setTime(23, 59, 59); } diff --git a/src/Form/Type/DatePickerType.php b/src/Form/Type/DatePickerType.php index 4c285bd8..5bbcb40b 100644 --- a/src/Form/Type/DatePickerType.php +++ b/src/Form/Type/DatePickerType.php @@ -39,10 +39,12 @@ class DatePickerType extends AbstractType return null; } - if ($reverseTransform instanceof \DateTime && $options['force_time']) { + if ($reverseTransform instanceof \DateTimeInterface && $options['force_time']) { if ($options['force_time'] === 'start') { + $reverseTransform = \DateTime::createFromInterface($reverseTransform); $reverseTransform->setTime(0, 0, 0); } elseif ($options['force_time'] === 'end') { + $reverseTransform = \DateTime::createFromInterface($reverseTransform); $reverseTransform->setTime(23, 59, 59); } } diff --git a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php index d920638b..75349e8f 100644 --- a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php @@ -48,7 +48,9 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator 'customer.invoice_text' => $customer->getInvoiceText() ?? '', ]; - $statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $model->getQuery()->getEnd()); + /** @var \DateTime $end */ + $end = $model->getQuery()->getEnd(); + $statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $end); $values = array_merge($values, $this->getBudgetValues('customer.', $statistic, $model)); diff --git a/src/Invoice/Renderer/DocxRenderer.php b/src/Invoice/Renderer/DocxRenderer.php index 680add24..0cb72eea 100644 --- a/src/Invoice/Renderer/DocxRenderer.php +++ b/src/Invoice/Renderer/DocxRenderer.php @@ -57,7 +57,12 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface $i++; } - $cacheFile = $template->save(); + $cacheFile = @tempnam(sys_get_temp_dir(), 'kimai-invoice-docx'); + if (false === $cacheFile) { + throw new \Exception('Could not open temporary file'); + } + + $template->saveAs($cacheFile); clearstatcache(true, $cacheFile); diff --git a/src/Model/TimesheetCountedStatistic.php b/src/Model/TimesheetCountedStatistic.php index 8746bca9..920bf6d4 100644 --- a/src/Model/TimesheetCountedStatistic.php +++ b/src/Model/TimesheetCountedStatistic.php @@ -47,6 +47,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->counter = $counter; } + public function addCounter(int $counter): void + { + $this->counter += $counter; + } + public function getCounterBillable(): int { return $this->counterBillable; @@ -57,6 +62,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->counterBillable = $counter; } + public function addCounterBillable(int $counter): void + { + $this->counterBillable += $counter; + } + public function getCounterExported(): int { return $this->counterExported; @@ -67,6 +77,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->counterExported = $counter; } + public function addCounterExported(int $counter): void + { + $this->counterExported += $counter; + } + /** * For unified access, used in frontend. * @@ -82,6 +97,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->recordDuration = $duration; } + public function addDuration(int $duration): void + { + $this->recordDuration += $duration; + } + /** * For unified access, used in frontend. * @@ -107,6 +127,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->recordRate = $rate; } + public function addRate(float $rate): void + { + $this->recordRate += $rate; + } + /** * Returns the total internal rate of all included timesheet records. * @@ -127,6 +152,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->internalRateBillable = $internalRateBillable; } + public function addInternalRateBillable(float $internalRateBillable): void + { + $this->internalRateBillable += $internalRateBillable; + } + public function getInternalRateExported(): float { return $this->internalRateExported; @@ -137,11 +167,21 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->internalRateExported = $internalRateExported; } + public function addInternalRateExported(float $internalRateExported): void + { + $this->internalRateExported += $internalRateExported; + } + public function setInternalRate(float $internalRate): void { $this->internalRate = $internalRate; } + public function addInternalRate(float $internalRate): void + { + $this->internalRate += $internalRate; + } + public function getDurationBillable(): int { return $this->recordDurationBillable; @@ -152,6 +192,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->recordDurationBillable = $recordDuration; } + public function addDurationBillable(int $recordDuration): void + { + $this->recordDurationBillable += $recordDuration; + } + public function getDurationBillableExported(): int { return $this->recordDurationBillableExported; @@ -162,6 +207,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->recordDurationBillableExported = $recordDuration; } + public function addDurationBillableExported(int $recordDuration): void + { + $this->recordDurationBillableExported += $recordDuration; + } + public function getRateBillable(): float { return $this->recordRateBillable; @@ -172,6 +222,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->recordRateBillable = $recordRate; } + public function addRateBillable(float $recordRate): void + { + $this->recordRateBillable += $recordRate; + } + public function getRateBillableExported(): float { return $this->recordRateBillableExported; @@ -182,6 +237,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->recordRateBillableExported = $recordRate; } + public function addRateBillableExported(float $recordRate): void + { + $this->recordRateBillableExported += $recordRate; + } + public function getDurationExported(): int { return $this->recordDurationExported; @@ -192,6 +252,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->recordDurationExported = $recordDuration; } + public function addDurationExported(int $recordDuration): void + { + $this->recordDurationExported += $recordDuration; + } + public function getRateExported(): float { return $this->recordRateExported; @@ -202,6 +267,11 @@ class TimesheetCountedStatistic implements \JsonSerializable $this->recordRateExported = $recordRate; } + public function addRateExported(float $recordRate): void + { + $this->recordRateExported += $recordRate; + } + public function jsonSerialize(): mixed { return [ diff --git a/src/Project/ProjectStatisticService.php b/src/Project/ProjectStatisticService.php index e2f7631e..6aa411e9 100644 --- a/src/Project/ProjectStatisticService.php +++ b/src/Project/ProjectStatisticService.php @@ -34,7 +34,8 @@ use App\Repository\ProjectRepository; use App\Repository\TimesheetRepository; use App\Repository\UserRepository; use App\Timesheet\DateTimeFactory; -use DateTime; +use DateTimeImmutable; +use DateTimeInterface; use Doctrine\DBAL\Types\Types; use Psr\EventDispatcher\EventDispatcherInterface; @@ -54,14 +55,9 @@ class ProjectStatisticService } /** - * WARNING: this method does not respect the budget type. Your results will always be wither the "full lifetime data" or the "selected date-range". - * - * @param Project $project - * @param DateTime|null $begin - * @param DateTime|null $end - * @return ProjectStatistic + * WARNING: this method does not respect the budget type. Your results will always be with the "full lifetime data" or the "selected date-range". */ - public function getProjectStatistics(Project $project, ?DateTime $begin = null, ?DateTime $end = null): ProjectStatistic + public function getProjectStatistics(Project $project, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): ProjectStatistic { $statistics = $this->getBudgetStatistic([$project], $begin, $end); $event = new ProjectStatisticEvent($project, array_pop($statistics), $begin, $end); @@ -71,14 +67,13 @@ class ProjectStatisticService } /** - * @param ProjectInactiveQuery $query * @return Project[] */ public function findInactiveProjects(ProjectInactiveQuery $query): array { $user = $query->getUser(); - $lastChange = clone $query->getLastChange(); - $now = new DateTime('now', $lastChange->getTimezone()); + $lastChange = DateTimeImmutable::createFromInterface($query->getLastChange()); + $now = new DateTimeImmutable('now', $lastChange->getTimezone()); $qb2 = $this->projectRepository->createQueryBuilder('t1'); $qb2 @@ -101,15 +96,15 @@ class ProjectStatisticService $qb->expr()->lte('p.start', ':project_start') ) ) - ->setParameter('project_start', $now, Types::DATETIME_MUTABLE) + ->setParameter('project_start', $now, Types::DATETIME_IMMUTABLE) ->andWhere( $qb->expr()->orX( $qb->expr()->isNull('p.end'), $qb->expr()->gte('p.end', ':project_end') ) ) - ->setParameter('project_end', $now, Types::DATETIME_MUTABLE) - ->setParameter('begin', $lastChange, Types::DATETIME_MUTABLE) + ->setParameter('project_end', $now, Types::DATETIME_IMMUTABLE) + ->setParameter('begin', $lastChange, Types::DATETIME_IMMUTABLE) ; $this->projectRepository->addPermissionCriteria($qb, $user); @@ -125,7 +120,6 @@ class ProjectStatisticService } /** - * @param ProjectDateRangeQuery $query * @return Project[] */ public function findProjectsForDateRange(ProjectDateRangeQuery $query, DateRange $dateRange): array @@ -208,7 +202,7 @@ class ProjectStatisticService return $projects; } - public function getBudgetStatisticModel(Project $project, DateTime $today): ProjectBudgetStatisticModel + public function getBudgetStatisticModel(Project $project, DateTimeInterface $today): ProjectBudgetStatisticModel { $stats = new ProjectBudgetStatisticModel($project); $stats->setStatisticTotal($this->getProjectStatistics($project)); @@ -229,10 +223,9 @@ class ProjectStatisticService /** * @param Project[] $projects - * @param DateTime $today * @return ProjectBudgetStatisticModel[] */ - public function getBudgetStatisticModelForProjects(array $projects, DateTime $today): array + public function getBudgetStatisticModelForProjects(array $projects, DateTimeInterface $today): array { $models = []; $monthly = []; @@ -282,12 +275,9 @@ class ProjectStatisticService /** * @param Project[] $projects - * @param DateTime $begin - * @param DateTime $end - * @param DateTime|null $totalsEnd * @return ProjectBudgetStatisticModel[] */ - public function getBudgetStatisticModelForProjectsByDateRange(array $projects, DateTime $begin, DateTime $end, ?DateTime $totalsEnd = null): array + public function getBudgetStatisticModelForProjectsByDateRange(array $projects, DateTimeInterface $begin, DateTimeInterface $end, ?DateTimeInterface $totalsEnd = null): array { $models = []; @@ -313,11 +303,9 @@ class ProjectStatisticService /** * @param Project[] $projects - * @param DateTime|null $begin - * @param DateTime|null $end * @return array */ - public function getBudgetStatistic(array $projects, ?DateTime $begin = null, ?DateTime $end = null): array + public function getBudgetStatistic(array $projects, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): array { $statistics = []; foreach ($projects as $project) { @@ -711,15 +699,13 @@ class ProjectStatisticService } /** - * @param User $user * @param Project[] $projects - * @param DateTime $today * @return ProjectViewModel[] */ - public function getProjectView(User $user, array $projects, DateTime $today): array + public function getProjectView(User $user, array $projects, DateTimeInterface $today): array { $factory = DateTimeFactory::createByUser($user); - $today = clone $today; + $today = DateTimeImmutable::createFromInterface($today); $startOfWeek = $factory->getStartOfWeek($today); $endOfWeek = $factory->getEndOfWeek($today); diff --git a/src/Repository/Query/DateRangeInterface.php b/src/Repository/Query/DateRangeInterface.php new file mode 100644 index 00000000..3bfa9c51 --- /dev/null +++ b/src/Repository/Query/DateRangeInterface.php @@ -0,0 +1,30 @@ +dateRange?->getBegin(); } - public function setBegin(\DateTime $begin): void + public function setBegin(\DateTimeInterface $begin): void { - $this->dateRange->setBegin($begin); + $this->dateRange->setBegin(\DateTime::createFromInterface($begin)); } public function getEnd(): ?\DateTime @@ -30,9 +30,9 @@ trait DateRangeTrait return $this->dateRange?->getEnd(); } - public function setEnd(\DateTime $end): void + public function setEnd(\DateTimeInterface $end): void { - $this->dateRange->setEnd($end); + $this->dateRange->setEnd(\DateTime::createFromInterface($end)); } public function getDateRange(): ?DateRange diff --git a/src/Repository/Query/InvoiceArchiveQuery.php b/src/Repository/Query/InvoiceArchiveQuery.php index 79caf0be..bb49eacb 100644 --- a/src/Repository/Query/InvoiceArchiveQuery.php +++ b/src/Repository/Query/InvoiceArchiveQuery.php @@ -16,7 +16,7 @@ use App\Form\Model\DateRange; /** * Query for created invoices. */ -class InvoiceArchiveQuery extends BaseQuery +class InvoiceArchiveQuery extends BaseQuery implements DateRangeInterface { use DateRangeTrait; diff --git a/src/Repository/Query/TimesheetQuery.php b/src/Repository/Query/TimesheetQuery.php index ca9cae20..fceffd31 100644 --- a/src/Repository/Query/TimesheetQuery.php +++ b/src/Repository/Query/TimesheetQuery.php @@ -14,7 +14,7 @@ use App\Entity\Tag; use App\Entity\User; use App\Form\Model\DateRange; -class TimesheetQuery extends ActivityQuery implements BillableInterface +class TimesheetQuery extends ActivityQuery implements BillableInterface, DateRangeInterface { use BillableTrait; use DateRangeTrait; diff --git a/src/Twig/LocaleFormatExtensions.php b/src/Twig/LocaleFormatExtensions.php index c973c2f7..9ffa9907 100644 --- a/src/Twig/LocaleFormatExtensions.php +++ b/src/Twig/LocaleFormatExtensions.php @@ -62,10 +62,10 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw return [ new TwigTest('weekend', [$this, 'isWeekend']), new TwigTest('today', function ($dateTime): bool { - if (!$dateTime instanceof \DateTime) { + if (!$dateTime instanceof \DateTimeInterface) { return false; } - $compare = new \DateTime('now', $dateTime->getTimezone()); + $compare = new \DateTimeImmutable('now', $dateTime->getTimezone()); return $compare->format('Y-m-d') === $dateTime->format('Y-m-d'); }), diff --git a/src/Validator/Constraints/TimesheetBudgetUsedValidator.php b/src/Validator/Constraints/TimesheetBudgetUsedValidator.php index 72dfdb7e..e17d93b2 100644 --- a/src/Validator/Constraints/TimesheetBudgetUsedValidator.php +++ b/src/Validator/Constraints/TimesheetBudgetUsedValidator.php @@ -58,8 +58,10 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator return; } + $begin = $timesheet->getBegin(); + // we can only work with stopped entries - if (null === $timesheet->getEnd() || null === $timesheet->getUser()) { + if ($begin === null || $timesheet->getEnd() === null || $timesheet->getUser() === null) { return; } @@ -115,7 +117,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator if ($duration === $rawData['duration'] && $rate === $rawData['rate'] && $timesheet->isBillable() === $rawData['billable'] && - $timesheet->getBegin()->format('Y.m.d') === $rawData['begin']->format('Y.m.d') && + $begin->format('Y.m.d') === $rawData['begin']->format('Y.m.d') && $timesheet->getProject()->getId() === $projectId && ($timesheet->getActivity() === null || $timesheet->getActivity()->getId() === $activityId) ) { @@ -145,11 +147,11 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator } } - $monthWasChanged = $timesheet->getBegin()->format('Y.m') !== $rawData['begin']->format('Y.m'); + $monthWasChanged = $begin->format('Y.m') !== $rawData['begin']->format('Y.m'); } - $now = new DateTime('now', $timesheet->getBegin()->getTimezone()); - $recordDate = $timesheet->getBegin(); + $now = new DateTime('now', $begin->getTimezone()); + $recordDate = $begin; if (null !== ($activity = $timesheet->getActivity()) && $activity->hasBudgets()) { $dateTime = $activity->isMonthlyBudget() ? $recordDate : $now; diff --git a/src/Validator/Constraints/TimesheetLockdownValidator.php b/src/Validator/Constraints/TimesheetLockdownValidator.php index 4a01508a..e40a6523 100644 --- a/src/Validator/Constraints/TimesheetLockdownValidator.php +++ b/src/Validator/Constraints/TimesheetLockdownValidator.php @@ -52,7 +52,7 @@ final class TimesheetLockdownValidator extends ConstraintValidator $now = new \DateTime('now', $timesheetStart->getTimezone()); if (!empty($constraint->now)) { - if ($constraint->now instanceof \DateTime) { + if ($constraint->now instanceof \DateTimeInterface) { $now = $constraint->now; } elseif (\is_string($constraint->now)) { try { diff --git a/symfony.lock b/symfony.lock index 4cc98547..5201f681 100644 --- a/symfony.lock +++ b/symfony.lock @@ -443,13 +443,23 @@ "version": "v4.0.3" }, "symfony/framework-bundle": { - "version": "3.3", + "version": "6.4", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "3.3", - "ref": "137a14eeb6b3f5370e7147af8aff6518504f50c7" - } + "branch": "main", + "version": "6.4", + "ref": "a91c965766ad3ff2ae15981801643330eb42b6a5" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] }, "symfony/http-client": { "version": "v4.4.5" diff --git a/tests/Event/ActivityBudgetStatisticEventTest.php b/tests/Event/ActivityBudgetStatisticEventTest.php index 9b8a9aff..1b1c1f3c 100644 --- a/tests/Event/ActivityBudgetStatisticEventTest.php +++ b/tests/Event/ActivityBudgetStatisticEventTest.php @@ -19,7 +19,7 @@ use PHPUnit\Framework\TestCase; */ class ActivityBudgetStatisticEventTest extends TestCase { - public function testStatistic() + public function testStatistic(): void { $activity = $this->createMock(Activity::class); $activity->expects($this->exactly(2))->method('getId')->willReturn(12); @@ -39,7 +39,7 @@ class ActivityBudgetStatisticEventTest extends TestCase self::assertNull($sut->getModel(1)); self::assertSame($model1, $sut->getModel(12)); self::assertSame($model2, $sut->getModel(4)); - self::assertSame($begin, $sut->getBegin()); - self::assertSame($end, $sut->getEnd()); + self::assertEquals($begin, $sut->getBegin()); + self::assertEquals($end, $sut->getEnd()); } } diff --git a/tests/Event/ActivityStatisticEventTest.php b/tests/Event/ActivityStatisticEventTest.php index b95740c3..2222b1ea 100644 --- a/tests/Event/ActivityStatisticEventTest.php +++ b/tests/Event/ActivityStatisticEventTest.php @@ -25,7 +25,7 @@ class ActivityStatisticEventTest extends AbstractActivityEventTest return new ActivityStatisticEvent($activity, new ActivityStatistic()); } - public function testStatistic() + public function testStatistic(): void { $activity = new Activity(); $statistic = new ActivityStatistic(); @@ -39,7 +39,7 @@ class ActivityStatisticEventTest extends AbstractActivityEventTest $begin = new \DateTime('2020-08-08 12:34:56'); $end = new \DateTime('2020-09-08 12:34:56'); $sut = new ActivityStatisticEvent($activity, $statistic, $begin, $end); - self::assertSame($begin, $sut->getBegin()); - self::assertSame($end, $sut->getEnd()); + self::assertEquals($begin, $sut->getBegin()); + self::assertEquals($end, $sut->getEnd()); } } diff --git a/tests/Event/ProjectBudgetStatisticEventTest.php b/tests/Event/ProjectBudgetStatisticEventTest.php index 47efb846..110e1dd1 100644 --- a/tests/Event/ProjectBudgetStatisticEventTest.php +++ b/tests/Event/ProjectBudgetStatisticEventTest.php @@ -19,7 +19,7 @@ use PHPUnit\Framework\TestCase; */ class ProjectBudgetStatisticEventTest extends TestCase { - public function testStatistic() + public function testStatistic(): void { $project = $this->createMock(Project::class); $project->expects($this->exactly(2))->method('getId')->willReturn(12); @@ -39,7 +39,7 @@ class ProjectBudgetStatisticEventTest extends TestCase self::assertNull($sut->getModel(1)); self::assertSame($model1, $sut->getModel(12)); self::assertSame($model2, $sut->getModel(4)); - self::assertSame($begin, $sut->getBegin()); - self::assertSame($end, $sut->getEnd()); + self::assertEquals($begin, $sut->getBegin()); + self::assertEquals($end, $sut->getEnd()); } } diff --git a/tests/Event/ProjectStatisticEventTest.php b/tests/Event/ProjectStatisticEventTest.php index d79f6911..b22d4e66 100644 --- a/tests/Event/ProjectStatisticEventTest.php +++ b/tests/Event/ProjectStatisticEventTest.php @@ -25,7 +25,7 @@ class ProjectStatisticEventTest extends AbstractProjectEventTest return new ProjectStatisticEvent($project, new ProjectStatistic()); } - public function testStatistic() + public function testStatistic(): void { $project = new Project(); $statistic = new ProjectStatistic(); @@ -36,8 +36,8 @@ class ProjectStatisticEventTest extends AbstractProjectEventTest self::assertNull($sut->getBegin()); self::assertNull($sut->getEnd()); - $begin = new \DateTime('2020-08-08 12:34:56'); - $end = new \DateTime('2020-09-08 12:34:56'); + $begin = new \DateTimeImmutable('2020-08-08 12:34:56'); + $end = new \DateTimeImmutable('2020-09-08 12:34:56'); $sut = new ProjectStatisticEvent($project, $statistic, $begin, $end); self::assertSame($begin, $sut->getBegin()); self::assertSame($end, $sut->getEnd()); diff --git a/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php index a87d6b2d..63fdcc5d 100644 --- a/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php +++ b/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php @@ -41,7 +41,7 @@ class DateFormatterTest extends AbstractFormatterTest public function testFormattedValueWithInvalidValue() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unsupported value given, only DateTime is supported'); + $this->expectExceptionMessage('Unsupported value given, only DateTimeInterface is supported'); $spreadsheet = new Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); diff --git a/tests/Export/Spreadsheet/CellFormatter/DateTimeFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/DateTimeFormatterTest.php index 079fa629..573b5bc4 100644 --- a/tests/Export/Spreadsheet/CellFormatter/DateTimeFormatterTest.php +++ b/tests/Export/Spreadsheet/CellFormatter/DateTimeFormatterTest.php @@ -40,7 +40,7 @@ class DateTimeFormatterTest extends AbstractFormatterTest public function testFormattedValueWithInvalidValue() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unsupported value given, only DateTime is supported'); + $this->expectExceptionMessage('Unsupported value given, only DateTimeInterface is supported'); $spreadsheet = new Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); diff --git a/tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php index d4582d2e..2cbc7979 100644 --- a/tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php +++ b/tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php @@ -40,7 +40,7 @@ class TimeFormatterTest extends AbstractFormatterTest public function testFormattedValueWithInvalidValue() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unsupported value given, only DateTime is supported'); + $this->expectExceptionMessage('Unsupported value given, only DateTimeInterface is supported'); $spreadsheet = new Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); diff --git a/tests/Invoice/Renderer/DocxRendererTest.php b/tests/Invoice/Renderer/DocxRendererTest.php index 4a87e2ce..ec5eb257 100644 --- a/tests/Invoice/Renderer/DocxRendererTest.php +++ b/tests/Invoice/Renderer/DocxRendererTest.php @@ -22,7 +22,7 @@ class DocxRendererTest extends TestCase { use RendererTestTrait; - public function testSupports() + public function testSupports(): void { $sut = $this->getAbstractRenderer(DocxRenderer::class); @@ -34,7 +34,7 @@ class DocxRendererTest extends TestCase $this->assertFalse($sut->supports($this->getInvoiceDocument('open-spreadsheet.ods', true))); } - public function testRender() + public function testRender(): void { /** @var DocxRenderer $sut */ $sut = $this->getAbstractRenderer(DocxRenderer::class); @@ -48,7 +48,7 @@ class DocxRendererTest extends TestCase $this->assertEquals('application/vnd.openxmlformats-officedocument.wordprocessingml.document', $response->headers->get('Content-Type')); $this->assertEquals('attachment; filename=' . $filename, $response->headers->get('Content-Disposition')); - $this->assertTrue(file_exists($file->getRealPath())); + $this->assertTrue(file_exists($file->getPathname())); ob_start(); $response->sendContent(); diff --git a/tests/Repository/Query/BaseQueryTest.php b/tests/Repository/Query/BaseQueryTest.php index 14752cf8..060f3ec4 100644 --- a/tests/Repository/Query/BaseQueryTest.php +++ b/tests/Repository/Query/BaseQueryTest.php @@ -17,6 +17,7 @@ use App\Entity\Team; use App\Form\Model\DateRange; use App\Repository\Query\ActivityQuery; use App\Repository\Query\BaseQuery; +use App\Repository\Query\DateRangeInterface; use App\Repository\Query\ProjectQuery; use App\Repository\Query\TimesheetQuery; use App\Utils\SearchTerm; @@ -334,7 +335,7 @@ class BaseQueryTest extends TestCase $this->assertEquals([13, 27], $sut->getProjectIds()); } - protected function assertDateRangeTrait($sut): void + protected function assertDateRangeTrait(DateRangeInterface $sut): void { self::assertNull($sut->getBegin()); self::assertNull($sut->getEnd()); @@ -346,14 +347,15 @@ class BaseQueryTest extends TestCase self::assertNull($sut->getBegin()); self::assertNull($sut->getEnd()); - $begin = new \DateTime('2013-11-23 13:45:07'); - $end = new \DateTime('2014-01-01 23:45:11'); - $dateRange->setBegin($begin); - $dateRange->setEnd($end); + $dateRange->setBegin(new \DateTimeImmutable('2013-11-23 13:45:07')); + $dateRange->setEnd(new \DateTimeImmutable('2014-01-01 23:45:11')); - self::assertSame($begin, $sut->getDateRange()->getBegin()); - self::assertSame($begin, $sut->getBegin()); - self::assertSame($end, $sut->getDateRange()->getEnd()); - self::assertSame($end, $sut->getEnd()); + $begin1 = new \DateTimeImmutable('2013-11-23 00:00:00'); + $end1 = new \DateTimeImmutable('2014-01-01 23:59:59'); + + self::assertEquals($begin1, $sut->getDateRange()->getBegin()); + self::assertEquals($begin1, $sut->getBegin()); + self::assertEquals($end1, $sut->getDateRange()->getEnd()); + self::assertEquals($end1, $sut->getEnd()); } } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 7df19db8..df1a4737 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -4072,11 +4072,6 @@ parameters: count: 1 path: Event/AbstractTimesheetMultipleEventTest.php - - - message: "#^Method App\\\\Tests\\\\Event\\\\ActivityBudgetStatisticEventTest\\:\\:testStatistic\\(\\) has no return type specified\\.$#" - count: 1 - path: Event/ActivityBudgetStatisticEventTest.php - - message: "#^Method App\\\\Tests\\\\Event\\\\ActivityMetaDefinitionEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#" count: 1 @@ -4087,11 +4082,6 @@ parameters: count: 1 path: Event/ActivityMetaDisplayEventTest.php - - - message: "#^Method App\\\\Tests\\\\Event\\\\ActivityStatisticEventTest\\:\\:testStatistic\\(\\) has no return type specified\\.$#" - count: 1 - path: Event/ActivityStatisticEventTest.php - - message: "#^Method App\\\\Tests\\\\Event\\\\CalendarConfigurationEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#" count: 1 @@ -4202,11 +4192,6 @@ parameters: count: 5 path: Event/PermissionsEventTest.php - - - message: "#^Method App\\\\Tests\\\\Event\\\\ProjectBudgetStatisticEventTest\\:\\:testStatistic\\(\\) has no return type specified\\.$#" - count: 1 - path: Event/ProjectBudgetStatisticEventTest.php - - message: "#^Method App\\\\Tests\\\\Event\\\\ProjectMetaDefinitionEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#" count: 1 @@ -4222,11 +4207,6 @@ parameters: count: 1 path: Event/ProjectMetaQueryDisplayTest.php - - - message: "#^Method App\\\\Tests\\\\Event\\\\ProjectStatisticEventTest\\:\\:testStatistic\\(\\) has no return type specified\\.$#" - count: 1 - path: Event/ProjectStatisticEventTest.php - - message: "#^Method App\\\\Tests\\\\Event\\\\RecentActivityEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#" count: 1 @@ -5912,16 +5892,6 @@ parameters: count: 1 path: Invoice/Renderer/DocxRendererTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DocxRendererTest\\:\\:testRender\\(\\) has no return type specified\\.$#" - count: 1 - path: Invoice/Renderer/DocxRendererTest.php - - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DocxRendererTest\\:\\:testSupports\\(\\) has no return type specified\\.$#" - count: 1 - path: Invoice/Renderer/DocxRendererTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\OdsRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" count: 1 @@ -6907,11 +6877,6 @@ parameters: count: 1 path: Repository/Query/BaseQueryTest.php - - - message: "#^Method App\\\\Tests\\\\Repository\\\\Query\\\\BaseQueryTest\\:\\:assertDateRangeTrait\\(\\) has parameter \\$sut with no type specified\\.$#" - count: 1 - path: Repository/Query/BaseQueryTest.php - - message: "#^Method App\\\\Tests\\\\Repository\\\\Query\\\\BaseQueryTest\\:\\:assertOrder\\(\\) has parameter \\$order with no type specified\\.$#" count: 1