From 1e0fbf0b734a06fddf9a9573c1473bd5b410566e Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sat, 24 May 2025 14:28:39 +0200 Subject: [PATCH] Release 2.35 (#5470) * open up API for plugins by removing internal * use constants in entity column definition * simplified entity management API * bump packages * allow installing assets and run database migrations independently * bump to apidoc-bundle 5 * do not duplicate http method in API operationId * new security entries in Open API definition * changed API UI provider for Swagger to Stoplight, improved endpoint titles, hide internal endpoints * deactivate swagger json endpoint --- .github/release-drafter.yml | 3 +- composer.json | 2 +- composer.lock | 180 +++++++++--------- config/packages/nelmio_api_doc.yaml | 31 +-- config/routes.yaml | 7 +- src/API/ActionsController.php | 14 +- src/API/ActivityController.php | 14 +- src/API/ConfigurationController.php | 4 +- src/API/CustomerController.php | 14 +- src/API/InvoiceController.php | 8 +- src/API/ProjectController.php | 18 +- src/API/StatusController.php | 6 +- src/API/TagController.php | 9 +- src/API/TeamController.php | 28 +-- src/API/TimesheetController.php | 62 +++--- src/API/UserController.php | 8 +- src/Activity/ActivityService.php | 15 ++ .../AbstractBundleInstallerCommand.php | 62 +++--- src/Command/InstallCommand.php | 2 +- src/Command/ReloadCommand.php | 12 +- src/Constants.php | 4 +- src/Controller/ActivityController.php | 6 +- src/Controller/ProjectController.php | 6 +- src/Customer/CustomerService.php | 15 ++ src/Entity/AccessToken.php | 11 +- src/Entity/Activity.php | 20 +- src/Entity/Bookmark.php | 9 +- src/Entity/BudgetTrait.php | 7 +- src/Entity/ColorTrait.php | 3 +- src/Entity/CommentTableTypeTrait.php | 9 +- src/Entity/Configuration.php | 7 +- src/Entity/Customer.php | 40 ++-- src/Entity/Invoice.php | 25 +-- src/Entity/InvoiceTemplate.php | 29 +-- src/Entity/MetaTableTypeTrait.php | 9 +- src/Entity/Project.php | 32 ++-- src/Entity/Rate.php | 9 +- src/Entity/Role.php | 5 +- src/Entity/RolePermission.php | 7 +- src/Entity/Tag.php | 7 +- src/Entity/Team.php | 5 +- src/Entity/TeamMember.php | 5 +- src/Entity/Timesheet.php | 31 +-- src/Entity/User.php | 39 ++-- src/Entity/UserPreference.php | 7 +- src/Entity/WorkingTime.php | 11 +- src/Model/ActivityBudgetStatisticModel.php | 1 - src/Model/BudgetStatisticModel.php | 2 - src/Model/BudgetStatisticModelInterface.php | 3 - src/Model/CustomerBudgetStatisticModel.php | 1 - src/Model/ProjectBudgetStatisticModel.php | 1 - src/Project/ProjectDuplicationService.php | 2 +- src/Project/ProjectService.php | 21 +- src/Repository/ActivityRepository.php | 2 - src/Repository/CustomerRepository.php | 2 - src/Repository/ProjectRepository.php | 2 - src/Repository/Result/TimesheetResult.php | 3 - tests/API/ApiDocControllerTest.php | 31 ++- tests/Activity/ActivityServiceTest.php | 19 +- tests/Command/BundleInstallerCommandTest.php | 25 --- tests/Customer/CustomerServiceTest.php | 19 +- tests/Project/ProjectServiceTest.php | 21 +- tests/phpstan.neon | 35 ---- 63 files changed, 518 insertions(+), 529 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 9763f761..04a69bdd 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -7,6 +7,7 @@ exclude-labels: - 'release' exclude-contributors: - 'dependabot' + - 'weblate' change-template: '- $TITLE (#$NUMBER)' change-title-escapes: '\<*_&`#@' version-resolver: @@ -25,4 +26,4 @@ template: | $CHANGES - Involved in this release: $CONTRIBUTORS - thank you! + Involved in this release: $CONTRIBUTORS diff --git a/composer.json b/composer.json index d04c3d18..39819f8e 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "kevinpapst/tabler-bundle": "^1.4", "league/csv": "^9.4", "mpdf/mpdf": "^8.0", - "nelmio/api-doc-bundle": "^4.0", + "nelmio/api-doc-bundle": "^5.0", "nelmio/cors-bundle": "^2.0", "onelogin/php-saml": "^4.0", "openspout/openspout": "^4.0", diff --git a/composer.lock b/composer.lock index 387c24f2..dedc5efe 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": "583b9b2747672ce16a337f137a0fe507", + "content-hash": "05802c03af8f523624d93297a3a658eb", "packages": [ { "name": "azuyalabs/yasumi", @@ -180,6 +180,7 @@ "issues": "https://github.com/Behat/Transliterator/issues", "source": "https://github.com/Behat/Transliterator/tree/v1.5.0" }, + "abandoned": true, "time": "2022-03-30T09:27:43+00:00" }, { @@ -945,16 +946,16 @@ }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.4.1", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "e858ce0f5c12b266dce7dce24834448355155da7" + "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/e858ce0f5c12b266dce7dce24834448355155da7", - "reference": "e858ce0f5c12b266dce7dce24834448355155da7", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/5a6ac7120c2924c4c070a869d08b11ccf9e277b9", + "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9", "shasum": "" }, "require": { @@ -968,7 +969,6 @@ "composer/semver": "^3.0", "doctrine/coding-standard": "^12", "doctrine/orm": "^2.6 || ^3", - "doctrine/persistence": "^2.0 || ^3", "phpstan/phpstan": "^1.4 || ^2", "phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpstan/phpstan-phpunit": "^1 || ^2", @@ -1011,7 +1011,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.1" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.2" }, "funding": [ { @@ -1027,7 +1027,7 @@ "type": "tidelift" } ], - "time": "2025-01-27T22:48:22+00:00" + "time": "2025-03-11T17:36:26+00:00" }, { "name": "doctrine/event-manager", @@ -2463,16 +2463,16 @@ }, { "name": "kevinpapst/tabler-bundle", - "version": "1.8.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/kevinpapst/TablerBundle.git", - "reference": "7cf2672d7181041efe0ccb9b1186bc0d87816735" + "reference": "a1b09b766a4bfd909217db83cab7a794b25d2cb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/7cf2672d7181041efe0ccb9b1186bc0d87816735", - "reference": "7cf2672d7181041efe0ccb9b1186bc0d87816735", + "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/a1b09b766a4bfd909217db83cab7a794b25d2cb6", + "reference": "a1b09b766a4bfd909217db83cab7a794b25d2cb6", "shasum": "" }, "require": { @@ -2522,7 +2522,7 @@ "description": "Admin/Backend theme bundle for Symfony based on Tabler.io", "support": { "issues": "https://github.com/kevinpapst/TablerBundle/issues", - "source": "https://github.com/kevinpapst/TablerBundle/tree/1.8.0" + "source": "https://github.com/kevinpapst/TablerBundle/tree/1.8.1" }, "funding": [ { @@ -2534,7 +2534,7 @@ "type": "github" } ], - "time": "2025-04-20T12:41:26+00:00" + "time": "2025-05-16T14:21:41+00:00" }, { "name": "league/csv", @@ -3202,74 +3202,73 @@ }, { "name": "nelmio/api-doc-bundle", - "version": "v4.38.2", + "version": "v5.2.0", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "fdc1cf5bc57287787db59f205a8e77485bd22072" + "reference": "f48d9cb3930a34df209547ffd86db01254d5fce8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/fdc1cf5bc57287787db59f205a8e77485bd22072", - "reference": "fdc1cf5bc57287787db59f205a8e77485bd22072", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/f48d9cb3930a34df209547ffd86db01254d5fce8", + "reference": "f48d9cb3930a34df209547ffd86db01254d5fce8", "shasum": "" }, "require": { - "ext-json": "*", - "php": ">=7.4", - "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^5.0", "phpdocumentor/type-resolver": "^1.8.2", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/config": "^5.4 || ^6.4 || ^7.1", - "symfony/console": "^5.4 || ^6.4 || ^7.1", - "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.1", + "symfony/config": "^6.4 || ^7.1", + "symfony/console": "^6.4 || ^7.1", + "symfony/dependency-injection": "^6.4 || ^7.1", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/framework-bundle": "^5.4.24 || ^6.4 || ^7.1", - "symfony/http-foundation": "^5.4 || ^6.4 || ^7.1", - "symfony/http-kernel": "^5.4 || ^6.4 || ^7.1", - "symfony/options-resolver": "^5.4 || ^6.4 || ^7.1", - "symfony/property-info": "^5.4.10 || ^6.4 || ^7.1", - "symfony/routing": "^5.4 || ^6.4 || ^7.1", + "symfony/framework-bundle": "^6.4 || ^7.1", + "symfony/http-foundation": "^6.4 || ^7.1", + "symfony/http-kernel": "^6.4 || ^7.1", + "symfony/options-resolver": "^6.4 || ^7.1", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/routing": "^6.4 || ^7.1", "zircote/swagger-php": "^4.11.1 || ^5.0" }, "conflict": { "zircote/swagger-php": "4.8.7" }, "require-dev": { - "api-platform/core": "^2.7.0 || ^3", - "composer/package-versions-deprecated": "1.11.99.1", - "doctrine/annotations": "^2.0", + "api-platform/core": "^3.2", "friendsofphp/php-cs-fixer": "^3.52", - "friendsofsymfony/rest-bundle": "^2.8 || ^3.0", - "jms/serializer": "^1.14 || ^3.0", - "jms/serializer-bundle": "^2.3 || ^3.0 || ^4.0 || ^5.0", + "friendsofsymfony/rest-bundle": "^3.2.0", + "jms/serializer": "^3.32", + "jms/serializer-bundle": "^5.5", "phpstan/phpstan": "^1.10", "phpstan/phpstan-phpunit": "^1.3", "phpstan/phpstan-strict-rules": "^1.5", "phpstan/phpstan-symfony": "^1.3", - "phpunit/phpunit": "^9.6 || ^10.5", - "symfony/asset": "^5.4 || ^6.4 || ^7.1", - "symfony/browser-kit": "^5.4 || ^6.4 || ^7.1", - "symfony/cache": "^5.4 || ^6.4 || ^7.1", - "symfony/dom-crawler": "^5.4 || ^6.4 || ^7.1", - "symfony/expression-language": "^5.4 || ^6.4 || ^7.1", - "symfony/form": "^5.4 || ^6.4 || ^7.1", - "symfony/phpunit-bridge": "^6.4", - "symfony/property-access": "^5.4 || ^6.4 || ^7.1", - "symfony/security-csrf": "^5.4 || ^6.4 || ^7.1", - "symfony/serializer": "^5.4 || ^6.4 || ^7.1", - "symfony/stopwatch": "^5.4 || ^6.4 || ^7.1", - "symfony/templating": "^5.4 || ^6.4 || ^7.1", - "symfony/twig-bundle": "^5.4 || ^6.4 || ^7.1", - "symfony/uid": "^5.4 || ^6.4 || ^7.1", - "symfony/validator": "^5.4 || ^6.4 || ^7.1", - "willdurand/hateoas-bundle": "^1.0 || ^2.0" + "phpunit/phpunit": "^10.5", + "symfony/asset": "^6.4 || ^7.1", + "symfony/browser-kit": "^6.4 || ^7.1", + "symfony/cache": "^6.4 || ^7.1", + "symfony/dom-crawler": "^6.4 || ^7.1", + "symfony/expression-language": "^6.4 || ^7.1", + "symfony/finder": "^6.4 || ^7.1", + "symfony/form": "^6.4 || ^7.1", + "symfony/phpunit-bridge": "^6.4 || ^7.1", + "symfony/property-access": "^6.4 || ^7.1", + "symfony/security-csrf": "^6.4 || ^7.1", + "symfony/security-http": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", + "symfony/stopwatch": "^6.4 || ^7.1", + "symfony/templating": "^6.4 || ^7.1", + "symfony/twig-bundle": "^6.4 || ^7.1", + "symfony/uid": "^6.4 || ^7.1", + "symfony/validator": "^6.4 || ^7.1", + "willdurand/hateoas-bundle": "^2.7", + "willdurand/negotiation": "^3.0" }, "suggest": { "api-platform/core": "For using an API oriented framework.", - "doctrine/annotations": "For using doctrine annotations", "friendsofsymfony/rest-bundle": "For using the parameters annotations.", "jms/serializer-bundle": "For describing your models.", "symfony/asset": "For using the Swagger UI.", @@ -3285,7 +3284,8 @@ "type": "symfony-bundle", "extra": { "branch-alias": { - "dev-4.x": "4.x-dev" + "dev-4.x": "4.x-dev", + "dev-5.x": "5.x-dev" } }, "autoload": { @@ -3303,7 +3303,7 @@ "homepage": "https://github.com/nelmio/NelmioApiDocBundle/contributors" } ], - "description": "Generates documentation for your REST API from annotations and attributes", + "description": "Generates documentation for your REST API from attributes", "keywords": [ "api", "doc", @@ -3312,7 +3312,7 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.38.2" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v5.2.0" }, "funding": [ { @@ -3320,7 +3320,7 @@ "type": "github" } ], - "time": "2025-03-24T15:00:53+00:00" + "time": "2025-05-23T21:30:13+00:00" }, { "name": "nelmio/cors-bundle", @@ -6438,16 +6438,16 @@ }, { "name": "symfony/flex", - "version": "v2.5.0", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018" + "reference": "5d743b3b78fabe9f3146586d77b0a1f9292851fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/8ce1acd9842abe0e9b4c4a0bd3f259859516c018", - "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018", + "url": "https://api.github.com/repos/symfony/flex/zipball/5d743b3b78fabe9f3146586d77b0a1f9292851fc", + "reference": "5d743b3b78fabe9f3146586d77b0a1f9292851fc", "shasum": "" }, "require": { @@ -6486,7 +6486,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.5.0" + "source": "https://github.com/symfony/flex/tree/v2.7.0" }, "funding": [ { @@ -6502,7 +6502,7 @@ "type": "tidelift" } ], - "time": "2025-03-03T07:50:46+00:00" + "time": "2025-05-23T11:41:40+00:00" }, { "name": "symfony/form", @@ -10357,16 +10357,16 @@ }, { "name": "zircote/swagger-php", - "version": "5.1.1", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "7a6544c60441ddb5959b91266b3a290dc28537ba" + "reference": "b8ba6bd99805c0ae09a38d1b26c1c92820509bd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/7a6544c60441ddb5959b91266b3a290dc28537ba", - "reference": "7a6544c60441ddb5959b91266b3a290dc28537ba", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/b8ba6bd99805c0ae09a38d1b26c1c92820509bd0", + "reference": "b8ba6bd99805c0ae09a38d1b26c1c92820509bd0", "shasum": "" }, "require": { @@ -10437,9 +10437,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/5.1.1" + "source": "https://github.com/zircote/swagger-php/tree/5.1.3" }, - "time": "2025-04-27T10:02:08+00:00" + "time": "2025-05-20T03:35:10+00:00" } ], "packages-dev": [ @@ -11274,16 +11274,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.14", + "version": "2.1.17", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2" + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2", - "reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", "shasum": "" }, "require": { @@ -11328,25 +11328,25 @@ "type": "github" } ], - "time": "2025-05-02T15:32:28+00:00" + "time": "2025-05-21T20:55:28+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "9d8e7d4e32711715ad78a1fb6ec368df9af01fdf" + "reference": "468e02c9176891cc901143da118f09dc9505fc2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/9d8e7d4e32711715ad78a1fb6ec368df9af01fdf", - "reference": "9d8e7d4e32711715ad78a1fb6ec368df9af01fdf", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.13" + "phpstan/phpstan": "^2.1.15" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -11373,9 +11373,9 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.2" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" }, - "time": "2025-04-26T19:59:57+00:00" + "time": "2025-05-14T10:56:57+00:00" }, { "name": "phpstan/phpstan-doctrine", @@ -11552,22 +11552,22 @@ }, { "name": "phpstan/phpstan-symfony", - "version": "2.0.4", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "648087fb4dd865a09b1828a3b0396eb447665f2e" + "reference": "5005288e07583546ea00b52de4a9ac412eb869d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/648087fb4dd865a09b1828a3b0396eb447665f2e", - "reference": "648087fb4dd865a09b1828a3b0396eb447665f2e", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/5005288e07583546ea00b52de4a9ac412eb869d7", + "reference": "5005288e07583546ea00b52de4a9ac412eb869d7", "shasum": "" }, "require": { "ext-simplexml": "*", "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.2" + "phpstan/phpstan": "^2.1.13" }, "conflict": { "symfony/framework-bundle": "<3.0" @@ -11577,7 +11577,7 @@ "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6", - "psr/container": "1.0 || 1.1.1", + "psr/container": "1.1.2", "symfony/config": "^5.4 || ^6.1", "symfony/console": "^5.4 || ^6.1", "symfony/dependency-injection": "^5.4 || ^6.1", @@ -11617,9 +11617,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.4" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.6" }, - "time": "2025-03-28T12:02:03+00:00" + "time": "2025-05-14T07:00:05+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index e06ca83b..455ac9c8 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -1,4 +1,5 @@ nelmio_api_doc: + operation_id_generation: conditionally_prepend models: use_jms: true names: @@ -43,20 +44,28 @@ nelmio_api_doc: - { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] } - { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] } areas: - path_patterns: - - ^/api(?!/doc) + default: + path_patterns: + - ^/api(?!/doc) + security: + bearer: + type: 'http' + description: 'API Token' + bearerFormat: 'KIMAI' + scheme: 'bearer' documentation: info: title: Kimai - API description: | - JSON API for the Kimai time-tracking software. Read our [API documentation](https://www.kimai.org/documentation/rest-api.html) and download the [Open API definition](doc.json) to import into your API client. - version: '1.0' - components: - securitySchemes: - bearer: - type: http - scheme: bearer - bearerFormat: KIMAI - description: API Token + JSON API for the Kimai time-tracking software. Find more infos in our [API documentation](https://www.kimai.org/documentation/rest-api.html). + version: '1.1' security: - bearer: [] + html_config: +# assets_mode: cdn + # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ + swagger_ui_config: [] + # https://redocly.com/docs/redoc/config/ + redocly_config: [] + # https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options + stoplight_config: { basePath: '/api/doc', router: 'memory', logo: '/touch-icon-192x192.png', hideInternal: true } diff --git a/config/routes.yaml b/config/routes.yaml index ef0a2123..51e42a56 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -8,12 +8,7 @@ controllers: api.swagger_ui: path: /api/doc methods: GET - defaults: { _controller: nelmio_api_doc.controller.swagger_ui } - -api.swagger: - path: /api/doc.json - methods: GET - defaults: { _controller: nelmio_api_doc.controller.swagger } + defaults: { _controller: nelmio_api_doc.controller.stoplight } api: resource: ../src/API/ diff --git a/src/API/ActionsController.php b/src/API/ActionsController.php index 21cedde3..3000f91e 100644 --- a/src/API/ActionsController.php +++ b/src/API/ActionsController.php @@ -38,8 +38,6 @@ final class ActionsController extends BaseApiController } /** - * @param PageActionsEvent $event - * @param string $locale * @return array */ private function convertEvent(PageActionsEvent $event, string $locale): array @@ -65,12 +63,13 @@ final class ActionsController extends BaseApiController } /** - * Get all item actions for the given Timesheet [for internal use] + * Fetch item actions for Timesheet */ #[OA\Response(response: 200, description: 'Returns item actions for the timesheet', content: new OA\JsonContent(ref: new Model(type: PageAction::class)))] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to fetch', required: true)] #[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)] #[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)] + #[OA\Get(x: ['internal' => true])] #[Route(methods: ['GET'], path: '/timesheet/{id}/{view}/{locale}', name: 'get_timesheet_actions', requirements: ['id' => '\d+'])] public function getTimesheetActions(Timesheet $timesheet, string $view, string $locale): Response { @@ -83,12 +82,13 @@ final class ActionsController extends BaseApiController } /** - * Get all item actions for the given Activity [for internal use] + * Fetch item actions for Activity */ #[OA\Response(response: 200, description: 'Returns item actions for the activity', content: new OA\JsonContent(ref: new Model(type: PageAction::class)))] #[OA\Parameter(name: 'id', in: 'path', description: 'Activity ID to fetch', required: true)] #[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)] #[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)] + #[OA\Get(x: ['internal' => true])] #[Route(methods: ['GET'], path: '/activity/{id}/{view}/{locale}', name: 'get_activity_actions', requirements: ['id' => '\d+'])] public function getActivityActions(Activity $activity, string $view, string $locale): Response { @@ -101,12 +101,13 @@ final class ActionsController extends BaseApiController } /** - * Get all item actions for the given Project [for internal use] + * Fetch item actions for Project */ #[OA\Response(response: 200, description: 'Returns item actions for the project', content: new OA\JsonContent(ref: new Model(type: PageAction::class)))] #[OA\Parameter(name: 'id', in: 'path', description: 'Project ID to fetch', required: true)] #[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)] #[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)] + #[OA\Get(x: ['internal' => true])] #[Route(methods: ['GET'], path: '/project/{id}/{view}/{locale}', name: 'get_project_actions', requirements: ['id' => '\d+'])] public function getProjectActions(Project $project, string $view, string $locale): Response { @@ -119,12 +120,13 @@ final class ActionsController extends BaseApiController } /** - * Get all item actions for the given Customer [for internal use] + * Fetch item actions for Customer */ #[OA\Response(response: 200, description: 'Returns item actions for the customer', content: new OA\JsonContent(ref: new Model(type: PageAction::class)))] #[OA\Parameter(name: 'id', in: 'path', description: 'Customer ID to fetch', required: true)] #[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)] #[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)] + #[OA\Get(x: ['internal' => true])] #[Route(methods: ['GET'], path: '/customer/{id}/{view}/{locale}', name: 'get_customer_actions', requirements: ['id' => '\d+'])] public function getCustomerActions(Customer $customer, string $view, string $locale): Response { diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index 37057faf..866c159c 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -53,7 +53,7 @@ final class ActivityController extends BaseApiController } /** - * Returns a collection of activities (which are visible to the user) + * Fetch collection of activities */ #[OA\Response(response: 200, description: 'Returns a collection of activities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ActivityCollection')))] #[Route(methods: ['GET'], path: '', name: 'get_activities')] @@ -122,7 +122,7 @@ final class ActivityController extends BaseApiController } /** - * Returns one activity + * Fetch activity */ #[OA\Response(response: 200, description: 'Returns one activity entity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))] #[OA\Parameter(name: 'id', in: 'path', description: 'Activity ID to fetch', required: true)] @@ -137,7 +137,7 @@ final class ActivityController extends BaseApiController } /** - * Creates a new activity + * Create activity */ #[OA\Post(description: 'Creates a new activity and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created activity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))])] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityEditForm'))] @@ -231,7 +231,7 @@ final class ActivityController extends BaseApiController } /** - * Sets the value of a meta-field for an existing activity + * Update activity custom-field */ #[IsGranted('edit', 'activity')] #[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))] @@ -262,7 +262,7 @@ final class ActivityController extends BaseApiController } /** - * Returns a collection of all rates for one activity + * Fetch all rates for one activity */ #[IsGranted('edit', 'activity')] #[OA\Response(response: 200, description: 'Returns a collection of activity rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ActivityRate')))] @@ -279,7 +279,7 @@ final class ActivityController extends BaseApiController } /** - * Deletes one rate for an activity + * Delete rate for activity */ #[IsGranted('edit', 'activity')] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] @@ -300,7 +300,7 @@ final class ActivityController extends BaseApiController } /** - * Adds a new rate to an activity + * Add rate for one activity */ #[IsGranted('edit', 'activity')] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ActivityRate'))])] diff --git a/src/API/ConfigurationController.php b/src/API/ConfigurationController.php index a77a1b20..7cdeb65f 100644 --- a/src/API/ConfigurationController.php +++ b/src/API/ConfigurationController.php @@ -28,7 +28,7 @@ final class ConfigurationController extends BaseApiController } /** - * Returns the timesheet configuration + * Fetch timesheet configuration */ #[OA\Response(response: 200, description: 'Returns the instance specific timesheet configuration', content: new OA\JsonContent(ref: new Model(type: TimesheetConfig::class)))] #[Route(path: '/config/timesheet', methods: ['GET'])] @@ -48,7 +48,7 @@ final class ConfigurationController extends BaseApiController } /** - * Returns the configured color codes and names + * Fetch configured color codes */ #[OA\Response(response: 200, description: 'Returns the configured color codes and names', content: new OA\JsonContent(type: 'object', example: ['Red' => '#ff0000'], additionalProperties: new OA\AdditionalProperties(type: 'string')))] #[Route(path: '/config/colors', methods: ['GET'])] diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php index 3cf899dc..faf45a8a 100644 --- a/src/API/CustomerController.php +++ b/src/API/CustomerController.php @@ -52,7 +52,7 @@ final class CustomerController extends BaseApiController } /** - * Returns a collection of customers (which are visible to the user) + * Fetch collection of customers */ #[OA\Response(response: 200, description: 'Returns a collection of customers', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/CustomerCollection')))] #[Route(methods: ['GET'], path: '', name: 'get_customers')] @@ -98,7 +98,7 @@ final class CustomerController extends BaseApiController } /** - * Returns one customer + * Fetch customer */ #[OA\Response(response: 200, description: 'Returns one customer entity', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))] #[Route(methods: ['GET'], path: '/{id}', name: 'get_customer', requirements: ['id' => '\d+'])] @@ -112,7 +112,7 @@ final class CustomerController extends BaseApiController } /** - * Creates a new customer + * Create customer */ #[OA\Post(description: 'Creates a new customer and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created customer', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))])] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerEditForm'))] @@ -206,7 +206,7 @@ final class CustomerController extends BaseApiController } /** - * Sets the value of a meta-field for an existing customer + * Update customer custom-field */ #[IsGranted('edit', 'customer')] #[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))] @@ -237,7 +237,7 @@ final class CustomerController extends BaseApiController } /** - * Returns a collection of all rates for one customer + * Fetch all rates for one customer */ #[IsGranted('edit', 'customer')] #[OA\Response(response: 200, description: 'Returns a collection of customer rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/CustomerRate')))] @@ -254,7 +254,7 @@ final class CustomerController extends BaseApiController } /** - * Deletes one rate for a customer + * Delete rate for customer */ #[IsGranted('edit', 'customer')] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] @@ -275,7 +275,7 @@ final class CustomerController extends BaseApiController } /** - * Adds a new rate to a customer + * Add rate for one customer */ #[IsGranted('edit', 'customer')] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/CustomerRate'))])] diff --git a/src/API/InvoiceController.php b/src/API/InvoiceController.php index 2f1beedc..a3484158 100644 --- a/src/API/InvoiceController.php +++ b/src/API/InvoiceController.php @@ -38,9 +38,7 @@ final class InvoiceController extends BaseApiController } /** - * Returns a paginated collection of invoices. - * - * Needs permission: view_invoice + * Fetch collection of invoices */ #[IsGranted('view_invoice')] #[OA\Response(response: 200, description: 'Returns a collection of invoices', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/InvoiceCollection')))] @@ -89,9 +87,7 @@ final class InvoiceController extends BaseApiController } /** - * Returns one invoice. - * - * Needs permission: view_invoice + * Fetch invoice */ #[IsGranted('view_invoice')] #[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))] diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php index 11da0d9e..739c12c3 100644 --- a/src/API/ProjectController.php +++ b/src/API/ProjectController.php @@ -54,7 +54,7 @@ final class ProjectController extends BaseApiController } /** - * Returns a collection of projects (which are visible to the user) + * Fetch collection of projects */ #[OA\Response(response: 200, description: 'Returns a collection of projects', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ProjectCollection')))] #[Route(methods: ['GET'], path: '', name: 'get_projects')] @@ -165,7 +165,7 @@ final class ProjectController extends BaseApiController } /** - * Creates a new project + * Create project */ #[OA\Post(description: 'Creates a new project and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created project', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))])] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectEditForm'))] @@ -188,7 +188,7 @@ final class ProjectController extends BaseApiController $form->submit($request->request->all()); if ($form->isValid()) { - $this->projectService->saveNewProject($project); + $this->projectService->saveProject($project); $view = new View($project, 200); $view->getContext()->setGroups(self::GROUPS_ENTITY); @@ -232,7 +232,7 @@ final class ProjectController extends BaseApiController return $this->viewHandler->handle($view); } - $this->projectService->updateProject($project); + $this->projectService->saveProject($project); $view = new View($project, Response::HTTP_OK); $view->getContext()->setGroups(self::GROUPS_ENTITY); @@ -260,7 +260,7 @@ final class ProjectController extends BaseApiController } /** - * Sets the value of a meta-field for an existing project + * Update project custom-field */ #[IsGranted('edit', 'project')] #[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))] @@ -282,7 +282,7 @@ final class ProjectController extends BaseApiController $meta->setValue($value); - $this->projectService->updateProject($project); + $this->projectService->saveProject($project); $view = new View($project, 200); $view->getContext()->setGroups(self::GROUPS_ENTITY); @@ -291,7 +291,7 @@ final class ProjectController extends BaseApiController } /** - * Returns a collection of all rates for one project + * Fetch all rates for one project */ #[IsGranted('edit', 'project')] #[OA\Response(response: 200, description: 'Returns a collection of project rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ProjectRate')))] @@ -308,7 +308,7 @@ final class ProjectController extends BaseApiController } /** - * Deletes one rate for a project + * Delete rate for project */ #[IsGranted('edit', 'project')] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] @@ -329,7 +329,7 @@ final class ProjectController extends BaseApiController } /** - * Adds a new rate to a project + * Add rate for one project */ #[IsGranted('edit', 'project')] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ProjectRate'))])] diff --git a/src/API/StatusController.php b/src/API/StatusController.php index 815ab8e7..ac42a186 100644 --- a/src/API/StatusController.php +++ b/src/API/StatusController.php @@ -29,7 +29,7 @@ final class StatusController extends BaseApiController } /** - * A testing route for the API + * Testing route for the API */ #[OA\Response(response: 200, description: "A simple route that returns a 'pong', which you can use for testing the API", content: new OA\JsonContent(example: "{'message': 'pong'}"))] #[Route(methods: ['GET'], path: '/ping')] @@ -41,7 +41,7 @@ final class StatusController extends BaseApiController } /** - * Returns information about the Kimai release + * Fetch Kimai release */ #[OA\Response(response: 200, description: 'Returns version information about the current release', content: new OA\JsonContent(ref: new Model(type: Version::class)))] #[Route(methods: ['GET'], path: '/version')] @@ -51,7 +51,7 @@ final class StatusController extends BaseApiController } /** - * Returns information about installed Plugins + * Fetch installed Plugins */ #[OA\Response(response: 200, description: 'Returns a list of plugin names and versions', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: new Model(type: Plugin::class))))] #[Route(methods: ['GET'], path: '/plugins')] diff --git a/src/API/TagController.php b/src/API/TagController.php index 93c8d9de..41534c87 100644 --- a/src/API/TagController.php +++ b/src/API/TagController.php @@ -39,9 +39,10 @@ final class TagController extends BaseApiController } /** - * Deprecated: Fetch tags by filter as string collection + * Deprecated: Fetch tags as strings */ - #[OA\Response(response: 200, description: 'Returns the collection of all existing tags as string array', content: new OA\JsonContent(type: 'array', items: new OA\Items(type: 'string')))] + #[OA\Response(response: 200, description: 'DEPRECATED: Returns existing tags as string array', content: new OA\JsonContent(type: 'array', items: new OA\Items(type: 'string')))] + #[OA\Get(x: ['internal' => true])] #[Route(methods: ['GET'], name: 'get_tags')] #[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')] public function cgetAction(ParamFetcherInterface $paramFetcher): Response @@ -58,7 +59,7 @@ final class TagController extends BaseApiController } /** - * Fetch tags by filter (as full entities) + * Fetch collection of tags */ #[OA\Response(response: 200, description: 'Find the collection of all matching tags', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TagEntity')))] #[Route(path: '/find', name: 'get_tags_full', methods: ['GET'])] @@ -79,7 +80,7 @@ final class TagController extends BaseApiController } /** - * Creates a new tag + * Create tag */ #[OA\Post(description: 'Creates a new tag and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created tag', content: new OA\JsonContent(ref: '#/components/schemas/TagEntity'))])] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TagEditForm'))] diff --git a/src/API/TeamController.php b/src/API/TeamController.php index cc4783cf..ea218108 100644 --- a/src/API/TeamController.php +++ b/src/API/TeamController.php @@ -47,10 +47,10 @@ final class TeamController extends BaseApiController } /** - * Fetch all existing teams (which are visible to the user) + * Fetch collection of teams */ #[IsGranted('view_team')] - #[OA\Response(response: 200, description: 'Returns the collection of teams', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamCollection')))] + #[OA\Response(response: 200, description: 'Returns a collection of teams', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamCollection')))] #[Route(methods: ['GET'], path: '', name: 'get_teams')] public function cgetAction(): Response { @@ -66,7 +66,7 @@ final class TeamController extends BaseApiController } /** - * Returns one team + * Fetch team */ #[IsGranted('view_team')] #[OA\Response(response: 200, description: 'Returns one team entity', content: new OA\JsonContent(ref: '#/components/schemas/Team'))] @@ -80,7 +80,7 @@ final class TeamController extends BaseApiController } /** - * Delete a team + * Delete team */ #[IsGranted('delete_team')] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one team')])] @@ -96,7 +96,7 @@ final class TeamController extends BaseApiController } /** - * Creates a new team + * Create team */ #[IsGranted('create_team')] #[OA\Post(description: 'Creates a new team and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -125,7 +125,7 @@ final class TeamController extends BaseApiController } /** - * Update an existing team + * Update team */ #[IsGranted('edit_team')] #[OA\Patch(description: 'Update an existing team, you can pass all or just a subset of all attributes (passing members will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -163,7 +163,7 @@ final class TeamController extends BaseApiController } /** - * Add a new member to a team + * Add team member */ #[IsGranted('edit_team')] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new user to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -187,7 +187,7 @@ final class TeamController extends BaseApiController } /** - * Removes a member from the team + * Remove team member */ #[IsGranted('edit_team')] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a user from the team. The teamlead cannot be removed.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -215,7 +215,7 @@ final class TeamController extends BaseApiController } /** - * Grant the team access to a customer + * Grant team access to customer */ #[IsGranted('edit_team')] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new customer to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -238,7 +238,7 @@ final class TeamController extends BaseApiController } /** - * Revokes access for a customer from a team + * Revoke customer access from team */ #[IsGranted('edit_team')] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a customer from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -261,7 +261,7 @@ final class TeamController extends BaseApiController } /** - * Grant the team access to a project + * Grant team access to project */ #[IsGranted('edit_team')] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new project to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -284,7 +284,7 @@ final class TeamController extends BaseApiController } /** - * Revokes access for a project from a team + * Revoke project access from team */ #[IsGranted('edit_team')] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a project from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -307,7 +307,7 @@ final class TeamController extends BaseApiController } /** - * Grant the team access to an activity + * Grant team access to activity */ #[IsGranted('edit_team')] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new activity to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] @@ -330,7 +330,7 @@ final class TeamController extends BaseApiController } /** - * Revokes access for an activity from a team + * Revoke activity access from team */ #[IsGranted('edit_team')] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a activity from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index c2dc73a1..80be398a 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -68,10 +68,10 @@ final class TimesheetController extends BaseApiController } /** - * Returns a collection of timesheet records (which are visible to the user) + * Fetch collection of timesheets */ #[IsGranted(new Expression("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')"))] - #[OA\Response(response: 200, description: 'Returns a collection of timesheet records. The datetime fields are given in the users local time including the timezone offset (ISO-8601).', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollection')))] + #[OA\Response(response: 200, description: 'Returns a collection of timesheets. The datetime fields are given in the users local time including the timezone offset (ISO-8601).', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollection')))] #[Route(methods: ['GET'], path: '', name: 'get_timesheets')] #[Rest\QueryParam(name: 'user', requirements: '\d+|all', strict: true, nullable: true, description: "User ID to filter timesheets. Needs permission 'view_other_timesheet', pass 'all' to fetch data for all user (default: current user)")] #[Rest\QueryParam(name: 'users', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of user IDs to filter, e.g.: users[]=1&users[]=2 (ignored if user=all)')] @@ -270,11 +270,11 @@ final class TimesheetController extends BaseApiController } /** - * Returns one timesheet record + * Return timesheet */ #[IsGranted('view', 'timesheet')] - #[OA\Response(response: 200, description: 'Returns one timesheet record. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] - #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to fetch', required: true)] + #[OA\Response(response: 200, description: 'Returns one timesheet. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] + #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to fetch', required: true)] #[Route(methods: ['GET'], path: '/{id}', name: 'get_timesheet', requirements: ['id' => '\d+'])] public function getAction(Timesheet $timesheet): Response { @@ -285,10 +285,10 @@ final class TimesheetController extends BaseApiController } /** - * Creates a new timesheet record + * Create timesheet */ #[IsGranted('create_own_timesheet')] - #[OA\Post(description: 'Creates a new timesheet record for the current user and returns it afterwards.', responses: [new OA\Response(response: 200, description: 'Returns the new created timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])] + #[OA\Post(description: 'Creates a new timesheet for the current user and returns it afterwards.', responses: [new OA\Response(response: 200, description: 'Returns the new created timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))] #[Route(methods: ['POST'], path: '', name: 'post_timesheet')] #[Rest\QueryParam(name: 'full', strict: true, nullable: true, description: 'Allows to fetch fully serialized objects including subresources (TimesheetExpanded). Allowed values: true (default: false)')] @@ -338,11 +338,11 @@ final class TimesheetController extends BaseApiController } /** - * Update an existing timesheet record + * Update timesheet */ #[IsGranted('edit', 'timesheet')] - #[OA\Patch(description: 'Update an existing timesheet record, you can pass all or just a subset of the attributes.', responses: [new OA\Response(response: 200, description: 'Returns the updated timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])] - #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to update', required: true)] + #[OA\Patch(description: 'Update timesheet, you can pass all or just a subset of the attributes.', responses: [new OA\Response(response: 200, description: 'Returns the updated timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])] + #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to update', required: true)] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))] #[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_timesheet', requirements: ['id' => '\d+'])] public function patchAction(Request $request, Timesheet $timesheet): Response @@ -381,11 +381,11 @@ final class TimesheetController extends BaseApiController } /** - * Delete an existing timesheet record + * Delete timesheet */ #[IsGranted('delete', 'timesheet')] - #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet record')])] - #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to delete', required: true)] + #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet')])] + #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to delete', required: true)] #[Route(methods: ['DELETE'], path: '/{id}', name: 'delete_timesheet', requirements: ['id' => '\d+'])] public function deleteAction(Timesheet $timesheet): Response { @@ -397,10 +397,10 @@ final class TimesheetController extends BaseApiController } /** - * Returns the collection of recent user activities + * Fetch recent user activities */ #[IsGranted('view_own_timesheet')] - #[OA\Response(response: 200, description: 'Returns the collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))] + #[OA\Response(response: 200, description: 'Returns a collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))] #[Route(methods: ['GET'], path: '/recent', name: 'recent_timesheet')] #[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included. Default: today - 1 year (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')] #[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries (default: 10)')] @@ -431,10 +431,10 @@ final class TimesheetController extends BaseApiController } /** - * Returns the collection of active timesheet records + * Fetch active timesheets */ #[IsGranted('view_own_timesheet')] - #[OA\Response(response: 200, description: 'Returns the collection of active timesheet records for the current user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))] + #[OA\Response(response: 200, description: 'Returns a collection of active timesheets for the current user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))] #[Route(methods: ['GET'], path: '/active', name: 'active_timesheet')] public function activeAction(): Response { @@ -450,16 +450,17 @@ final class TimesheetController extends BaseApiController } /** - * Stops an active timesheet record. + * Stop active timesheet * * This route is available via GET and PATCH, as users over and over again run into errors when stopping. * Likely caused by a slow JS engine and a fast-click after page reload. */ #[IsGranted('stop', 'timesheet')] - #[OA\Response(response: 200, description: 'Stops an active timesheet record and returns it afterwards.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] - #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to stop', required: true)] + #[OA\Response(response: 200, description: 'Stops an active timesheet and returns it afterwards.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] + #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to stop', required: true)] #[Route(methods: ['GET'], path: '/{id}/stop', name: 'stop_timesheet_get', requirements: ['id' => '\d+'])] #[Route(methods: ['PATCH'], path: '/{id}/stop', name: 'stop_timesheet', requirements: ['id' => '\d+'])] + #[OA\Get(x: ['internal' => true])] public function stopAction(Timesheet $timesheet): Response { $this->service->stopTimesheet($timesheet); @@ -471,11 +472,12 @@ final class TimesheetController extends BaseApiController } /** - * Restarts a previously stopped timesheet record for the current user + * Restart a timesheet for the current user */ #[IsGranted('start', 'timesheet')] - #[OA\Response(response: 200, description: 'Restarts a timesheet record for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] - #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)] + #[OA\Response(response: 200, description: 'Restart a timesheet for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] + #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to restart', required: true)] + #[OA\Get(x: ['internal' => true])] #[Route(methods: ['GET'], path: '/{id}/restart', name: 'restart_timesheet_get', requirements: ['id' => '\d+'])] #[Route(methods: ['PATCH'], path: '/{id}/restart', name: 'restart_timesheet', requirements: ['id' => '\d+'])] #[Rest\RequestParam(name: 'copy', requirements: 'all', strict: true, nullable: true, description: 'Whether data should be copied to the new entry. Allowed values: all (default: nothing is copied)')] @@ -532,11 +534,11 @@ final class TimesheetController extends BaseApiController } /** - * Duplicates an existing timesheet record + * Duplicate a timesheet */ #[IsGranted('duplicate', 'timesheet')] - #[OA\Response(response: 200, description: 'Duplicates a timesheet record, resetting the export state only.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] - #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to duplicate', required: true)] + #[OA\Response(response: 200, description: 'Duplicates a timesheet, resetting the export state only.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] + #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to duplicate', required: true)] #[Route(methods: ['PATCH'], path: '/{id}/duplicate', name: 'duplicate_timesheet', requirements: ['id' => '\d+'])] public function duplicateAction(Timesheet $timesheet): Response { @@ -553,11 +555,11 @@ final class TimesheetController extends BaseApiController } /** - * Switch the export state of a timesheet record to (un-)lock it + * Toggle timesheet export state */ #[IsGranted('edit_export', 'timesheet')] #[OA\Response(response: 200, description: 'Switches the exported state on the record and therefor locks / unlocks it for further updates. Needs edit_export_*_timesheet permission.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] - #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to switch export state', required: true)] + #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to switch export state', required: true)] #[Route(methods: ['PATCH'], path: '/{id}/export', name: 'export_timesheet', requirements: ['id' => '\d+'])] public function exportAction(Timesheet $timesheet): Response { @@ -576,11 +578,11 @@ final class TimesheetController extends BaseApiController } /** - * Sets the value of a meta-field for an existing timesheet. + * Update timesheet custom-field */ #[IsGranted('edit', 'timesheet')] #[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] - #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to set the meta-field value for', required: true)] + #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to set the meta-field value for', required: true)] #[Route(methods: ['PATCH'], path: '/{id}/meta', requirements: ['id' => '\d+'])] #[Rest\RequestParam(name: 'name', strict: true, nullable: false, description: 'The meta-field name')] #[Rest\RequestParam(name: 'value', strict: true, nullable: false, description: 'The meta-field value')] diff --git a/src/API/UserController.php b/src/API/UserController.php index a9cf50f1..96934718 100644 --- a/src/API/UserController.php +++ b/src/API/UserController.php @@ -47,10 +47,10 @@ final class UserController extends BaseApiController } /** - * Returns the collection of users (which are visible to the user) + * Fetch collection of users */ #[IsGranted('view_user')] - #[OA\Response(response: 200, description: 'Returns the collection of users. Required permission: view_user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/UserCollection')))] + #[OA\Response(response: 200, description: 'Returns a collection of users. Required permission: view_user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/UserCollection')))] #[Route(methods: ['GET'], path: '', name: 'get_users')] #[Rest\QueryParam(name: 'visible', requirements: '1|2|3', default: 1, strict: true, nullable: true, description: 'Visibility status to filter users: 1=visible, 2=hidden, 3=all')] #[Rest\QueryParam(name: 'orderBy', requirements: 'id|username|alias|email', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, username, alias, email (default: username)')] @@ -129,7 +129,7 @@ final class UserController extends BaseApiController } /** - * Creates a new user + * Create user */ #[IsGranted('create_user')] #[OA\Post(description: 'Creates a new user and returns it afterwards')] @@ -199,7 +199,7 @@ final class UserController extends BaseApiController } /** - * Delete an API token for the current user + * Delete API token for the current user */ #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Success if the token could be deleted.')])] #[OA\Parameter(name: 'id', in: 'path', description: 'The API token ID to remove', required: true)] diff --git a/src/Activity/ActivityService.php b/src/Activity/ActivityService.php index 6539cad2..3a429e7f 100644 --- a/src/Activity/ActivityService.php +++ b/src/Activity/ActivityService.php @@ -55,6 +55,18 @@ class ActivityService return $activity; } + public function saveActivity(Activity $activity): Activity + { + if ($activity->isNew()) { + return $this->saveNewActivity($activity); // @phpstan-ignore method.deprecated + } else { + return $this->updateActivity($activity); // @phpstan-ignore method.deprecated + } + } + + /** + * @deprecated since 2.35 - use saveActivity() instead + */ public function saveNewActivity(Activity $activity): Activity { if (null !== $activity->getId()) { @@ -89,6 +101,9 @@ class ActivityService } } + /** + * @deprecated since 2.35 - use saveActivity() instead + */ public function updateActivity(Activity $activity): Activity { $this->validateActivity($activity); diff --git a/src/Command/AbstractBundleInstallerCommand.php b/src/Command/AbstractBundleInstallerCommand.php index 567e3ddf..d9e3be84 100755 --- a/src/Command/AbstractBundleInstallerCommand.php +++ b/src/Command/AbstractBundleInstallerCommand.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Exception\FileNotFoundException; @@ -25,8 +26,6 @@ abstract class AbstractBundleInstallerCommand extends Command { /** * Returns the base directory to the Kimai installation. - * - * @return string */ protected function getRootDirectory(): string { @@ -39,18 +38,26 @@ abstract class AbstractBundleInstallerCommand extends Command /** * If your bundle ships assets, that need to be available in the public/ directory, * then overwrite this method and return: . - * - * @return bool */ protected function hasAssets(): bool { return false; } + private function hasMigrations(SymfonyStyle $io): bool + { + $config = $this->getMigrationConfigFilename(); + + if ($config === null) { + return false; + } + + return true; + } + /** - * Returns an absolute filename to your doctrine migrations configuration, if you want to install database tables. - * - * @return string|null + * Returns an absolute filename to your doctrine migrations configuration + * if you want to run database migrations. */ protected function getMigrationConfigFilename(): ?string { @@ -59,16 +66,12 @@ abstract class AbstractBundleInstallerCommand extends Command /** * Returns the bundle short name for the installer command. - * - * @return string */ abstract protected function getBundleCommandNamePart(): string; /** * Returns the full name fo this command. * Please stick to the standard and overwrite getBundleCommandNamePart() only. - * - * @return string */ protected function getInstallerCommandName(): string { @@ -77,8 +80,6 @@ abstract class AbstractBundleInstallerCommand extends Command /** * Returns the bundles real name (same as your namespace). - * - * @return string */ protected function getBundleName(): string { @@ -100,6 +101,8 @@ abstract class AbstractBundleInstallerCommand extends Command ->setName($this->getInstallerCommandName()) ->setDescription('Install the bundle: ' . $this->getBundleName()) ->setHelp('This command will perform the basic installation steps to get the bundle up and running.') + ->addOption('database', null, InputOption::VALUE_NONE, 'Only run the database installation') + ->addOption('assets', null, InputOption::VALUE_NONE, 'Only run the asset installation') ; } @@ -107,6 +110,15 @@ abstract class AbstractBundleInstallerCommand extends Command { $io = new SymfonyStyle($input, $output); + $noAssets = $input->getOption('database'); + $onlyAssets = $input->getOption('assets'); + + if ($noAssets && $onlyAssets) { + $io->error('Options --assets and --database are mutually exclusive'); + + return Command::FAILURE; + } + // many users execute the bin/console command from arbitrary locations // this will make sure that relative paths (like doctrine migrations) work as expected $path = getcwd(); @@ -117,17 +129,19 @@ abstract class AbstractBundleInstallerCommand extends Command \sprintf('Starting installation of plugin: %s ...', $bundleName) ); - try { - $this->importMigrations($io, $output); - } catch (\Exception $ex) { - $io->error( - \sprintf('Failed to install database for bundle %s. %s', $bundleName, $ex->getMessage()) - ); + if (!$onlyAssets && $this->hasMigrations($io)) { + try { + $this->importMigrations($io, $output); + } catch (\Exception $ex) { + $io->error( + \sprintf('Failed to install database for bundle %s. %s', $bundleName, $ex->getMessage()) + ); - return Command::FAILURE; + return Command::FAILURE; + } } - if ($this->hasAssets()) { + if (!$noAssets && $this->hasAssets()) { try { $this->installAssets($io, $output); } catch (\Exception $ex) { @@ -148,7 +162,7 @@ abstract class AbstractBundleInstallerCommand extends Command return Command::SUCCESS; } - protected function installAssets(SymfonyStyle $io, OutputInterface $output): void + private function installAssets(SymfonyStyle $io, OutputInterface $output): void { $command = $this->getApplication()->find('assets:install'); $cmdInput = new ArrayInput([]); @@ -160,11 +174,11 @@ abstract class AbstractBundleInstallerCommand extends Command $io->writeln(''); } - protected function importMigrations(SymfonyStyle $io, OutputInterface $output): void + private function importMigrations(SymfonyStyle $io, OutputInterface $output): void { $config = $this->getMigrationConfigFilename(); - if (null === $config) { + if ($config === null) { return; } diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 1468fca1..d3384378 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -25,7 +25,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; * * @codeCoverageIgnore */ -#[AsCommand(name: 'kimai:install', description: 'Kimai installation command', aliases: ['kimai:update'])] +#[AsCommand(name: 'kimai:install', description: 'Install and update Kimai', aliases: ['kimai:update'])] final class InstallCommand extends Command { public function __construct(private readonly Connection $connection) diff --git a/src/Command/ReloadCommand.php b/src/Command/ReloadCommand.php index e16a9d76..c7439427 100644 --- a/src/Command/ReloadCommand.php +++ b/src/Command/ReloadCommand.php @@ -22,10 +22,13 @@ use Symfony\Component\Console\Style\SymfonyStyle; * * @codeCoverageIgnore */ -#[AsCommand(name: 'kimai:reload')] +#[AsCommand(name: 'kimai:reload', description: 'Reload Kimai caches')] final class ReloadCommand extends Command { - public function __construct(private string $projectDirectory, private string $kernelEnvironment) + public function __construct( + private readonly string $projectDirectory, + private readonly string $kernelEnvironment + ) { parent::__construct(); } @@ -42,10 +45,7 @@ final class ReloadCommand extends Command protected function configure(): void { - $this - ->setDescription('Reload Kimai caches') - ->setHelp('This command will validate the configurations and translations and then clear and rebuild the application cache.') - ; + $this->setHelp('This will validate configurations and translations and then clear and rebuild the application cache.'); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Constants.php b/src/Constants.php index 40b8dc6b..6c41467c 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ final class Constants /** * The current release version */ - public const VERSION = '2.34.0'; + public const VERSION = '2.35.0'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 23400; + public const VERSION_ID = 23500; /** * The software name */ diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php index 2730b2b9..130c5bdc 100644 --- a/src/Controller/ActivityController.php +++ b/src/Controller/ActivityController.php @@ -271,7 +271,7 @@ final class ActivityController extends AbstractController if ($editForm->isSubmitted() && $editForm->isValid()) { try { - $this->activityService->saveNewActivity($activity); + $this->activityService->saveActivity($activity); $this->flashSuccess('action.update.success'); return $this->redirectToRouteAfterCreate('activity_details', ['id' => $activity->getId()]); @@ -300,7 +300,7 @@ final class ActivityController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { try { - $this->activityService->updateActivity($activity); + $this->activityService->saveActivity($activity); $this->flashSuccess('action.update.success'); if ($this->isGranted('view', $activity)) { @@ -355,7 +355,7 @@ final class ActivityController extends AbstractController if ($editForm->isSubmitted() && $editForm->isValid()) { try { - $this->activityService->updateActivity($activity); + $this->activityService->saveActivity($activity); $this->flashSuccess('action.update.success'); if ($this->isGranted('view', $activity)) { diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 91f8e5c7..d69546fa 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -154,7 +154,7 @@ final class ProjectController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { try { - $this->projectService->updateProject($project); + $this->projectService->saveProject($project); $this->flashSuccess('action.update.success'); if ($this->isGranted('view', $project)) { @@ -197,7 +197,7 @@ final class ProjectController extends AbstractController if ($editForm->isSubmitted() && $editForm->isValid()) { try { - $this->projectService->saveNewProject($project, new Context($this->getUser())); + $this->projectService->saveProject($project, new Context($this->getUser())); $this->flashSuccess('action.update.success'); return $this->redirectToRouteAfterCreate('project_details', ['id' => $project->getId()]); @@ -454,7 +454,7 @@ final class ProjectController extends AbstractController if ($editForm->isSubmitted() && $editForm->isValid()) { try { - $this->projectService->updateProject($project); + $this->projectService->saveProject($project); $this->flashSuccess('action.update.success'); if ($this->isGranted('view', $project)) { diff --git a/src/Customer/CustomerService.php b/src/Customer/CustomerService.php index 8ea2ee4b..51d0b762 100644 --- a/src/Customer/CustomerService.php +++ b/src/Customer/CustomerService.php @@ -59,6 +59,18 @@ final class CustomerService return $customer; } + public function saveCustomer(Customer $customer): Customer + { + if ($customer->isNew()) { + return $this->saveNewCustomer($customer); // @phpstan-ignore method.deprecated + } else { + return $this->updateCustomer($customer); // @phpstan-ignore method.deprecated + } + } + + /** + * @deprecated since 2.35 - use saveCustomer() instead + */ public function saveNewCustomer(Customer $customer): Customer { if (null !== $customer->getId()) { @@ -93,6 +105,9 @@ final class CustomerService } } + /** + * @deprecated since 2.35 - use saveCustomer() instead + */ public function updateCustomer(Customer $customer): Customer { $this->validateCustomer($customer); diff --git a/src/Entity/AccessToken.php b/src/Entity/AccessToken.php index b0a63420..eccdb58c 100644 --- a/src/Entity/AccessToken.php +++ b/src/Entity/AccessToken.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Repository\AccessTokenRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity(fields: ['token'])] class AccessToken { - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; @@ -29,17 +30,17 @@ class AccessToken #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Assert\NotNull] private User $user; - #[ORM\Column(name: 'token', type: 'string', length: 100, nullable: false)] + #[ORM\Column(name: 'token', type: Types::STRING, length: 100, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 100)] private string $token; - #[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 50)] private ?string $name = null; - #[ORM\Column(name: 'last_usage', type: 'datetime_immutable', nullable: true)] + #[ORM\Column(name: 'last_usage', type: Types::DATETIME_IMMUTABLE, nullable: true)] private ?\DateTimeImmutable $lastUsage = null; - #[ORM\Column(name: 'expires_at', type: 'datetime_immutable', nullable: true)] + #[ORM\Column(name: 'expires_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] private ?\DateTimeImmutable $expiresAt = null; public function __construct(User $user, string $token) diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php index 0410069d..8f6bfcac 100644 --- a/src/Entity/Activity.php +++ b/src/Entity/Activity.php @@ -16,6 +16,7 @@ use App\Repository\ActivityRepository; use App\Validator\Constraints as Constraints; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use OpenApi\Attributes as OA; @@ -42,7 +43,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Unique activity ID */ - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[Serializer\Expose] @@ -58,7 +59,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Name of this activity */ - #[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 150, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 150)] #[Serializer\Expose] @@ -68,7 +69,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Description of this activity */ - #[ORM\Column(name: 'comment', type: 'text', nullable: true)] + #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'comment')] @@ -76,13 +77,13 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Whether this activity is visible and can be selected */ - #[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => true])] + #[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false, options: ['default' => true])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'visible', type: 'boolean')] private bool $visible = true; - #[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])] + #[ORM\Column(name: 'billable', type: Types::BOOLEAN, nullable: false, options: ['default' => true])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] @@ -113,9 +114,9 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt #[Serializer\Groups(['Activity'])] #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))] private Collection $teams; - #[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)] + #[ORM\Column(name: 'invoice_text', type: Types::TEXT, nullable: true)] private ?string $invoiceText = null; - #[ORM\Column(name: 'number', type: 'string', length: 10, nullable: true)] + #[ORM\Column(name: 'number', type: Types::STRING, length: 10, nullable: true)] #[Assert\Length(max: 10)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] @@ -134,6 +135,11 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt return $this->id; } + public function isNew(): bool + { + return $this->id === null; + } + public function getProject(): ?Project { return $this->project; diff --git a/src/Entity/Bookmark.php b/src/Entity/Bookmark.php index 35d58cd3..f4f34cca 100644 --- a/src/Entity/Bookmark.php +++ b/src/Entity/Bookmark.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Repository\BookmarkRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -25,7 +26,7 @@ class Bookmark public const COLUMN_VISIBILITY = 'columns'; public const TIMESHEET = 'timesheet'; - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; @@ -33,15 +34,15 @@ class Bookmark #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Assert\NotNull] private ?User $user = null; - #[ORM\Column(name: 'type', type: 'string', length: 20, nullable: false)] + #[ORM\Column(name: 'type', type: Types::STRING, length: 20, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 20)] private ?string $type = null; - #[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 50)] private ?string $name = null; - #[ORM\Column(name: 'content', type: 'text', nullable: false)] + #[ORM\Column(name: 'content', type: Types::TEXT, nullable: false)] private ?string $content = null; public function getId(): ?int diff --git a/src/Entity/BudgetTrait.php b/src/Entity/BudgetTrait.php index f3cde65b..18dab9d7 100644 --- a/src/Entity/BudgetTrait.php +++ b/src/Entity/BudgetTrait.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Export\Annotation as Exporter; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use Symfony\Component\Validator\Constraints as Assert; @@ -19,7 +20,7 @@ trait BudgetTrait /** * The total monetary budget, will be zero if not configured. */ - #[ORM\Column(name: 'budget', type: 'float', nullable: false)] + #[ORM\Column(name: 'budget', type: Types::FLOAT, nullable: false)] #[Assert\Range(min: 0.00, max: 900000000000.00)] #[Assert\NotNull] #[Serializer\Expose] @@ -29,7 +30,7 @@ trait BudgetTrait /** * The time budget in seconds, will be zero if not configured. */ - #[ORM\Column(name: 'time_budget', type: 'integer', nullable: false)] + #[ORM\Column(name: 'time_budget', type: Types::INTEGER, nullable: false)] #[Assert\Range(min: 0, max: 2145600000)] #[Assert\NotNull] #[Serializer\Expose] @@ -41,7 +42,7 @@ trait BudgetTrait * - null = default / full time * - month = monthly budget */ - #[ORM\Column(name: 'budget_type', type: 'string', length: 10, nullable: true)] + #[ORM\Column(name: 'budget_type', type: Types::STRING, length: 10, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Activity_Entity', 'Project_Entity', 'Customer_Entity'])] #[Exporter\Expose(label: 'budgetType')] diff --git a/src/Entity/ColorTrait.php b/src/Entity/ColorTrait.php index 986fc5b8..85396c53 100644 --- a/src/Entity/ColorTrait.php +++ b/src/Entity/ColorTrait.php @@ -12,6 +12,7 @@ namespace App\Entity; use App\Constants; use App\Export\Annotation as Exporter; use App\Validator\Constraints as Constraints; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; @@ -20,7 +21,7 @@ trait ColorTrait /** * The assigned color in HTML hex format, e.g. #dd1d00 */ - #[ORM\Column(name: 'color', type: 'string', length: 7, nullable: true)] + #[ORM\Column(name: 'color', type: Types::STRING, length: 7, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'color')] diff --git a/src/Entity/CommentTableTypeTrait.php b/src/Entity/CommentTableTypeTrait.php index 8a1b51d8..35e7529d 100644 --- a/src/Entity/CommentTableTypeTrait.php +++ b/src/Entity/CommentTableTypeTrait.php @@ -9,6 +9,7 @@ namespace App\Entity; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -16,19 +17,19 @@ trait CommentTableTypeTrait { #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] private ?int $id = null; - #[ORM\Column(name: 'message', type: 'text', nullable: false)] + #[ORM\Column(name: 'message', type: Types::TEXT, nullable: false)] #[Assert\NotNull] private ?string $message = null; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Assert\NotNull] private ?User $createdBy = null; - #[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)] + #[ORM\Column(name: 'created_at', type: Types::DATETIME_MUTABLE, nullable: false)] #[Assert\NotNull] private ?\DateTime $createdAt = null; - #[ORM\Column(name: 'pinned', type: 'boolean', nullable: false, options: ['default' => false])] + #[ORM\Column(name: 'pinned', type: Types::BOOLEAN, nullable: false, options: ['default' => false])] #[Assert\NotNull] private bool $pinned = false; diff --git a/src/Entity/Configuration.php b/src/Entity/Configuration.php index eb8f8772..49a9dd60 100644 --- a/src/Entity/Configuration.php +++ b/src/Entity/Configuration.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Repository\ConfigurationRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -23,13 +24,13 @@ class Configuration { #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] private ?int $id = null; - #[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 100, nullable: false)] #[Assert\NotNull] #[Assert\Length(min: 2, max: 100)] private ?string $name = null; - #[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)] + #[ORM\Column(name: 'value', type: Types::TEXT, length: 65535, nullable: true)] #[Assert\Length(max: 65535)] private ?string $value = null; diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 4931ce2e..98f141da 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -16,6 +16,7 @@ use App\Repository\CustomerRepository; use App\Validator\Constraints as Constraints; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use OpenApi\Attributes as OA; @@ -36,50 +37,50 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt use ColorTrait; use CreatedTrait; - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'id', type: 'integer')] private ?int $id = null; - #[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 150, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 150)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'name')] private ?string $name = null; - #[ORM\Column(name: 'number', type: 'string', length: 50, nullable: true)] + #[ORM\Column(name: 'number', type: Types::STRING, length: 50, nullable: true)] #[Assert\Length(max: 50)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'number')] private ?string $number = null; - #[ORM\Column(name: 'comment', type: 'text', nullable: true)] + #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'comment')] private ?string $comment = null; - #[ORM\Column(name: 'visible', type: 'boolean', nullable: false)] + #[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'visible', type: 'boolean')] private bool $visible = true; - #[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])] + #[ORM\Column(name: 'billable', type: Types::BOOLEAN, nullable: false, options: ['default' => true])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'billable', type: 'boolean')] private bool $billable = true; - #[ORM\Column(name: 'company', type: 'string', length: 100, nullable: true)] + #[ORM\Column(name: 'company', type: Types::STRING, length: 100, nullable: true)] #[Assert\Length(max: 100)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'company')] private ?string $company = null; - #[ORM\Column(name: 'vat_id', type: 'string', length: 50, nullable: true)] + #[ORM\Column(name: 'vat_id', type: Types::STRING, length: 50, nullable: true)] #[Assert\Length(max: 50)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] @@ -96,7 +97,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'address')] private ?string $address = null; - #[ORM\Column(name: 'country', type: 'string', length: 2, nullable: false)] + #[ORM\Column(name: 'country', type: Types::STRING, length: 2, nullable: false)] #[Assert\NotBlank] #[Assert\Country] #[Assert\Length(max: 2)] @@ -104,7 +105,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'country')] private ?string $country = null; - #[ORM\Column(name: 'currency', type: 'string', length: 3, nullable: false)] + #[ORM\Column(name: 'currency', type: Types::STRING, length: 3, nullable: false)] #[Assert\NotBlank] #[Assert\Currency] #[Assert\Length(max: 3)] @@ -112,19 +113,19 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt #[Serializer\Groups(['Customer'])] #[Exporter\Expose(label: 'currency')] private ?string $currency = self::DEFAULT_CURRENCY; - #[ORM\Column(name: 'phone', type: 'string', length: 30, nullable: true)] + #[ORM\Column(name: 'phone', type: Types::STRING, length: 30, nullable: true)] #[Assert\Length(max: 30)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'phone')] private ?string $phone = null; - #[ORM\Column(name: 'fax', type: 'string', length: 30, nullable: true)] + #[ORM\Column(name: 'fax', type: Types::STRING, length: 30, nullable: true)] #[Assert\Length(max: 30)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'fax')] private ?string $fax = null; - #[ORM\Column(name: 'mobile', type: 'string', length: 30, nullable: true)] + #[ORM\Column(name: 'mobile', type: Types::STRING, length: 30, nullable: true)] #[Assert\Length(max: 30)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] @@ -133,13 +134,13 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Customers contact email */ - #[ORM\Column(name: 'email', type: 'string', length: 75, nullable: true)] + #[ORM\Column(name: 'email', type: Types::STRING, length: 75, nullable: true)] #[Assert\Length(max: 75)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'email')] private ?string $email = null; - #[ORM\Column(name: 'homepage', type: 'string', length: 100, nullable: true)] + #[ORM\Column(name: 'homepage', type: Types::STRING, length: 100, nullable: true)] #[Assert\Length(max: 100)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] @@ -148,7 +149,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Timezone of begin and end */ - #[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)] + #[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: false)] #[Assert\NotBlank] #[Assert\Timezone] #[Assert\Length(max: 64)] @@ -187,7 +188,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt #[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)] #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] private ?InvoiceTemplate $invoiceTemplate = null; - #[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)] + #[ORM\Column(name: 'invoice_text', type: Types::TEXT, nullable: true)] private ?string $invoiceText = null; public function __construct(string $name) @@ -203,6 +204,11 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt return $this->id; } + public function isNew(): bool + { + return $this->id === null; + } + public function setName(?string $name): void { $this->name = $name; diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index 424b238b..1a24189f 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -14,6 +14,7 @@ use App\Invoice\InvoiceModel; use App\Repository\InvoiceRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use OpenApi\Attributes as OA; @@ -44,20 +45,20 @@ class Invoice implements EntityWithMetaFields /** * Unique invoice ID */ - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'id', type: 'integer')] private ?int $id = null; - #[ORM\Column(name: 'invoice_number', type: 'string', length: 50, nullable: false)] + #[ORM\Column(name: 'invoice_number', type: Types::STRING, length: 50, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'invoice.number', type: 'string')] private ?string $invoiceNumber = null; - #[ORM\Column(name: 'comment', type: 'text', nullable: true)] + #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Invoice'])] #[Exporter\Expose(label: 'comment')] @@ -76,53 +77,53 @@ class Invoice implements EntityWithMetaFields #[Serializer\Groups(['Default'])] #[OA\Property(ref: '#/components/schemas/User')] private ?User $user = null; - #[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)] + #[ORM\Column(name: 'created_at', type: Types::DATETIME_MUTABLE, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?\DateTime $createdAt = null; - #[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)] + #[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: false)] private ?string $timezone = null; - #[ORM\Column(name: 'total', type: 'float', nullable: false)] + #[ORM\Column(name: 'total', type: Types::FLOAT, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'total_rate', type: 'float')] private float $total = 0.00; - #[ORM\Column(name: 'tax', type: 'float', nullable: false)] + #[ORM\Column(name: 'tax', type: Types::FLOAT, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'invoice.tax', type: 'float')] private float $tax = 0.00; - #[ORM\Column(name: 'currency', type: 'string', length: 3, nullable: false)] + #[ORM\Column(name: 'currency', type: Types::STRING, length: 3, nullable: false)] #[Assert\NotNull] #[Assert\Length(max: 3)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'currency', type: 'string')] private ?string $currency = null; - #[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)] + #[ORM\Column(name: 'due_days', type: Types::INTEGER, length: 3, nullable: false)] #[Assert\NotNull] #[Assert\Range(min: 0, max: 999)] #[Serializer\Expose] #[Serializer\Groups(['Invoice'])] #[Exporter\Expose(label: 'due_days', type: 'integer')] private int $dueDays = 30; - #[ORM\Column(name: 'vat', type: 'float', nullable: false)] + #[ORM\Column(name: 'vat', type: Types::FLOAT, nullable: false)] #[Assert\NotNull] #[Assert\Range(min: 0.0, max: 99.99)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'tax_rate', type: 'float')] private float $vat = 0.00; - #[ORM\Column(name: 'status', type: 'string', length: 20, nullable: false)] + #[ORM\Column(name: 'status', type: Types::STRING, length: 20, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'status', type: 'string')] private string $status = self::STATUS_NEW; - #[ORM\Column(name: 'invoice_filename', type: 'string', length: 150, nullable: false)] + #[ORM\Column(name: 'invoice_filename', type: Types::STRING, length: 150, nullable: false)] #[Assert\NotNull] #[Assert\Length(min: 1, max: 150)] #[Exporter\Expose(label: 'file', type: 'string')] diff --git a/src/Entity/InvoiceTemplate.php b/src/Entity/InvoiceTemplate.php index e87b7a94..7b5f1fe1 100644 --- a/src/Entity/InvoiceTemplate.php +++ b/src/Entity/InvoiceTemplate.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Repository\InvoiceTemplateRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -21,15 +22,15 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity('name')] class InvoiceTemplate { - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; - #[ORM\Column(name: 'name', type: 'string', length: 60, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 60, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 1, max: 60)] private ?string $name = null; - #[ORM\Column(name: 'title', type: 'string', length: 255, nullable: false)] + #[ORM\Column(name: 'title', type: Types::STRING, length: 255, nullable: false)] #[Assert\Length(max: 255)] #[Assert\NotBlank] private ?string $title = null; @@ -37,41 +38,41 @@ class InvoiceTemplate #[Assert\Length(max: 255)] #[Assert\NotBlank] private ?string $company = null; - #[ORM\Column(name: 'vat_id', type: 'string', length: 50, nullable: true)] + #[ORM\Column(name: 'vat_id', type: Types::STRING, length: 50, nullable: true)] #[Assert\Length(max: 50)] private ?string $vatId = null; - #[ORM\Column(name: 'address', type: 'text', nullable: true)] + #[ORM\Column(name: 'address', type: Types::TEXT, nullable: true)] private ?string $address = null; - #[ORM\Column(name: 'contact', type: 'text', nullable: true)] + #[ORM\Column(name: 'contact', type: Types::TEXT, nullable: true)] private ?string $contact = null; - #[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)] + #[ORM\Column(name: 'due_days', type: Types::INTEGER, length: 3, nullable: false)] #[Assert\NotNull] #[Assert\Range(min: 0, max: 999)] private ?int $dueDays = 30; - #[ORM\Column(name: 'vat', type: 'float', nullable: false)] + #[ORM\Column(name: 'vat', type: Types::FLOAT, nullable: false)] #[Assert\NotNull] #[Assert\Range(min: 0.0, max: 99.99)] private ?float $vat = 0.00; - #[ORM\Column(name: 'calculator', type: 'string', length: 20, nullable: false)] + #[ORM\Column(name: 'calculator', type: Types::STRING, length: 20, nullable: false)] #[Assert\NotBlank] #[Assert\Length(max: 20)] private string $calculator = 'default'; - #[ORM\Column(name: 'number_generator', type: 'string', length: 20, nullable: false)] + #[ORM\Column(name: 'number_generator', type: Types::STRING, length: 20, nullable: false)] #[Assert\NotBlank] #[Assert\Length(max: 20)] private string $numberGenerator = 'default'; - #[ORM\Column(name: 'renderer', type: 'string', length: 20, nullable: false)] + #[ORM\Column(name: 'renderer', type: Types::STRING, length: 20, nullable: false)] #[Assert\NotBlank] #[Assert\Length(max: 20)] private string $renderer = 'default'; - #[ORM\Column(name: 'payment_terms', type: 'text', nullable: true)] + #[ORM\Column(name: 'payment_terms', type: Types::TEXT, nullable: true)] private ?string $paymentTerms = null; - #[ORM\Column(name: 'payment_details', type: 'text', nullable: true)] + #[ORM\Column(name: 'payment_details', type: Types::TEXT, nullable: true)] private ?string $paymentDetails = null; /** * Used for translations and formatting money, numbers, dates and time. */ - #[ORM\Column(name: 'language', type: 'string', length: 6, nullable: false)] + #[ORM\Column(name: 'language', type: Types::STRING, length: 6, nullable: false)] #[Assert\NotBlank] private ?string $language = 'en'; diff --git a/src/Entity/MetaTableTypeTrait.php b/src/Entity/MetaTableTypeTrait.php index 151f2220..855e3a65 100644 --- a/src/Entity/MetaTableTypeTrait.php +++ b/src/Entity/MetaTableTypeTrait.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Form\Type\YesNoType; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -22,13 +23,13 @@ trait MetaTableTypeTrait { #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[Serializer\Exclude] private ?int $id = null; /** * Name of the meta (custom) field */ - #[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)] #[Assert\NotNull] #[Assert\Length(min: 2, max: 50)] #[Serializer\Expose] @@ -37,12 +38,12 @@ trait MetaTableTypeTrait /** * Value of the meta (custom) field */ - #[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)] + #[ORM\Column(name: 'value', type: Types::TEXT, length: 65535, nullable: true)] #[Assert\Length(max: 65535)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?string $value = null; - #[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => false])] + #[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false, options: ['default' => false])] #[Assert\NotNull] private bool $visible = false; private ?string $label = null; diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 331bd538..95207233 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -16,6 +16,7 @@ use App\Repository\ProjectRepository; use App\Validator\Constraints as Constraints; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use OpenApi\Attributes as OA; @@ -41,7 +42,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Unique Project ID */ - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[Serializer\Expose] @@ -61,7 +62,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Project name */ - #[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 150, nullable: false)] #[Assert\NotNull] #[Assert\Length(min: 2, max: 150)] #[Serializer\Expose] @@ -71,7 +72,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * Project order number */ - #[ORM\Column(name: 'order_number', type: 'text', length: 50, nullable: true)] + #[ORM\Column(name: 'order_number', type: Types::TEXT, length: 50, nullable: true)] #[Assert\Length(max: 50)] #[Serializer\Expose] #[Serializer\Groups(['Project_Entity'])] @@ -82,7 +83,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt * * Attention: Accessor MUST be used, otherwise date will be serialized in UTC. */ - #[ORM\Column(name: 'order_date', type: 'datetime', nullable: true)] + #[ORM\Column(name: 'order_date', type: Types::DATETIME_MUTABLE, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Project_Entity'])] #[Serializer\Type(name: "DateTime<'Y-m-d'>")] @@ -93,7 +94,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt * * Attention: Accessor MUST be used, otherwise date will be serialized in UTC. */ - #[ORM\Column(name: 'start', type: 'datetime', nullable: true)] + #[ORM\Column(name: 'start', type: Types::DATETIME_MUTABLE, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Project'])] #[Serializer\Type(name: "DateTime<'Y-m-d'>")] @@ -104,16 +105,16 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt * * Attention: Accessor MUST be used, otherwise date will be serialized in UTC. */ - #[ORM\Column(name: 'end', type: 'datetime', nullable: true)] + #[ORM\Column(name: 'end', type: Types::DATETIME_MUTABLE, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Project'])] #[Serializer\Type(name: "DateTime<'Y-m-d'>")] #[Serializer\Accessor(getter: 'getEnd')] private ?\DateTime $end = null; - #[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: true)] + #[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: true)] private ?string $timezone = null; private bool $localized = false; - #[ORM\Column(name: 'comment', type: 'text', nullable: true)] + #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'comment')] @@ -121,13 +122,13 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt /** * If the project is not visible, times cannot be recorded */ - #[ORM\Column(name: 'visible', type: 'boolean', nullable: false)] + #[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'visible', type: 'boolean')] private bool $visible = true; - #[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])] + #[ORM\Column(name: 'billable', type: Types::BOOLEAN, nullable: false, options: ['default' => true])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] @@ -158,17 +159,17 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt #[Serializer\Groups(['Project'])] #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))] private Collection $teams; - #[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)] + #[ORM\Column(name: 'invoice_text', type: Types::TEXT, nullable: true)] private ?string $invoiceText = null; /** * Whether this project allows booking of global activities */ - #[ORM\Column(name: 'global_activities', type: 'boolean', nullable: false, options: ['default' => true])] + #[ORM\Column(name: 'global_activities', type: Types::BOOLEAN, nullable: false, options: ['default' => true])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private bool $globalActivities = true; - #[ORM\Column(name: 'number', type: 'string', length: 10, nullable: true)] + #[ORM\Column(name: 'number', type: Types::STRING, length: 10, nullable: true)] #[Assert\Length(max: 10)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] @@ -187,6 +188,11 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt return $this->id; } + public function isNew(): bool + { + return $this->id === null; + } + public function getCustomer(): ?Customer { return $this->customer; diff --git a/src/Entity/Rate.php b/src/Entity/Rate.php index cb7b71ce..9cd2f790 100644 --- a/src/Entity/Rate.php +++ b/src/Entity/Rate.php @@ -9,6 +9,7 @@ namespace App\Entity; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use OpenApi\Attributes as OA; @@ -16,7 +17,7 @@ use Symfony\Component\Validator\Constraints as Assert; trait Rate { - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[Serializer\Expose] @@ -28,16 +29,16 @@ trait Rate #[Serializer\Groups(['Default'])] #[OA\Property(ref: '#/components/schemas/User')] private ?User $user = null; - #[ORM\Column(name: 'rate', type: 'float', nullable: false)] + #[ORM\Column(name: 'rate', type: Types::FLOAT, nullable: false)] #[Assert\GreaterThanOrEqual(0)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private float $rate = 0.00; - #[ORM\Column(name: 'internal_rate', type: 'float', nullable: true)] + #[ORM\Column(name: 'internal_rate', type: Types::FLOAT, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?float $internalRate = null; - #[ORM\Column(name: 'fixed', type: 'boolean', nullable: false)] + #[ORM\Column(name: 'fixed', type: Types::BOOLEAN, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] diff --git a/src/Entity/Role.php b/src/Entity/Role.php index c877becf..ebca72b1 100644 --- a/src/Entity/Role.php +++ b/src/Entity/Role.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Repository\RoleRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -21,11 +22,11 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity('name')] class Role { - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; - #[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(min: 5, max: 50)] diff --git a/src/Entity/RolePermission.php b/src/Entity/RolePermission.php index eda45186..c491010e 100644 --- a/src/Entity/RolePermission.php +++ b/src/Entity/RolePermission.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Repository\RolePermissionRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[UniqueEntity(['role', 'permission'])] class RolePermission { - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; @@ -29,10 +30,10 @@ class RolePermission #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Assert\NotNull] private ?Role $role = null; - #[ORM\Column(name: 'permission', type: 'string', length: 50, nullable: false)] + #[ORM\Column(name: 'permission', type: Types::STRING, length: 50, nullable: false)] #[Assert\Length(max: 50)] private ?string $permission = null; - #[ORM\Column(name: 'allowed', type: 'boolean', nullable: false, options: ['default' => false])] + #[ORM\Column(name: 'allowed', type: Types::BOOLEAN, nullable: false, options: ['default' => false])] #[Assert\NotNull] private bool $allowed = false; diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php index 1701ae80..3ae32199 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -11,6 +11,7 @@ namespace App\Entity; use App\Repository\TagRepository; use App\Utils\Color; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -28,7 +29,7 @@ class Tag /** * Internal Tag ID */ - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[Serializer\Expose] @@ -37,14 +38,14 @@ class Tag /** * The tag name */ - #[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 100, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 100, normalizer: 'trim')] #[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?string $name = null; - #[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => true])] + #[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false, options: ['default' => true])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] diff --git a/src/Entity/Team.php b/src/Entity/Team.php index c9c9d2e9..048a52eb 100644 --- a/src/Entity/Team.php +++ b/src/Entity/Team.php @@ -13,6 +13,7 @@ use App\Repository\TeamRepository; use App\Validator\Constraints as Constraints; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use OpenApi\Attributes as OA; @@ -28,7 +29,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[Constraints\Team] class Team { - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[Serializer\Expose] @@ -37,7 +38,7 @@ class Team /** * Team name */ - #[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 100, nullable: false)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 100)] #[Serializer\Expose] diff --git a/src/Entity/TeamMember.php b/src/Entity/TeamMember.php index 2aed3f3e..91f572f7 100644 --- a/src/Entity/TeamMember.php +++ b/src/Entity/TeamMember.php @@ -9,6 +9,7 @@ namespace App\Entity; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use OpenApi\Attributes as OA; @@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[Serializer\ExclusionPolicy('all')] class TeamMember { - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; @@ -39,7 +40,7 @@ class TeamMember #[Serializer\Groups(['Default', 'Entity', 'User_Entity'])] #[OA\Property(ref: '#/components/schemas/Team')] private ?Team $team = null; - #[ORM\Column(name: 'teamlead', type: 'boolean', nullable: false, options: ['default' => false])] + #[ORM\Column(name: 'teamlead', type: Types::BOOLEAN, nullable: false, options: ['default' => false])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default', 'Entity', 'Team_Entity', 'User_Entity'])] diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 06642ac6..cec08828 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -17,6 +17,7 @@ use DateTime; use DateTimeZone; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use OpenApi\Attributes as OA; @@ -81,7 +82,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt /** * Unique Timesheet ID */ - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[Serializer\Expose] @@ -91,7 +92,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt * Reflects the date in the user timezone (not in UTC). * This value is automatically set through the begin column and ONLY used in statistic queries. */ - #[ORM\Column(name: 'date_tz', type: 'date_immutable', nullable: false)] + #[ORM\Column(name: 'date_tz', type: Types::DATE_IMMUTABLE, nullable: false)] #[Assert\NotNull] private ?\DateTimeImmutable $date = null; /** @@ -99,7 +100,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt * * Attention: Accessor MUST be used, otherwise date will be serialized in UTC. */ - #[ORM\Column(name: 'start_time', type: 'datetime', nullable: false)] + #[ORM\Column(name: 'start_time', type: Types::DATETIME_MUTABLE, nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] @@ -111,7 +112,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt * * Attention: Accessor MUST be used, otherwise date will be serialized in UTC. */ - #[ORM\Column(name: 'end_time', type: 'datetime', nullable: true)] + #[ORM\Column(name: 'end_time', type: Types::DATETIME_MUTABLE, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Serializer\Type(name: 'DateTime')] @@ -120,18 +121,18 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt /** * @internal for storing the timezone of "begin" and "end" date */ - #[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)] + #[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: false)] #[Assert\Timezone] private ?string $timezone = null; /** * @internal for storing the localized state of dates (see $timezone) */ private bool $localized = false; - #[ORM\Column(name: 'duration', type: 'integer', nullable: true)] + #[ORM\Column(name: 'duration', type: Types::INTEGER, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?int $duration = 0; - #[ORM\Column(name: 'break', type: 'integer', nullable: true)] + #[ORM\Column(name: 'break', type: Types::INTEGER, nullable: true)] private ?int $break = 0; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] @@ -154,36 +155,36 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt #[Serializer\Groups(['Subresource', 'Expanded'])] #[OA\Property(ref: '#/components/schemas/ProjectExpanded')] private ?Project $project = null; - #[ORM\Column(name: 'description', type: 'text', nullable: true)] + #[ORM\Column(name: 'description', type: Types::TEXT, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?string $description = null; - #[ORM\Column(name: 'rate', type: 'float', nullable: false)] + #[ORM\Column(name: 'rate', type: Types::FLOAT, nullable: false)] #[Assert\GreaterThanOrEqual(0)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private float $rate = 0.00; - #[ORM\Column(name: 'internal_rate', type: 'float', nullable: true)] + #[ORM\Column(name: 'internal_rate', type: Types::FLOAT, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?float $internalRate = null; - #[ORM\Column(name: 'fixed_rate', type: 'float', nullable: true)] + #[ORM\Column(name: 'fixed_rate', type: Types::FLOAT, nullable: true)] #[Assert\GreaterThanOrEqual(0)] #[Serializer\Expose] #[Serializer\Groups(['Entity'])] private ?float $fixedRate = null; - #[ORM\Column(name: 'hourly_rate', type: 'float', nullable: true)] + #[ORM\Column(name: 'hourly_rate', type: Types::FLOAT, nullable: true)] #[Assert\GreaterThanOrEqual(0)] #[Serializer\Expose] #[Serializer\Groups(['Entity'])] private ?float $hourlyRate = null; - #[ORM\Column(name: 'exported', type: 'boolean', nullable: false, options: ['default' => false])] + #[ORM\Column(name: 'exported', type: Types::BOOLEAN, nullable: false, options: ['default' => false])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private bool $exported = false; - #[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])] + #[ORM\Column(name: 'billable', type: Types::BOOLEAN, nullable: false, options: ['default' => true])] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default'])] @@ -193,7 +194,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt */ #[Assert\NotNull] private ?string $billableMode = self::BILLABLE_DEFAULT; - #[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])] + #[ORM\Column(name: 'category', type: Types::STRING, length: 10, nullable: false, options: ['default' => 'work'])] #[Assert\NotNull] private ?string $category = self::WORK; /** diff --git a/src/Entity/User.php b/src/Entity/User.php index 07542034..3492bd67 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -17,6 +17,7 @@ use App\WorkingTime\Mode\WorkingTimeModeNone; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Exception; use JMS\Serializer\Annotation as Serializer; @@ -70,7 +71,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas */ #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'id', type: 'integer')] @@ -78,7 +79,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas /** * The user alias will be displayed in the frontend instead of the username */ - #[ORM\Column(name: 'alias', type: 'string', length: 60, nullable: true)] + #[ORM\Column(name: 'alias', type: Types::STRING, length: 60, nullable: true)] #[Assert\Length(max: 60)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] @@ -87,13 +88,13 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas /** * Registration date for the user */ - #[ORM\Column(name: 'registration_date', type: 'datetime', nullable: true)] + #[ORM\Column(name: 'registration_date', type: Types::DATETIME_MUTABLE, nullable: true)] #[Exporter\Expose(label: 'profile.registration_date', type: 'datetime')] private ?\DateTime $registeredAt = null; /** * An additional title for the user, like the Job position or Department */ - #[ORM\Column(name: 'title', type: 'string', length: 50, nullable: true)] + #[ORM\Column(name: 'title', type: Types::STRING, length: 50, nullable: true)] #[Assert\Length(max: 50)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] @@ -102,7 +103,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas /** * URL to the user avatar, will be auto-generated if empty */ - #[ORM\Column(name: 'avatar', type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'avatar', type: Types::STRING, length: 255, nullable: true)] #[Assert\Length(max: 255, groups: ['Profile'])] #[Serializer\Expose] #[Serializer\Groups(['User_Entity'])] @@ -110,7 +111,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas /** * API token (password) for this user */ - #[ORM\Column(name: 'api_token', type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'api_token', type: Types::STRING, length: 255, nullable: true)] private ?string $apiToken = null; /** * @internal to be set via form, must not be persisted @@ -148,7 +149,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas * * @internal for internal usage only */ - #[ORM\Column(name: 'auth', type: 'string', length: 20, nullable: true)] + #[ORM\Column(name: 'auth', type: Types::STRING, length: 20, nullable: true)] #[Assert\Length(max: 20)] private ?string $auth = self::AUTH_INTERNAL; /** @@ -157,32 +158,32 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas * @internal has no database mapping as the value is calculated from a permission */ private ?bool $isAllowedToSeeAllData = null; - #[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)] + #[ORM\Column(name: 'username', type: Types::STRING, length: 180, nullable: false)] #[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Regex(pattern: '/\//', match: false, groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Length(min: 2, max: 64, groups: ['Registration', 'UserCreate', 'Profile'])] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?string $username = null; - #[ORM\Column(name: 'email', type: 'string', length: 180, nullable: false)] + #[ORM\Column(name: 'email', type: Types::STRING, length: 180, nullable: false)] #[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Length(min: 2, max: 180)] #[Assert\Email(mode: 'html5', groups: ['Registration', 'UserCreate', 'Profile'])] private ?string $email = null; - #[ORM\Column(name: 'account', type: 'string', length: 30, nullable: true)] + #[ORM\Column(name: 'account', type: Types::STRING, length: 30, nullable: true)] #[Assert\Length(max: 30)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'account_number')] private ?string $accountNumber = null; - #[ORM\Column(name: 'enabled', type: 'boolean', nullable: false)] + #[ORM\Column(name: 'enabled', type: Types::BOOLEAN, nullable: false)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private bool $enabled = false; /** * Encrypted password. Must be persisted. */ - #[ORM\Column(name: 'password', type: 'string', nullable: false)] + #[ORM\Column(name: 'password', type: Types::STRING, nullable: false)] private ?string $password = null; /** * Plain password. Used for model validation, not persisted. @@ -190,20 +191,20 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas #[Assert\NotBlank(groups: ['Registration', 'PasswordUpdate', 'UserCreate'])] #[Assert\Length(min: 8, max: 60, groups: ['Registration', 'PasswordUpdate', 'UserCreate', 'ResetPassword', 'ChangePassword'])] private ?string $plainPassword = null; - #[ORM\Column(name: 'last_login', type: 'datetime', nullable: true)] + #[ORM\Column(name: 'last_login', type: Types::DATETIME_MUTABLE, nullable: true)] private ?DateTime $lastLogin = null; /** * Random string sent to the user email address in order to verify it. */ - #[ORM\Column(name: 'confirmation_token', type: 'string', length: 180, unique: true, nullable: true)] + #[ORM\Column(name: 'confirmation_token', type: Types::STRING, length: 180, unique: true, nullable: true)] #[Assert\Length(max: 180)] private ?string $confirmationToken = null; - #[ORM\Column(name: 'password_requested_at', type: 'datetime_immutable', nullable: true)] + #[ORM\Column(name: 'password_requested_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] private ?\DateTimeImmutable $passwordRequestedAt = null; /** * List of all role names */ - #[ORM\Column(name: 'roles', type: 'array', nullable: false)] + #[ORM\Column(name: 'roles', type: Types::ARRAY, nullable: false)] // @phpstan-ignore classConstant.deprecated #[Serializer\Expose] #[Serializer\Groups(['User_Entity'])] #[Serializer\Type('array')] @@ -213,11 +214,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas * If not empty two-factor authentication is enabled. * TODO reduce the length, which was initially forgotten and set to 255, as this is the default for MySQL with Doctrine (see migration Version20230126002049) */ - #[ORM\Column(name: 'totp_secret', type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'totp_secret', type: Types::STRING, length: 255, nullable: true)] private ?string $totpSecret = null; - #[ORM\Column(name: 'totp_enabled', type: 'boolean', nullable: false, options: ['default' => false])] + #[ORM\Column(name: 'totp_enabled', type: Types::BOOLEAN, nullable: false, options: ['default' => false])] private bool $totpEnabled = false; - #[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])] + #[ORM\Column(name: 'system_account', type: Types::BOOLEAN, nullable: false, options: ['default' => false])] private bool $systemAccount = false; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] diff --git a/src/Entity/UserPreference.php b/src/Entity/UserPreference.php index 0db38c21..243a215a 100644 --- a/src/Entity/UserPreference.php +++ b/src/Entity/UserPreference.php @@ -11,6 +11,7 @@ namespace App\Entity; use App\Form\Type\YesNoType; use App\WorkingTime\Calculator\WorkingTimeCalculatorDay; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -53,19 +54,19 @@ class UserPreference #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] private ?int $id = null; #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'preferences')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Assert\NotNull] private ?User $user = null; - #[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)] + #[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)] #[Assert\NotNull] #[Assert\Length(min: 2, max: 50)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private string $name; - #[ORM\Column(name: 'value', type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'value', type: Types::STRING, length: 255, nullable: true)] #[Assert\Length(max: 250)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] diff --git a/src/Entity/WorkingTime.php b/src/Entity/WorkingTime.php index e0df450f..b5df3ebd 100644 --- a/src/Entity/WorkingTime.php +++ b/src/Entity/WorkingTime.php @@ -10,6 +10,7 @@ namespace App\Entity; use App\Repository\WorkingTimeRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use Symfony\Component\Validator\Constraints as Assert; @@ -23,25 +24,25 @@ class WorkingTime { #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Column(name: 'id', type: Types::INTEGER)] private ?int $id = null; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Assert\NotNull] private ?User $user = null; - #[ORM\Column(name: 'date', type: 'date', nullable: false)] + #[ORM\Column(name: 'date', type: Types::DATE_MUTABLE, nullable: false)] #[Assert\NotNull] private \DateTimeInterface $date; - #[ORM\Column(name: 'expected', type: 'integer', nullable: false)] + #[ORM\Column(name: 'expected', type: Types::INTEGER, nullable: false)] #[Assert\NotNull] private int $expectedTime = 0; - #[ORM\Column(name: 'actual', type: 'integer', nullable: false)] + #[ORM\Column(name: 'actual', type: Types::INTEGER, nullable: false)] #[Assert\NotNull] private int $actualTime = 0; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')] private ?User $approvedBy = null; - #[ORM\Column(name: 'approved_at', type: 'datetime_immutable', nullable: true)] + #[ORM\Column(name: 'approved_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] #[Assert\NotNull] private ?\DateTimeImmutable $approvedAt = null; diff --git a/src/Model/ActivityBudgetStatisticModel.php b/src/Model/ActivityBudgetStatisticModel.php index 43c7d76b..511fb7fa 100644 --- a/src/Model/ActivityBudgetStatisticModel.php +++ b/src/Model/ActivityBudgetStatisticModel.php @@ -14,7 +14,6 @@ use App\Entity\Activity; /** * Object used to unify the access to budget data in charts. * - * @internal do not use in plugins, no BC promise given! * @method Activity getEntity() */ class ActivityBudgetStatisticModel extends BudgetStatisticModel diff --git a/src/Model/BudgetStatisticModel.php b/src/Model/BudgetStatisticModel.php index 6218fc0b..9652a152 100644 --- a/src/Model/BudgetStatisticModel.php +++ b/src/Model/BudgetStatisticModel.php @@ -14,8 +14,6 @@ use App\Model\Statistic\BudgetStatistic; /** * Object used to unify the access to budget data in charts. - * - * @internal do not use in plugins, no BC promise given! */ class BudgetStatisticModel implements BudgetStatisticModelInterface { diff --git a/src/Model/BudgetStatisticModelInterface.php b/src/Model/BudgetStatisticModelInterface.php index 1d22dc5d..df1fa806 100644 --- a/src/Model/BudgetStatisticModelInterface.php +++ b/src/Model/BudgetStatisticModelInterface.php @@ -9,9 +9,6 @@ namespace App\Model; -/** - * @internal do not use in plugins, no BC promise given! - */ interface BudgetStatisticModelInterface { public function isMonthlyBudget(): bool; diff --git a/src/Model/CustomerBudgetStatisticModel.php b/src/Model/CustomerBudgetStatisticModel.php index 333535a2..56c55cce 100644 --- a/src/Model/CustomerBudgetStatisticModel.php +++ b/src/Model/CustomerBudgetStatisticModel.php @@ -14,7 +14,6 @@ use App\Entity\Customer; /** * Object used to unify the access to budget data in charts. * - * @internal do not use in plugins, no BC promise given! * @method Customer getEntity() */ class CustomerBudgetStatisticModel extends BudgetStatisticModel diff --git a/src/Model/ProjectBudgetStatisticModel.php b/src/Model/ProjectBudgetStatisticModel.php index 7343dda0..8c897378 100644 --- a/src/Model/ProjectBudgetStatisticModel.php +++ b/src/Model/ProjectBudgetStatisticModel.php @@ -14,7 +14,6 @@ use App\Entity\Project; /** * Object used to unify the access to budget data in charts. * - * @internal do not use in plugins, no BC promise given! * @method Project getEntity() */ class ProjectBudgetStatisticModel extends BudgetStatisticModel diff --git a/src/Project/ProjectDuplicationService.php b/src/Project/ProjectDuplicationService.php index 212fcd9c..08ec71c7 100644 --- a/src/Project/ProjectDuplicationService.php +++ b/src/Project/ProjectDuplicationService.php @@ -46,7 +46,7 @@ final class ProjectDuplicationService $newProject->setEnd(null); } - $this->projectService->saveNewProject($newProject); + $this->projectService->saveProject($newProject); foreach ($this->projectRateRepository->getRatesForProject($project) as $rate) { $newRate = clone $rate; diff --git a/src/Project/ProjectService.php b/src/Project/ProjectService.php index fa7c328d..83130af1 100644 --- a/src/Project/ProjectService.php +++ b/src/Project/ProjectService.php @@ -56,6 +56,18 @@ final class ProjectService return $project; } + public function saveProject(Project $project, ?Context $context = null): Project + { + if ($project->isNew()) { + return $this->saveNewProject($project, $context); // @phpstan-ignore method.deprecated + } else { + return $this->updateProject($project); // @phpstan-ignore method.deprecated + } + } + + /** + * @deprecated since 2.35 - use saveProject() instead + */ public function saveNewProject(Project $project, ?Context $context = null): Project { if (null !== $project->getId()) { @@ -97,6 +109,9 @@ final class ProjectService } } + /** + * @deprecated since 2.35 - use saveProject() instead + */ public function updateProject(Project $project): Project { $this->validateProject($project); @@ -108,8 +123,12 @@ final class ProjectService return $project; } - public function findProjectByName(string $name): ?Project + public function findProjectByName(string $name, ?Customer $customer): ?Project { + if ($customer !== null) { + return $this->repository->findOneBy(['name' => $name, 'customer' => $customer->getId()]); + } + return $this->repository->findOneBy(['name' => $name]); } diff --git a/src/Repository/ActivityRepository.php b/src/Repository/ActivityRepository.php index 77670b91..45929e1a 100644 --- a/src/Repository/ActivityRepository.php +++ b/src/Repository/ActivityRepository.php @@ -152,8 +152,6 @@ class ActivityRepository extends EntityRepository /** * Returns a query builder that is used for ActivityType and your own 'query_builder' option. - * - * @internal */ public function getQueryBuilderForFormType(ActivityFormTypeQuery $query): QueryBuilder { diff --git a/src/Repository/CustomerRepository.php b/src/Repository/CustomerRepository.php index 5bbfd3ed..ee9e5f6a 100644 --- a/src/Repository/CustomerRepository.php +++ b/src/Repository/CustomerRepository.php @@ -134,8 +134,6 @@ class CustomerRepository extends EntityRepository /** * Returns a query builder that is used for CustomerType and your own 'query_builder' option. - * - * @internal */ public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder { diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index d23ee718..53c33a7e 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -147,8 +147,6 @@ class ProjectRepository extends EntityRepository /** * Returns a query builder that is used for ProjectType and your own 'query_builder' option. - * - * @internal */ public function getQueryBuilderForFormType(ProjectFormTypeQuery $query): QueryBuilder { diff --git a/src/Repository/Result/TimesheetResult.php b/src/Repository/Result/TimesheetResult.php index 18208abe..4161c020 100644 --- a/src/Repository/Result/TimesheetResult.php +++ b/src/Repository/Result/TimesheetResult.php @@ -18,9 +18,6 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; -/** - * @internal - */ final class TimesheetResult { private ?TimesheetResultStatistic $statisticCache = null; diff --git a/tests/API/ApiDocControllerTest.php b/tests/API/ApiDocControllerTest.php index 3afd7821..baaaa514 100644 --- a/tests/API/ApiDocControllerTest.php +++ b/tests/API/ApiDocControllerTest.php @@ -26,11 +26,21 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/doc'); - self::assertStringContainsString('Kimai', $client->getResponse()->getContent()); - $result = $client->getCrawler()->filter('script#swagger-data'); - $swaggerJson = json_decode($result->text(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + self::assertStringContainsString('<title>Kimai', $content); + self::assertStringContainsString('docs.apiDescriptionDocument', $content); + self::assertStringContainsString('const config = {"basePath":"/api/doc","router":"memory","logo":"/touch-icon-192x192.png","hideInternal":true};', $content); + $results = preg_match('/docs\.apiDescriptionDocument\ \=\ (.*)\.spec;/', $content, $matches); + self::assertNotFalse($results); + $swaggerJson = json_decode($matches[1], true); + self::assertIsArray($swaggerJson); + self::assertArrayHasKey('spec', $swaggerJson); + $json = $swaggerJson['spec']; + self::assertArrayHasKey('paths', $json); + $tags = []; - foreach ($swaggerJson['spec']['paths'] as $path) { + foreach ($json['paths'] as $path) { foreach ($path as $method) { foreach ($method['tags'] as $tag) { $tags[$tag] = $tag; @@ -45,13 +55,6 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase sort($expectedKeys); self::assertEquals($expectedKeys, $actual, \sprintf('Expected %s sections in API docs, but found %s.', \count($actual), \count($expectedKeys))); - } - - public function testGetJsonDocs(): void - { - $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $this->assertAccessIsGranted($client, '/api/doc.json'); - $json = json_decode($client->getResponse()->getContent(), true); $paths = [ '/api/actions/timesheet/{id}/{view}/{locale}', @@ -108,7 +111,7 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase self::assertEquals('3.0.0', $json['openapi']); self::assertArrayHasKey('info', $json); self::assertStringStartsWith('Kimai', $json['info']['title']); - self::assertEquals('1.0', $json['info']['version']); + self::assertEquals('1.1', $json['info']['version']); self::assertArrayHasKey('paths', $json); self::assertEquals($paths, array_keys($json['paths'])); @@ -119,10 +122,6 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase self::assertArrayHasKey('components', $json); self::assertArrayHasKey('schemas', $json['components']); self::assertArrayHasKey('securitySchemes', $json['components']); - - $result = json_decode($client->getResponse()->getContent(), true); - self::assertIsArray($result); - self::assertNotEmpty($result); } protected function createUrl(string $url): string diff --git a/tests/Activity/ActivityServiceTest.php b/tests/Activity/ActivityServiceTest.php index 8374a531..2412dd84 100644 --- a/tests/Activity/ActivityServiceTest.php +++ b/tests/Activity/ActivityServiceTest.php @@ -58,19 +58,6 @@ class ActivityServiceTest extends TestCase return $service; } - public function testCannotSavePersistedProjectAsNew(): void - { - $project = $this->createMock(Activity::class); - $project->expects($this->once())->method('getId')->willReturn(1); - - $sut = $this->getSut(); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot create activity, already persisted'); - - $sut->saveNewActivity($project); - } - public function testsaveNewActivityHasValidationError(): void { $constraints = new ConstraintViolationList(); @@ -84,7 +71,7 @@ class ActivityServiceTest extends TestCase $this->expectException(ValidationFailedException::class); $this->expectExceptionMessage('Validation Failed'); - $sut->saveNewActivity(new Activity()); + $sut->saveActivity(new Activity()); } public function testUpdateDispatchesEvents(): void @@ -107,7 +94,7 @@ class ActivityServiceTest extends TestCase $sut = $this->getSut($dispatcher); - $sut->updateActivity($project); + $sut->saveActivity($project); } public function testcreateNewActivityDispatchesEvents(): void @@ -143,7 +130,7 @@ class ActivityServiceTest extends TestCase $sut = $this->getSut($dispatcher); $activity = new Activity(); - $sut->saveNewActivity($activity); + $sut->saveActivity($activity); } public function testcreateNewActivityWithoutCustomer(): void diff --git a/tests/Command/BundleInstallerCommandTest.php b/tests/Command/BundleInstallerCommandTest.php index 037f69eb..f31da074 100644 --- a/tests/Command/BundleInstallerCommandTest.php +++ b/tests/Command/BundleInstallerCommandTest.php @@ -16,7 +16,6 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Tester\CommandTester; /** @@ -62,17 +61,6 @@ class BundleInstallerCommandTest extends KernelTestCase self::assertEquals(1, $commandTester->getStatusCode()); } - public function testAssetsInstallationFailure(): void - { - $command = $this->getCommand(AssetsInstallerFailureCommand::class); - $commandTester = new CommandTester($command); - $commandTester->execute(['command' => $command->getName()]); - $result = $commandTester->getDisplay(); - - self::assertStringContainsString('[ERROR] Failed to install assets for bundle TestBundle.', $result); - self::assertEquals(1, $commandTester->getStatusCode()); - } - public function testInvalidNamespaceWillRaiseException(): void { $this->expectException(LogicException::class); @@ -162,19 +150,6 @@ class InstallerWithMissingMigrationsCommand extends TestBundleInstallerCommand } } -class AssetsInstallerFailureCommand extends TestBundleInstallerCommand -{ - protected function hasAssets(): bool - { - return true; - } - - protected function installAssets(SymfonyStyle $io, OutputInterface $output): void - { - throw new \Exception('Problem occurred while installing assets.'); - } -} - class InstallerWithAssetsCommand extends TestBundleInstallerCommand { protected function hasAssets(): bool diff --git a/tests/Customer/CustomerServiceTest.php b/tests/Customer/CustomerServiceTest.php index 6d91ccdf..47a049b9 100644 --- a/tests/Customer/CustomerServiceTest.php +++ b/tests/Customer/CustomerServiceTest.php @@ -69,19 +69,6 @@ class CustomerServiceTest extends TestCase return new CustomerService($repository, $configuration, $validator, $dispatcher); } - public function testCannotSavePersistedCustomerAsNew(): void - { - $Customer = $this->createMock(Customer::class); - $Customer->expects($this->once())->method('getId')->willReturn(1); - - $sut = $this->getSut(); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot create customer, already persisted'); - - $sut->saveNewCustomer($Customer); - } - public function testSaveNewCustomerHasValidationError(): void { $constraints = new ConstraintViolationList(); @@ -95,7 +82,7 @@ class CustomerServiceTest extends TestCase $this->expectException(ValidationFailedException::class); $this->expectExceptionMessage('Validation Failed'); - $sut->saveNewCustomer(new Customer('foo')); + $sut->saveCustomer(new Customer('foo')); } public function testUpdateDispatchesEvents(): void @@ -118,7 +105,7 @@ class CustomerServiceTest extends TestCase $sut = $this->getSut($dispatcher); - $sut->updateCustomer($Customer); + $sut->saveCustomer($Customer); } public function testCreateNewCustomerDispatchesEvents(): void @@ -155,7 +142,7 @@ class CustomerServiceTest extends TestCase $sut = $this->getSut($dispatcher); $Customer = new Customer('foo'); - $sut->saveNewCustomer($Customer); + $sut->saveCustomer($Customer); } /** diff --git a/tests/Project/ProjectServiceTest.php b/tests/Project/ProjectServiceTest.php index 4a52b2c0..10dd943e 100644 --- a/tests/Project/ProjectServiceTest.php +++ b/tests/Project/ProjectServiceTest.php @@ -64,19 +64,6 @@ class ProjectServiceTest extends TestCase return new ProjectService($repository, $configuration, $dispatcher, $validator); } - public function testCannotSavePersistedProjectAsNew(): void - { - $project = $this->createMock(Project::class); - $project->expects($this->once())->method('getId')->willReturn(1); - - $sut = $this->getSut(); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot create project, already persisted'); - - $sut->saveNewProject($project, new Context(new User())); - } - public function testSaveNewProjectHasValidationError(): void { $constraints = new ConstraintViolationList(); @@ -90,7 +77,7 @@ class ProjectServiceTest extends TestCase $this->expectException(ValidationFailedException::class); $this->expectExceptionMessage('Validation Failed'); - $sut->saveNewProject(new Project(), new Context(new User())); + $sut->saveProject(new Project(), new Context(new User())); } public function testUpdateDispatchesEvents(): void @@ -113,7 +100,7 @@ class ProjectServiceTest extends TestCase $sut = $this->getSut($dispatcher); - $sut->updateProject($project); + $sut->saveProject($project); } public function testCreateNewProjectDispatchesEvents(): void @@ -149,7 +136,7 @@ class ProjectServiceTest extends TestCase $sut = $this->getSut($dispatcher); $project = new Project(); - $sut->saveNewProject($project, new Context(new User())); + $sut->saveProject($project, new Context(new User())); self::assertCount(0, $project->getTeams()); } @@ -170,7 +157,7 @@ class ProjectServiceTest extends TestCase $user->addTeam($team2); $project = new Project(); - $sut->saveNewProject($project, new Context($user)); + $sut->saveProject($project, new Context($user)); self::assertCount(2, $project->getTeams()); } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 24910436..9c7437d0 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -187,41 +187,6 @@ parameters: count: 13 path: API/ActivityControllerTest.php - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 3 - path: API/ApiDocControllerTest.php - - - - message: "#^Cannot access offset 'paths' on mixed\\.$#" - count: 1 - path: API/ApiDocControllerTest.php - - - - message: "#^Cannot access offset 'spec' on mixed\\.$#" - count: 1 - path: API/ApiDocControllerTest.php - - - - message: "#^Cannot access offset 'tags' on mixed\\.$#" - count: 1 - path: API/ApiDocControllerTest.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 2 - path: API/ApiDocControllerTest.php - - - - message: "#^Parameter \\#2 \\$array of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) expects array\\|ArrayAccess, mixed given\\.$#" - count: 1 - path: API/ApiDocControllerTest.php - - - - message: "#^Parameter \\#2 \\$haystack of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: API/ApiDocControllerTest.php - - message: "#^Cannot call method getValue\\(\\) on App\\\\Entity\\\\MetaTableTypeInterface\\|null\\.$#" count: 1