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
This commit is contained in:
Kevin Papst
2025-05-24 14:28:39 +02:00
committed by GitHub
parent 6e26a37c4d
commit 1e0fbf0b73
63 changed files with 518 additions and 529 deletions

View File

@@ -7,6 +7,7 @@ exclude-labels:
- 'release' - 'release'
exclude-contributors: exclude-contributors:
- 'dependabot' - 'dependabot'
- 'weblate'
change-template: '- $TITLE (#$NUMBER)' change-template: '- $TITLE (#$NUMBER)'
change-title-escapes: '\<*_&`#@' change-title-escapes: '\<*_&`#@'
version-resolver: version-resolver:
@@ -25,4 +26,4 @@ template: |
$CHANGES $CHANGES
Involved in this release: $CONTRIBUTORS - thank you! Involved in this release: $CONTRIBUTORS

View File

@@ -39,7 +39,7 @@
"kevinpapst/tabler-bundle": "^1.4", "kevinpapst/tabler-bundle": "^1.4",
"league/csv": "^9.4", "league/csv": "^9.4",
"mpdf/mpdf": "^8.0", "mpdf/mpdf": "^8.0",
"nelmio/api-doc-bundle": "^4.0", "nelmio/api-doc-bundle": "^5.0",
"nelmio/cors-bundle": "^2.0", "nelmio/cors-bundle": "^2.0",
"onelogin/php-saml": "^4.0", "onelogin/php-saml": "^4.0",
"openspout/openspout": "^4.0", "openspout/openspout": "^4.0",

180
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "583b9b2747672ce16a337f137a0fe507", "content-hash": "05802c03af8f523624d93297a3a658eb",
"packages": [ "packages": [
{ {
"name": "azuyalabs/yasumi", "name": "azuyalabs/yasumi",
@@ -180,6 +180,7 @@
"issues": "https://github.com/Behat/Transliterator/issues", "issues": "https://github.com/Behat/Transliterator/issues",
"source": "https://github.com/Behat/Transliterator/tree/v1.5.0" "source": "https://github.com/Behat/Transliterator/tree/v1.5.0"
}, },
"abandoned": true,
"time": "2022-03-30T09:27:43+00:00" "time": "2022-03-30T09:27:43+00:00"
}, },
{ {
@@ -945,16 +946,16 @@
}, },
{ {
"name": "doctrine/doctrine-migrations-bundle", "name": "doctrine/doctrine-migrations-bundle",
"version": "3.4.1", "version": "3.4.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git",
"reference": "e858ce0f5c12b266dce7dce24834448355155da7" "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/e858ce0f5c12b266dce7dce24834448355155da7", "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/5a6ac7120c2924c4c070a869d08b11ccf9e277b9",
"reference": "e858ce0f5c12b266dce7dce24834448355155da7", "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -968,7 +969,6 @@
"composer/semver": "^3.0", "composer/semver": "^3.0",
"doctrine/coding-standard": "^12", "doctrine/coding-standard": "^12",
"doctrine/orm": "^2.6 || ^3", "doctrine/orm": "^2.6 || ^3",
"doctrine/persistence": "^2.0 || ^3",
"phpstan/phpstan": "^1.4 || ^2", "phpstan/phpstan": "^1.4 || ^2",
"phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpstan/phpstan-phpunit": "^1 || ^2", "phpstan/phpstan-phpunit": "^1 || ^2",
@@ -1011,7 +1011,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", "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": [ "funding": [
{ {
@@ -1027,7 +1027,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-01-27T22:48:22+00:00" "time": "2025-03-11T17:36:26+00:00"
}, },
{ {
"name": "doctrine/event-manager", "name": "doctrine/event-manager",
@@ -2463,16 +2463,16 @@
}, },
{ {
"name": "kevinpapst/tabler-bundle", "name": "kevinpapst/tabler-bundle",
"version": "1.8.0", "version": "1.8.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/kevinpapst/TablerBundle.git", "url": "https://github.com/kevinpapst/TablerBundle.git",
"reference": "7cf2672d7181041efe0ccb9b1186bc0d87816735" "reference": "a1b09b766a4bfd909217db83cab7a794b25d2cb6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/7cf2672d7181041efe0ccb9b1186bc0d87816735", "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/a1b09b766a4bfd909217db83cab7a794b25d2cb6",
"reference": "7cf2672d7181041efe0ccb9b1186bc0d87816735", "reference": "a1b09b766a4bfd909217db83cab7a794b25d2cb6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2522,7 +2522,7 @@
"description": "Admin/Backend theme bundle for Symfony based on Tabler.io", "description": "Admin/Backend theme bundle for Symfony based on Tabler.io",
"support": { "support": {
"issues": "https://github.com/kevinpapst/TablerBundle/issues", "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": [ "funding": [
{ {
@@ -2534,7 +2534,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-04-20T12:41:26+00:00" "time": "2025-05-16T14:21:41+00:00"
}, },
{ {
"name": "league/csv", "name": "league/csv",
@@ -3202,74 +3202,73 @@
}, },
{ {
"name": "nelmio/api-doc-bundle", "name": "nelmio/api-doc-bundle",
"version": "v4.38.2", "version": "v5.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nelmio/NelmioApiDocBundle.git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git",
"reference": "fdc1cf5bc57287787db59f205a8e77485bd22072" "reference": "f48d9cb3930a34df209547ffd86db01254d5fce8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/fdc1cf5bc57287787db59f205a8e77485bd22072", "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/f48d9cb3930a34df209547ffd86db01254d5fce8",
"reference": "fdc1cf5bc57287787db59f205a8e77485bd22072", "reference": "f48d9cb3930a34df209547ffd86db01254d5fce8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "php": ">=8.1",
"php": ">=7.4", "phpdocumentor/reflection-docblock": "^5.0",
"phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0",
"phpdocumentor/type-resolver": "^1.8.2", "phpdocumentor/type-resolver": "^1.8.2",
"psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/cache": "^1.0 || ^2.0 || ^3.0",
"psr/container": "^1.0 || ^2.0", "psr/container": "^1.0 || ^2.0",
"psr/log": "^1.0 || ^2.0 || ^3.0", "psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/config": "^5.4 || ^6.4 || ^7.1", "symfony/config": "^6.4 || ^7.1",
"symfony/console": "^5.4 || ^6.4 || ^7.1", "symfony/console": "^6.4 || ^7.1",
"symfony/dependency-injection": "^5.4 || ^6.4 || ^7.1", "symfony/dependency-injection": "^6.4 || ^7.1",
"symfony/deprecation-contracts": "^2.1 || ^3", "symfony/deprecation-contracts": "^2.1 || ^3",
"symfony/framework-bundle": "^5.4.24 || ^6.4 || ^7.1", "symfony/framework-bundle": "^6.4 || ^7.1",
"symfony/http-foundation": "^5.4 || ^6.4 || ^7.1", "symfony/http-foundation": "^6.4 || ^7.1",
"symfony/http-kernel": "^5.4 || ^6.4 || ^7.1", "symfony/http-kernel": "^6.4 || ^7.1",
"symfony/options-resolver": "^5.4 || ^6.4 || ^7.1", "symfony/options-resolver": "^6.4 || ^7.1",
"symfony/property-info": "^5.4.10 || ^6.4 || ^7.1", "symfony/property-info": "^6.4 || ^7.1",
"symfony/routing": "^5.4 || ^6.4 || ^7.1", "symfony/routing": "^6.4 || ^7.1",
"zircote/swagger-php": "^4.11.1 || ^5.0" "zircote/swagger-php": "^4.11.1 || ^5.0"
}, },
"conflict": { "conflict": {
"zircote/swagger-php": "4.8.7" "zircote/swagger-php": "4.8.7"
}, },
"require-dev": { "require-dev": {
"api-platform/core": "^2.7.0 || ^3", "api-platform/core": "^3.2",
"composer/package-versions-deprecated": "1.11.99.1",
"doctrine/annotations": "^2.0",
"friendsofphp/php-cs-fixer": "^3.52", "friendsofphp/php-cs-fixer": "^3.52",
"friendsofsymfony/rest-bundle": "^2.8 || ^3.0", "friendsofsymfony/rest-bundle": "^3.2.0",
"jms/serializer": "^1.14 || ^3.0", "jms/serializer": "^3.32",
"jms/serializer-bundle": "^2.3 || ^3.0 || ^4.0 || ^5.0", "jms/serializer-bundle": "^5.5",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpstan/phpstan-phpunit": "^1.3", "phpstan/phpstan-phpunit": "^1.3",
"phpstan/phpstan-strict-rules": "^1.5", "phpstan/phpstan-strict-rules": "^1.5",
"phpstan/phpstan-symfony": "^1.3", "phpstan/phpstan-symfony": "^1.3",
"phpunit/phpunit": "^9.6 || ^10.5", "phpunit/phpunit": "^10.5",
"symfony/asset": "^5.4 || ^6.4 || ^7.1", "symfony/asset": "^6.4 || ^7.1",
"symfony/browser-kit": "^5.4 || ^6.4 || ^7.1", "symfony/browser-kit": "^6.4 || ^7.1",
"symfony/cache": "^5.4 || ^6.4 || ^7.1", "symfony/cache": "^6.4 || ^7.1",
"symfony/dom-crawler": "^5.4 || ^6.4 || ^7.1", "symfony/dom-crawler": "^6.4 || ^7.1",
"symfony/expression-language": "^5.4 || ^6.4 || ^7.1", "symfony/expression-language": "^6.4 || ^7.1",
"symfony/form": "^5.4 || ^6.4 || ^7.1", "symfony/finder": "^6.4 || ^7.1",
"symfony/phpunit-bridge": "^6.4", "symfony/form": "^6.4 || ^7.1",
"symfony/property-access": "^5.4 || ^6.4 || ^7.1", "symfony/phpunit-bridge": "^6.4 || ^7.1",
"symfony/security-csrf": "^5.4 || ^6.4 || ^7.1", "symfony/property-access": "^6.4 || ^7.1",
"symfony/serializer": "^5.4 || ^6.4 || ^7.1", "symfony/security-csrf": "^6.4 || ^7.1",
"symfony/stopwatch": "^5.4 || ^6.4 || ^7.1", "symfony/security-http": "^6.4 || ^7.1",
"symfony/templating": "^5.4 || ^6.4 || ^7.1", "symfony/serializer": "^6.4 || ^7.1",
"symfony/twig-bundle": "^5.4 || ^6.4 || ^7.1", "symfony/stopwatch": "^6.4 || ^7.1",
"symfony/uid": "^5.4 || ^6.4 || ^7.1", "symfony/templating": "^6.4 || ^7.1",
"symfony/validator": "^5.4 || ^6.4 || ^7.1", "symfony/twig-bundle": "^6.4 || ^7.1",
"willdurand/hateoas-bundle": "^1.0 || ^2.0" "symfony/uid": "^6.4 || ^7.1",
"symfony/validator": "^6.4 || ^7.1",
"willdurand/hateoas-bundle": "^2.7",
"willdurand/negotiation": "^3.0"
}, },
"suggest": { "suggest": {
"api-platform/core": "For using an API oriented framework.", "api-platform/core": "For using an API oriented framework.",
"doctrine/annotations": "For using doctrine annotations",
"friendsofsymfony/rest-bundle": "For using the parameters annotations.", "friendsofsymfony/rest-bundle": "For using the parameters annotations.",
"jms/serializer-bundle": "For describing your models.", "jms/serializer-bundle": "For describing your models.",
"symfony/asset": "For using the Swagger UI.", "symfony/asset": "For using the Swagger UI.",
@@ -3285,7 +3284,8 @@
"type": "symfony-bundle", "type": "symfony-bundle",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-4.x": "4.x-dev" "dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev"
} }
}, },
"autoload": { "autoload": {
@@ -3303,7 +3303,7 @@
"homepage": "https://github.com/nelmio/NelmioApiDocBundle/contributors" "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": [ "keywords": [
"api", "api",
"doc", "doc",
@@ -3312,7 +3312,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", "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": [ "funding": [
{ {
@@ -3320,7 +3320,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-03-24T15:00:53+00:00" "time": "2025-05-23T21:30:13+00:00"
}, },
{ {
"name": "nelmio/cors-bundle", "name": "nelmio/cors-bundle",
@@ -6438,16 +6438,16 @@
}, },
{ {
"name": "symfony/flex", "name": "symfony/flex",
"version": "v2.5.0", "version": "v2.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/flex.git", "url": "https://github.com/symfony/flex.git",
"reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018" "reference": "5d743b3b78fabe9f3146586d77b0a1f9292851fc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/flex/zipball/8ce1acd9842abe0e9b4c4a0bd3f259859516c018", "url": "https://api.github.com/repos/symfony/flex/zipball/5d743b3b78fabe9f3146586d77b0a1f9292851fc",
"reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018", "reference": "5d743b3b78fabe9f3146586d77b0a1f9292851fc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6486,7 +6486,7 @@
"description": "Composer plugin for Symfony", "description": "Composer plugin for Symfony",
"support": { "support": {
"issues": "https://github.com/symfony/flex/issues", "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": [ "funding": [
{ {
@@ -6502,7 +6502,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-03-03T07:50:46+00:00" "time": "2025-05-23T11:41:40+00:00"
}, },
{ {
"name": "symfony/form", "name": "symfony/form",
@@ -10357,16 +10357,16 @@
}, },
{ {
"name": "zircote/swagger-php", "name": "zircote/swagger-php",
"version": "5.1.1", "version": "5.1.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/zircote/swagger-php.git", "url": "https://github.com/zircote/swagger-php.git",
"reference": "7a6544c60441ddb5959b91266b3a290dc28537ba" "reference": "b8ba6bd99805c0ae09a38d1b26c1c92820509bd0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/7a6544c60441ddb5959b91266b3a290dc28537ba", "url": "https://api.github.com/repos/zircote/swagger-php/zipball/b8ba6bd99805c0ae09a38d1b26c1c92820509bd0",
"reference": "7a6544c60441ddb5959b91266b3a290dc28537ba", "reference": "b8ba6bd99805c0ae09a38d1b26c1c92820509bd0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10437,9 +10437,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/zircote/swagger-php/issues", "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": [ "packages-dev": [
@@ -11274,16 +11274,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "2.1.14", "version": "2.1.17",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2" "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2", "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -11328,25 +11328,25 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-05-02T15:32:28+00:00" "time": "2025-05-21T20:55:28+00:00"
}, },
{ {
"name": "phpstan/phpstan-deprecation-rules", "name": "phpstan/phpstan-deprecation-rules",
"version": "2.0.2", "version": "2.0.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-deprecation-rules.git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git",
"reference": "9d8e7d4e32711715ad78a1fb6ec368df9af01fdf" "reference": "468e02c9176891cc901143da118f09dc9505fc2f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/9d8e7d4e32711715ad78a1fb6ec368df9af01fdf", "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f",
"reference": "9d8e7d4e32711715ad78a1fb6ec368df9af01fdf", "reference": "468e02c9176891cc901143da118f09dc9505fc2f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.13" "phpstan/phpstan": "^2.1.15"
}, },
"require-dev": { "require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2", "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.", "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", "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", "name": "phpstan/phpstan-doctrine",
@@ -11552,22 +11552,22 @@
}, },
{ {
"name": "phpstan/phpstan-symfony", "name": "phpstan/phpstan-symfony",
"version": "2.0.4", "version": "2.0.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git", "url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "648087fb4dd865a09b1828a3b0396eb447665f2e" "reference": "5005288e07583546ea00b52de4a9ac412eb869d7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/648087fb4dd865a09b1828a3b0396eb447665f2e", "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/5005288e07583546ea00b52de4a9ac412eb869d7",
"reference": "648087fb4dd865a09b1828a3b0396eb447665f2e", "reference": "5005288e07583546ea00b52de4a9ac412eb869d7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-simplexml": "*", "ext-simplexml": "*",
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.2" "phpstan/phpstan": "^2.1.13"
}, },
"conflict": { "conflict": {
"symfony/framework-bundle": "<3.0" "symfony/framework-bundle": "<3.0"
@@ -11577,7 +11577,7 @@
"phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6", "phpunit/phpunit": "^9.6",
"psr/container": "1.0 || 1.1.1", "psr/container": "1.1.2",
"symfony/config": "^5.4 || ^6.1", "symfony/config": "^5.4 || ^6.1",
"symfony/console": "^5.4 || ^6.1", "symfony/console": "^5.4 || ^6.1",
"symfony/dependency-injection": "^5.4 || ^6.1", "symfony/dependency-injection": "^5.4 || ^6.1",
@@ -11617,9 +11617,9 @@
"description": "Symfony Framework extensions and rules for PHPStan", "description": "Symfony Framework extensions and rules for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-symfony/issues", "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", "name": "phpunit/php-code-coverage",

View File

@@ -1,4 +1,5 @@
nelmio_api_doc: nelmio_api_doc:
operation_id_generation: conditionally_prepend
models: models:
use_jms: true use_jms: true
names: names:
@@ -43,20 +44,28 @@ nelmio_api_doc:
- { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] } - { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] }
- { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] } - { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] }
areas: areas:
default:
path_patterns: path_patterns:
- ^/api(?!/doc) - ^/api(?!/doc)
security:
bearer:
type: 'http'
description: 'API Token'
bearerFormat: 'KIMAI'
scheme: 'bearer'
documentation: documentation:
info: info:
title: Kimai - API title: Kimai - API
description: | 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. 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.0' version: '1.1'
components:
securitySchemes:
bearer:
type: http
scheme: bearer
bearerFormat: KIMAI
description: API Token
security: security:
- bearer: [] - 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 }

View File

@@ -8,12 +8,7 @@ controllers:
api.swagger_ui: api.swagger_ui:
path: /api/doc path: /api/doc
methods: GET methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger_ui } defaults: { _controller: nelmio_api_doc.controller.stoplight }
api.swagger:
path: /api/doc.json
methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger }
api: api:
resource: ../src/API/ resource: ../src/API/

View File

@@ -38,8 +38,6 @@ final class ActionsController extends BaseApiController
} }
/** /**
* @param PageActionsEvent $event
* @param string $locale
* @return array<PageAction> * @return array<PageAction>
*/ */
private function convertEvent(PageActionsEvent $event, string $locale): 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\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: '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: '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\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+'])] #[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 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\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: '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: '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\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+'])] #[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 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\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: '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: '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\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+'])] #[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 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\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: '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: '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\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+'])] #[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 public function getCustomerActions(Customer $customer, string $view, string $locale): Response
{ {

View File

@@ -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')))] #[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')] #[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\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)] #[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\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'))] #[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')] #[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'))] #[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')] #[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')))] #[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')] #[IsGranted('edit', 'activity')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] #[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')] #[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'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ActivityRate'))])]

View File

@@ -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)))] #[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'])] #[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')))] #[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'])] #[Route(path: '/config/colors', methods: ['GET'])]

View File

@@ -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')))] #[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')] #[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'))] #[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+'])] #[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\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'))] #[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')] #[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'))] #[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')] #[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')))] #[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')] #[IsGranted('edit', 'customer')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] #[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')] #[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'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/CustomerRate'))])]

View File

@@ -38,9 +38,7 @@ final class InvoiceController extends BaseApiController
} }
/** /**
* Returns a paginated collection of invoices. * Fetch collection of invoices
*
* Needs permission: view_invoice
*/ */
#[IsGranted('view_invoice')] #[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')))] #[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. * Fetch invoice
*
* Needs permission: view_invoice
*/ */
#[IsGranted('view_invoice')] #[IsGranted('view_invoice')]
#[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))] #[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))]

View File

@@ -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')))] #[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')] #[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\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'))] #[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()); $form->submit($request->request->all());
if ($form->isValid()) { if ($form->isValid()) {
$this->projectService->saveNewProject($project); $this->projectService->saveProject($project);
$view = new View($project, 200); $view = new View($project, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY); $view->getContext()->setGroups(self::GROUPS_ENTITY);
@@ -232,7 +232,7 @@ final class ProjectController extends BaseApiController
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$this->projectService->updateProject($project); $this->projectService->saveProject($project);
$view = new View($project, Response::HTTP_OK); $view = new View($project, Response::HTTP_OK);
$view->getContext()->setGroups(self::GROUPS_ENTITY); $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')] #[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'))] #[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); $meta->setValue($value);
$this->projectService->updateProject($project); $this->projectService->saveProject($project);
$view = new View($project, 200); $view = new View($project, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY); $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')] #[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')))] #[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')] #[IsGranted('edit', 'project')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] #[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')] #[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'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ProjectRate'))])]

View File

@@ -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'}"))] #[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')] #[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)))] #[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')] #[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))))] #[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')] #[Route(methods: ['GET'], path: '/plugins')]

View File

@@ -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')] #[Route(methods: ['GET'], name: 'get_tags')]
#[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')] #[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')]
public function cgetAction(ParamFetcherInterface $paramFetcher): Response 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')))] #[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'])] #[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\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'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TagEditForm'))]

View File

@@ -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')] #[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')] #[Route(methods: ['GET'], path: '', name: 'get_teams')]
public function cgetAction(): Response public function cgetAction(): Response
{ {
@@ -66,7 +66,7 @@ final class TeamController extends BaseApiController
} }
/** /**
* Returns one team * Fetch team
*/ */
#[IsGranted('view_team')] #[IsGranted('view_team')]
#[OA\Response(response: 200, description: 'Returns one team entity', content: new OA\JsonContent(ref: '#/components/schemas/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')] #[IsGranted('delete_team')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one 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')] #[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'))])] #[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')] #[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'))])] #[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')] #[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'))])] #[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')] #[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'))])] #[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')] #[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'))])] #[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')] #[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'))])] #[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')] #[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'))])] #[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')] #[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'))])] #[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')] #[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'))])] #[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')] #[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'))])] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a activity from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]

View File

@@ -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')"))] #[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')] #[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: '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)')] #[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')] #[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\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 record ID to fetch', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to fetch', required: true)]
#[Route(methods: ['GET'], path: '/{id}', name: 'get_timesheet', requirements: ['id' => '\d+'])] #[Route(methods: ['GET'], path: '/{id}', name: 'get_timesheet', requirements: ['id' => '\d+'])]
public function getAction(Timesheet $timesheet): Response 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')] #[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'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
#[Route(methods: ['POST'], path: '', name: 'post_timesheet')] #[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)')] #[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')] #[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\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 record ID to update', required: true)] #[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'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_timesheet', requirements: ['id' => '\d+'])] #[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_timesheet', requirements: ['id' => '\d+'])]
public function patchAction(Request $request, Timesheet $timesheet): Response 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')] #[IsGranted('delete', 'timesheet')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet record')])] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to delete', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to delete', required: true)]
#[Route(methods: ['DELETE'], path: '/{id}', name: 'delete_timesheet', requirements: ['id' => '\d+'])] #[Route(methods: ['DELETE'], path: '/{id}', name: 'delete_timesheet', requirements: ['id' => '\d+'])]
public function deleteAction(Timesheet $timesheet): Response 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')] #[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')] #[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: '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)')] #[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')] #[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')] #[Route(methods: ['GET'], path: '/active', name: 'active_timesheet')]
public function activeAction(): Response 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. * 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. * Likely caused by a slow JS engine and a fast-click after page reload.
*/ */
#[IsGranted('stop', 'timesheet')] #[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\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 record ID to stop', required: true)] #[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: ['GET'], path: '/{id}/stop', name: 'stop_timesheet_get', requirements: ['id' => '\d+'])]
#[Route(methods: ['PATCH'], path: '/{id}/stop', name: 'stop_timesheet', 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 public function stopAction(Timesheet $timesheet): Response
{ {
$this->service->stopTimesheet($timesheet); $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')] #[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\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 record ID to restart', required: true)] #[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: ['GET'], path: '/{id}/restart', name: 'restart_timesheet_get', requirements: ['id' => '\d+'])]
#[Route(methods: ['PATCH'], path: '/{id}/restart', name: 'restart_timesheet', 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)')] #[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')] #[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\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 record ID to duplicate', required: true)] #[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+'])] #[Route(methods: ['PATCH'], path: '/{id}/duplicate', name: 'duplicate_timesheet', requirements: ['id' => '\d+'])]
public function duplicateAction(Timesheet $timesheet): Response 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')] #[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\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+'])] #[Route(methods: ['PATCH'], path: '/{id}/export', name: 'export_timesheet', requirements: ['id' => '\d+'])]
public function exportAction(Timesheet $timesheet): Response 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')] #[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\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+'])] #[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: 'name', strict: true, nullable: false, description: 'The meta-field name')]
#[Rest\RequestParam(name: 'value', strict: true, nullable: false, description: 'The meta-field value')] #[Rest\RequestParam(name: 'value', strict: true, nullable: false, description: 'The meta-field value')]

View File

@@ -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')] #[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')] #[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: '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)')] #[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')] #[IsGranted('create_user')]
#[OA\Post(description: 'Creates a new user and returns it afterwards')] #[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\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)] #[OA\Parameter(name: 'id', in: 'path', description: 'The API token ID to remove', required: true)]

View File

@@ -55,6 +55,18 @@ class ActivityService
return $activity; 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 public function saveNewActivity(Activity $activity): Activity
{ {
if (null !== $activity->getId()) { if (null !== $activity->getId()) {
@@ -89,6 +101,9 @@ class ActivityService
} }
} }
/**
* @deprecated since 2.35 - use saveActivity() instead
*/
public function updateActivity(Activity $activity): Activity public function updateActivity(Activity $activity): Activity
{ {
$this->validateActivity($activity); $this->validateActivity($activity);

View File

@@ -14,6 +14,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\FileNotFoundException; use Symfony\Component\Filesystem\Exception\FileNotFoundException;
@@ -25,8 +26,6 @@ abstract class AbstractBundleInstallerCommand extends Command
{ {
/** /**
* Returns the base directory to the Kimai installation. * Returns the base directory to the Kimai installation.
*
* @return string
*/ */
protected function getRootDirectory(): 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, * If your bundle ships assets, that need to be available in the public/ directory,
* then overwrite this method and return: <true>. * then overwrite this method and return: <true>.
*
* @return bool
*/ */
protected function hasAssets(): bool protected function hasAssets(): bool
{ {
return false; 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. * Returns an absolute filename to your doctrine migrations configuration
* * if you want to run database migrations.
* @return string|null
*/ */
protected function getMigrationConfigFilename(): ?string protected function getMigrationConfigFilename(): ?string
{ {
@@ -59,16 +66,12 @@ abstract class AbstractBundleInstallerCommand extends Command
/** /**
* Returns the bundle short name for the installer command. * Returns the bundle short name for the installer command.
*
* @return string
*/ */
abstract protected function getBundleCommandNamePart(): string; abstract protected function getBundleCommandNamePart(): string;
/** /**
* Returns the full name fo this command. * Returns the full name fo this command.
* Please stick to the standard and overwrite getBundleCommandNamePart() only. * Please stick to the standard and overwrite getBundleCommandNamePart() only.
*
* @return string
*/ */
protected function getInstallerCommandName(): string protected function getInstallerCommandName(): string
{ {
@@ -77,8 +80,6 @@ abstract class AbstractBundleInstallerCommand extends Command
/** /**
* Returns the bundles real name (same as your namespace). * Returns the bundles real name (same as your namespace).
*
* @return string
*/ */
protected function getBundleName(): string protected function getBundleName(): string
{ {
@@ -100,6 +101,8 @@ abstract class AbstractBundleInstallerCommand extends Command
->setName($this->getInstallerCommandName()) ->setName($this->getInstallerCommandName())
->setDescription('Install the bundle: ' . $this->getBundleName()) ->setDescription('Install the bundle: ' . $this->getBundleName())
->setHelp('This command will perform the basic installation steps to get the bundle up and running.') ->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); $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 // many users execute the bin/console command from arbitrary locations
// this will make sure that relative paths (like doctrine migrations) work as expected // this will make sure that relative paths (like doctrine migrations) work as expected
$path = getcwd(); $path = getcwd();
@@ -117,6 +129,7 @@ abstract class AbstractBundleInstallerCommand extends Command
\sprintf('Starting installation of plugin: %s ...', $bundleName) \sprintf('Starting installation of plugin: %s ...', $bundleName)
); );
if (!$onlyAssets && $this->hasMigrations($io)) {
try { try {
$this->importMigrations($io, $output); $this->importMigrations($io, $output);
} catch (\Exception $ex) { } catch (\Exception $ex) {
@@ -126,8 +139,9 @@ abstract class AbstractBundleInstallerCommand extends Command
return Command::FAILURE; return Command::FAILURE;
} }
}
if ($this->hasAssets()) { if (!$noAssets && $this->hasAssets()) {
try { try {
$this->installAssets($io, $output); $this->installAssets($io, $output);
} catch (\Exception $ex) { } catch (\Exception $ex) {
@@ -148,7 +162,7 @@ abstract class AbstractBundleInstallerCommand extends Command
return Command::SUCCESS; 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'); $command = $this->getApplication()->find('assets:install');
$cmdInput = new ArrayInput([]); $cmdInput = new ArrayInput([]);
@@ -160,11 +174,11 @@ abstract class AbstractBundleInstallerCommand extends Command
$io->writeln(''); $io->writeln('');
} }
protected function importMigrations(SymfonyStyle $io, OutputInterface $output): void private function importMigrations(SymfonyStyle $io, OutputInterface $output): void
{ {
$config = $this->getMigrationConfigFilename(); $config = $this->getMigrationConfigFilename();
if (null === $config) { if ($config === null) {
return; return;
} }

View File

@@ -25,7 +25,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
* *
* @codeCoverageIgnore * @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 final class InstallCommand extends Command
{ {
public function __construct(private readonly Connection $connection) public function __construct(private readonly Connection $connection)

View File

@@ -22,10 +22,13 @@ use Symfony\Component\Console\Style\SymfonyStyle;
* *
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
#[AsCommand(name: 'kimai:reload')] #[AsCommand(name: 'kimai:reload', description: 'Reload Kimai caches')]
final class ReloadCommand extends Command 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(); parent::__construct();
} }
@@ -42,10 +45,7 @@ final class ReloadCommand extends Command
protected function configure(): void protected function configure(): void
{ {
$this $this->setHelp('This will validate configurations and translations and then clear and rebuild the application cache.');
->setDescription('Reload Kimai caches')
->setHelp('This command will validate the configurations and translations and then clear and rebuild the application cache.')
;
} }
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int

View File

@@ -17,11 +17,11 @@ final class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '2.34.0'; public const VERSION = '2.35.0';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 23400; public const VERSION_ID = 23500;
/** /**
* The software name * The software name
*/ */

View File

@@ -271,7 +271,7 @@ final class ActivityController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) { if ($editForm->isSubmitted() && $editForm->isValid()) {
try { try {
$this->activityService->saveNewActivity($activity); $this->activityService->saveActivity($activity);
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');
return $this->redirectToRouteAfterCreate('activity_details', ['id' => $activity->getId()]); return $this->redirectToRouteAfterCreate('activity_details', ['id' => $activity->getId()]);
@@ -300,7 +300,7 @@ final class ActivityController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
try { try {
$this->activityService->updateActivity($activity); $this->activityService->saveActivity($activity);
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');
if ($this->isGranted('view', $activity)) { if ($this->isGranted('view', $activity)) {
@@ -355,7 +355,7 @@ final class ActivityController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) { if ($editForm->isSubmitted() && $editForm->isValid()) {
try { try {
$this->activityService->updateActivity($activity); $this->activityService->saveActivity($activity);
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');
if ($this->isGranted('view', $activity)) { if ($this->isGranted('view', $activity)) {

View File

@@ -154,7 +154,7 @@ final class ProjectController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
try { try {
$this->projectService->updateProject($project); $this->projectService->saveProject($project);
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');
if ($this->isGranted('view', $project)) { if ($this->isGranted('view', $project)) {
@@ -197,7 +197,7 @@ final class ProjectController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) { if ($editForm->isSubmitted() && $editForm->isValid()) {
try { try {
$this->projectService->saveNewProject($project, new Context($this->getUser())); $this->projectService->saveProject($project, new Context($this->getUser()));
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');
return $this->redirectToRouteAfterCreate('project_details', ['id' => $project->getId()]); return $this->redirectToRouteAfterCreate('project_details', ['id' => $project->getId()]);
@@ -454,7 +454,7 @@ final class ProjectController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) { if ($editForm->isSubmitted() && $editForm->isValid()) {
try { try {
$this->projectService->updateProject($project); $this->projectService->saveProject($project);
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');
if ($this->isGranted('view', $project)) { if ($this->isGranted('view', $project)) {

View File

@@ -59,6 +59,18 @@ final class CustomerService
return $customer; 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 public function saveNewCustomer(Customer $customer): Customer
{ {
if (null !== $customer->getId()) { if (null !== $customer->getId()) {
@@ -93,6 +105,9 @@ final class CustomerService
} }
} }
/**
* @deprecated since 2.35 - use saveCustomer() instead
*/
public function updateCustomer(Customer $customer): Customer public function updateCustomer(Customer $customer): Customer
{ {
$this->validateCustomer($customer); $this->validateCustomer($customer);

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\AccessTokenRepository; use App\Repository\AccessTokenRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity(fields: ['token'])] #[UniqueEntity(fields: ['token'])]
class AccessToken class AccessToken
{ {
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null; private ?int $id = null;
@@ -29,17 +30,17 @@ class AccessToken
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull] #[Assert\NotNull]
private User $user; 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\NotBlank]
#[Assert\Length(min: 2, max: 100)] #[Assert\Length(min: 2, max: 100)]
private string $token; 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\NotBlank]
#[Assert\Length(min: 2, max: 50)] #[Assert\Length(min: 2, max: 50)]
private ?string $name = null; 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; 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; private ?\DateTimeImmutable $expiresAt = null;
public function __construct(User $user, string $token) public function __construct(User $user, string $token)

View File

@@ -16,6 +16,7 @@ use App\Repository\ActivityRepository;
use App\Validator\Constraints as Constraints; use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -42,7 +43,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Unique activity ID * Unique activity ID
*/ */
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose] #[Serializer\Expose]
@@ -58,7 +59,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Name of this activity * 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\NotBlank]
#[Assert\Length(min: 2, max: 150)] #[Assert\Length(min: 2, max: 150)]
#[Serializer\Expose] #[Serializer\Expose]
@@ -68,7 +69,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Description of this activity * Description of this activity
*/ */
#[ORM\Column(name: 'comment', type: 'text', nullable: true)] #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'comment')] #[Exporter\Expose(label: 'comment')]
@@ -76,13 +77,13 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Whether this activity is visible and can be selected * 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'visible', type: 'boolean')] #[Exporter\Expose(label: 'visible', type: 'boolean')]
private bool $visible = true; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
@@ -113,9 +114,9 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Activity'])] #[Serializer\Groups(['Activity'])]
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))] #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
private Collection $teams; 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; 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)] #[Assert\Length(max: 10)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
@@ -134,6 +135,11 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
return $this->id; return $this->id;
} }
public function isNew(): bool
{
return $this->id === null;
}
public function getProject(): ?Project public function getProject(): ?Project
{ {
return $this->project; return $this->project;

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\BookmarkRepository; use App\Repository\BookmarkRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -25,7 +26,7 @@ class Bookmark
public const COLUMN_VISIBILITY = 'columns'; public const COLUMN_VISIBILITY = 'columns';
public const TIMESHEET = 'timesheet'; public const TIMESHEET = 'timesheet';
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null; private ?int $id = null;
@@ -33,15 +34,15 @@ class Bookmark
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull] #[Assert\NotNull]
private ?User $user = null; 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\NotBlank]
#[Assert\Length(min: 2, max: 20)] #[Assert\Length(min: 2, max: 20)]
private ?string $type = null; 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\NotBlank]
#[Assert\Length(min: 2, max: 50)] #[Assert\Length(min: 2, max: 50)]
private ?string $name = null; 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; private ?string $content = null;
public function getId(): ?int public function getId(): ?int

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Export\Annotation as Exporter; use App\Export\Annotation as Exporter;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -19,7 +20,7 @@ trait BudgetTrait
/** /**
* The total monetary budget, will be zero if not configured. * 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\Range(min: 0.00, max: 900000000000.00)]
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
@@ -29,7 +30,7 @@ trait BudgetTrait
/** /**
* The time budget in seconds, will be zero if not configured. * 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\Range(min: 0, max: 2145600000)]
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
@@ -41,7 +42,7 @@ trait BudgetTrait
* - null = default / full time * - null = default / full time
* - month = monthly budget * - 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\Expose]
#[Serializer\Groups(['Activity_Entity', 'Project_Entity', 'Customer_Entity'])] #[Serializer\Groups(['Activity_Entity', 'Project_Entity', 'Customer_Entity'])]
#[Exporter\Expose(label: 'budgetType')] #[Exporter\Expose(label: 'budgetType')]

View File

@@ -12,6 +12,7 @@ namespace App\Entity;
use App\Constants; use App\Constants;
use App\Export\Annotation as Exporter; use App\Export\Annotation as Exporter;
use App\Validator\Constraints as Constraints; use App\Validator\Constraints as Constraints;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
@@ -20,7 +21,7 @@ trait ColorTrait
/** /**
* The assigned color in HTML hex format, e.g. #dd1d00 * 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\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'color')] #[Exporter\Expose(label: 'color')]

View File

@@ -9,6 +9,7 @@
namespace App\Entity; namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -16,19 +17,19 @@ trait CommentTableTypeTrait
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(name: 'message', type: 'text', nullable: false)] #[ORM\Column(name: 'message', type: Types::TEXT, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
private ?string $message = null; private ?string $message = null;
#[ORM\ManyToOne(targetEntity: User::class)] #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull] #[Assert\NotNull]
private ?User $createdBy = null; 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] #[Assert\NotNull]
private ?\DateTime $createdAt = null; 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] #[Assert\NotNull]
private bool $pinned = false; private bool $pinned = false;

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\ConfigurationRepository; use App\Repository\ConfigurationRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -23,13 +24,13 @@ class Configuration
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
private ?int $id = null; 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\NotNull]
#[Assert\Length(min: 2, max: 100)] #[Assert\Length(min: 2, max: 100)]
private ?string $name = null; 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)] #[Assert\Length(max: 65535)]
private ?string $value = null; private ?string $value = null;

View File

@@ -16,6 +16,7 @@ use App\Repository\CustomerRepository;
use App\Validator\Constraints as Constraints; use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -36,50 +37,50 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
use ColorTrait; use ColorTrait;
use CreatedTrait; use CreatedTrait;
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'id', type: 'integer')] #[Exporter\Expose(label: 'id', type: 'integer')]
private ?int $id = null; 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\NotBlank]
#[Assert\Length(min: 2, max: 150)] #[Assert\Length(min: 2, max: 150)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'name')] #[Exporter\Expose(label: 'name')]
private ?string $name = null; 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)] #[Assert\Length(max: 50)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'number')] #[Exporter\Expose(label: 'number')]
private ?string $number = null; private ?string $number = null;
#[ORM\Column(name: 'comment', type: 'text', nullable: true)] #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'comment')] #[Exporter\Expose(label: 'comment')]
private ?string $comment = null; private ?string $comment = null;
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false)] #[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'visible', type: 'boolean')] #[Exporter\Expose(label: 'visible', type: 'boolean')]
private bool $visible = true; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'billable', type: 'boolean')] #[Exporter\Expose(label: 'billable', type: 'boolean')]
private bool $billable = true; 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)] #[Assert\Length(max: 100)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'company')] #[Exporter\Expose(label: 'company')]
private ?string $company = null; 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)] #[Assert\Length(max: 50)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
@@ -96,7 +97,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'address')] #[Exporter\Expose(label: 'address')]
private ?string $address = null; 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\NotBlank]
#[Assert\Country] #[Assert\Country]
#[Assert\Length(max: 2)] #[Assert\Length(max: 2)]
@@ -104,7 +105,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'country')] #[Exporter\Expose(label: 'country')]
private ?string $country = null; 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\NotBlank]
#[Assert\Currency] #[Assert\Currency]
#[Assert\Length(max: 3)] #[Assert\Length(max: 3)]
@@ -112,19 +113,19 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Customer'])] #[Serializer\Groups(['Customer'])]
#[Exporter\Expose(label: 'currency')] #[Exporter\Expose(label: 'currency')]
private ?string $currency = self::DEFAULT_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)] #[Assert\Length(max: 30)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'phone')] #[Exporter\Expose(label: 'phone')]
private ?string $phone = null; 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)] #[Assert\Length(max: 30)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'fax')] #[Exporter\Expose(label: 'fax')]
private ?string $fax = null; 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)] #[Assert\Length(max: 30)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
@@ -133,13 +134,13 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Customers contact email * 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)] #[Assert\Length(max: 75)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'email')] #[Exporter\Expose(label: 'email')]
private ?string $email = null; 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)] #[Assert\Length(max: 100)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])] #[Serializer\Groups(['Customer_Entity'])]
@@ -148,7 +149,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Timezone of begin and end * 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\NotBlank]
#[Assert\Timezone] #[Assert\Timezone]
#[Assert\Length(max: 64)] #[Assert\Length(max: 64)]
@@ -187,7 +188,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)] #[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?InvoiceTemplate $invoiceTemplate = 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; private ?string $invoiceText = null;
public function __construct(string $name) public function __construct(string $name)
@@ -203,6 +204,11 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
return $this->id; return $this->id;
} }
public function isNew(): bool
{
return $this->id === null;
}
public function setName(?string $name): void public function setName(?string $name): void
{ {
$this->name = $name; $this->name = $name;

View File

@@ -14,6 +14,7 @@ use App\Invoice\InvoiceModel;
use App\Repository\InvoiceRepository; use App\Repository\InvoiceRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -44,20 +45,20 @@ class Invoice implements EntityWithMetaFields
/** /**
* Unique invoice ID * Unique invoice ID
*/ */
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'id', type: 'integer')] #[Exporter\Expose(label: 'id', type: 'integer')]
private ?int $id = null; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'invoice.number', type: 'string')] #[Exporter\Expose(label: 'invoice.number', type: 'string')]
private ?string $invoiceNumber = null; private ?string $invoiceNumber = null;
#[ORM\Column(name: 'comment', type: 'text', nullable: true)] #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Invoice'])] #[Serializer\Groups(['Invoice'])]
#[Exporter\Expose(label: 'comment')] #[Exporter\Expose(label: 'comment')]
@@ -76,53 +77,53 @@ class Invoice implements EntityWithMetaFields
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[OA\Property(ref: '#/components/schemas/User')] #[OA\Property(ref: '#/components/schemas/User')]
private ?User $user = null; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private ?\DateTime $createdAt = null; 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; private ?string $timezone = null;
#[ORM\Column(name: 'total', type: 'float', nullable: false)] #[ORM\Column(name: 'total', type: Types::FLOAT, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'total_rate', type: 'float')] #[Exporter\Expose(label: 'total_rate', type: 'float')]
private float $total = 0.00; private float $total = 0.00;
#[ORM\Column(name: 'tax', type: 'float', nullable: false)] #[ORM\Column(name: 'tax', type: Types::FLOAT, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'invoice.tax', type: 'float')] #[Exporter\Expose(label: 'invoice.tax', type: 'float')]
private float $tax = 0.00; 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\NotNull]
#[Assert\Length(max: 3)] #[Assert\Length(max: 3)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'currency', type: 'string')] #[Exporter\Expose(label: 'currency', type: 'string')]
private ?string $currency = null; 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\NotNull]
#[Assert\Range(min: 0, max: 999)] #[Assert\Range(min: 0, max: 999)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Invoice'])] #[Serializer\Groups(['Invoice'])]
#[Exporter\Expose(label: 'due_days', type: 'integer')] #[Exporter\Expose(label: 'due_days', type: 'integer')]
private int $dueDays = 30; private int $dueDays = 30;
#[ORM\Column(name: 'vat', type: 'float', nullable: false)] #[ORM\Column(name: 'vat', type: Types::FLOAT, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
#[Assert\Range(min: 0.0, max: 99.99)] #[Assert\Range(min: 0.0, max: 99.99)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'tax_rate', type: 'float')] #[Exporter\Expose(label: 'tax_rate', type: 'float')]
private float $vat = 0.00; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'status', type: 'string')] #[Exporter\Expose(label: 'status', type: 'string')]
private string $status = self::STATUS_NEW; 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\NotNull]
#[Assert\Length(min: 1, max: 150)] #[Assert\Length(min: 1, max: 150)]
#[Exporter\Expose(label: 'file', type: 'string')] #[Exporter\Expose(label: 'file', type: 'string')]

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\InvoiceTemplateRepository; use App\Repository\InvoiceTemplateRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -21,15 +22,15 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity('name')] #[UniqueEntity('name')]
class InvoiceTemplate class InvoiceTemplate
{ {
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null; 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\NotBlank]
#[Assert\Length(min: 1, max: 60)] #[Assert\Length(min: 1, max: 60)]
private ?string $name = null; 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\Length(max: 255)]
#[Assert\NotBlank] #[Assert\NotBlank]
private ?string $title = null; private ?string $title = null;
@@ -37,41 +38,41 @@ class InvoiceTemplate
#[Assert\Length(max: 255)] #[Assert\Length(max: 255)]
#[Assert\NotBlank] #[Assert\NotBlank]
private ?string $company = null; 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)] #[Assert\Length(max: 50)]
private ?string $vatId = null; 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; 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; 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\NotNull]
#[Assert\Range(min: 0, max: 999)] #[Assert\Range(min: 0, max: 999)]
private ?int $dueDays = 30; private ?int $dueDays = 30;
#[ORM\Column(name: 'vat', type: 'float', nullable: false)] #[ORM\Column(name: 'vat', type: Types::FLOAT, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
#[Assert\Range(min: 0.0, max: 99.99)] #[Assert\Range(min: 0.0, max: 99.99)]
private ?float $vat = 0.00; 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\NotBlank]
#[Assert\Length(max: 20)] #[Assert\Length(max: 20)]
private string $calculator = 'default'; 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\NotBlank]
#[Assert\Length(max: 20)] #[Assert\Length(max: 20)]
private string $numberGenerator = 'default'; 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\NotBlank]
#[Assert\Length(max: 20)] #[Assert\Length(max: 20)]
private string $renderer = 'default'; 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; 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; private ?string $paymentDetails = null;
/** /**
* Used for translations and formatting money, numbers, dates and time. * 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] #[Assert\NotBlank]
private ?string $language = 'en'; private ?string $language = 'en';

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Form\Type\YesNoType; use App\Form\Type\YesNoType;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -22,13 +23,13 @@ trait MetaTableTypeTrait
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[Serializer\Exclude] #[Serializer\Exclude]
private ?int $id = null; private ?int $id = null;
/** /**
* Name of the meta (custom) field * 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\NotNull]
#[Assert\Length(min: 2, max: 50)] #[Assert\Length(min: 2, max: 50)]
#[Serializer\Expose] #[Serializer\Expose]
@@ -37,12 +38,12 @@ trait MetaTableTypeTrait
/** /**
* Value of the meta (custom) field * 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)] #[Assert\Length(max: 65535)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private ?string $value = null; 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] #[Assert\NotNull]
private bool $visible = false; private bool $visible = false;
private ?string $label = null; private ?string $label = null;

View File

@@ -16,6 +16,7 @@ use App\Repository\ProjectRepository;
use App\Validator\Constraints as Constraints; use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -41,7 +42,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Unique Project ID * Unique Project ID
*/ */
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose] #[Serializer\Expose]
@@ -61,7 +62,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Project name * 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\NotNull]
#[Assert\Length(min: 2, max: 150)] #[Assert\Length(min: 2, max: 150)]
#[Serializer\Expose] #[Serializer\Expose]
@@ -71,7 +72,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* Project order number * 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)] #[Assert\Length(max: 50)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Project_Entity'])] #[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. * 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\Expose]
#[Serializer\Groups(['Project_Entity'])] #[Serializer\Groups(['Project_Entity'])]
#[Serializer\Type(name: "DateTime<'Y-m-d'>")] #[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. * 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\Expose]
#[Serializer\Groups(['Project'])] #[Serializer\Groups(['Project'])]
#[Serializer\Type(name: "DateTime<'Y-m-d'>")] #[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. * 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\Expose]
#[Serializer\Groups(['Project'])] #[Serializer\Groups(['Project'])]
#[Serializer\Type(name: "DateTime<'Y-m-d'>")] #[Serializer\Type(name: "DateTime<'Y-m-d'>")]
#[Serializer\Accessor(getter: 'getEnd')] #[Serializer\Accessor(getter: 'getEnd')]
private ?\DateTime $end = null; 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 ?string $timezone = null;
private bool $localized = false; private bool $localized = false;
#[ORM\Column(name: 'comment', type: 'text', nullable: true)] #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'comment')] #[Exporter\Expose(label: 'comment')]
@@ -121,13 +122,13 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/** /**
* If the project is not visible, times cannot be recorded * 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'visible', type: 'boolean')] #[Exporter\Expose(label: 'visible', type: 'boolean')]
private bool $visible = true; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
@@ -158,17 +159,17 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Project'])] #[Serializer\Groups(['Project'])]
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))] #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
private Collection $teams; 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; private ?string $invoiceText = null;
/** /**
* Whether this project allows booking of global activities * 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private bool $globalActivities = true; 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)] #[Assert\Length(max: 10)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
@@ -187,6 +188,11 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
return $this->id; return $this->id;
} }
public function isNew(): bool
{
return $this->id === null;
}
public function getCustomer(): ?Customer public function getCustomer(): ?Customer
{ {
return $this->customer; return $this->customer;

View File

@@ -9,6 +9,7 @@
namespace App\Entity; namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -16,7 +17,7 @@ use Symfony\Component\Validator\Constraints as Assert;
trait Rate trait Rate
{ {
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose] #[Serializer\Expose]
@@ -28,16 +29,16 @@ trait Rate
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[OA\Property(ref: '#/components/schemas/User')] #[OA\Property(ref: '#/components/schemas/User')]
private ?User $user = null; private ?User $user = null;
#[ORM\Column(name: 'rate', type: 'float', nullable: false)] #[ORM\Column(name: 'rate', type: Types::FLOAT, nullable: false)]
#[Assert\GreaterThanOrEqual(0)] #[Assert\GreaterThanOrEqual(0)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private float $rate = 0.00; 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\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private ?float $internalRate = null; private ?float $internalRate = null;
#[ORM\Column(name: 'fixed', type: 'boolean', nullable: false)] #[ORM\Column(name: 'fixed', type: Types::BOOLEAN, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\RoleRepository; use App\Repository\RoleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -21,11 +22,11 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity('name')] #[UniqueEntity('name')]
class Role class Role
{ {
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null; 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\NotNull]
#[Assert\NotBlank] #[Assert\NotBlank]
#[Assert\Length(min: 5, max: 50)] #[Assert\Length(min: 5, max: 50)]

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\RolePermissionRepository; use App\Repository\RolePermissionRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity(['role', 'permission'])] #[UniqueEntity(['role', 'permission'])]
class RolePermission class RolePermission
{ {
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null; private ?int $id = null;
@@ -29,10 +30,10 @@ class RolePermission
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull] #[Assert\NotNull]
private ?Role $role = null; 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)] #[Assert\Length(max: 50)]
private ?string $permission = null; 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] #[Assert\NotNull]
private bool $allowed = false; private bool $allowed = false;

View File

@@ -11,6 +11,7 @@ namespace App\Entity;
use App\Repository\TagRepository; use App\Repository\TagRepository;
use App\Utils\Color; use App\Utils\Color;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
@@ -28,7 +29,7 @@ class Tag
/** /**
* Internal Tag ID * Internal Tag ID
*/ */
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose] #[Serializer\Expose]
@@ -37,14 +38,14 @@ class Tag
/** /**
* The tag name * 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\NotBlank]
#[Assert\Length(min: 2, max: 100, normalizer: 'trim')] #[Assert\Length(min: 2, max: 100, normalizer: 'trim')]
#[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)] #[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private ?string $name = null; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]

View File

@@ -13,6 +13,7 @@ use App\Repository\TeamRepository;
use App\Validator\Constraints as Constraints; use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -28,7 +29,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[Constraints\Team] #[Constraints\Team]
class Team class Team
{ {
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose] #[Serializer\Expose]
@@ -37,7 +38,7 @@ class Team
/** /**
* Team name * 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\NotBlank]
#[Assert\Length(min: 2, max: 100)] #[Assert\Length(min: 2, max: 100)]
#[Serializer\Expose] #[Serializer\Expose]

View File

@@ -9,6 +9,7 @@
namespace App\Entity; namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[Serializer\ExclusionPolicy('all')] #[Serializer\ExclusionPolicy('all')]
class TeamMember class TeamMember
{ {
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null; private ?int $id = null;
@@ -39,7 +40,7 @@ class TeamMember
#[Serializer\Groups(['Default', 'Entity', 'User_Entity'])] #[Serializer\Groups(['Default', 'Entity', 'User_Entity'])]
#[OA\Property(ref: '#/components/schemas/Team')] #[OA\Property(ref: '#/components/schemas/Team')]
private ?Team $team = null; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default', 'Entity', 'Team_Entity', 'User_Entity'])] #[Serializer\Groups(['Default', 'Entity', 'Team_Entity', 'User_Entity'])]

View File

@@ -17,6 +17,7 @@ use DateTime;
use DateTimeZone; use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -81,7 +82,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
/** /**
* Unique Timesheet ID * Unique Timesheet ID
*/ */
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose] #[Serializer\Expose]
@@ -91,7 +92,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
* Reflects the date in the user timezone (not in UTC). * 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. * 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] #[Assert\NotNull]
private ?\DateTimeImmutable $date = null; 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. * 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[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. * 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\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Serializer\Type(name: 'DateTime')] #[Serializer\Type(name: 'DateTime')]
@@ -120,18 +121,18 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
/** /**
* @internal for storing the timezone of "begin" and "end" date * @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] #[Assert\Timezone]
private ?string $timezone = null; private ?string $timezone = null;
/** /**
* @internal for storing the localized state of dates (see $timezone) * @internal for storing the localized state of dates (see $timezone)
*/ */
private bool $localized = false; private bool $localized = false;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)] #[ORM\Column(name: 'duration', type: Types::INTEGER, nullable: true)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private ?int $duration = 0; 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; private ?int $break = 0;
#[ORM\ManyToOne(targetEntity: User::class)] #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
@@ -154,36 +155,36 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
#[Serializer\Groups(['Subresource', 'Expanded'])] #[Serializer\Groups(['Subresource', 'Expanded'])]
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')] #[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
private ?Project $project = null; private ?Project $project = null;
#[ORM\Column(name: 'description', type: 'text', nullable: true)] #[ORM\Column(name: 'description', type: Types::TEXT, nullable: true)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private ?string $description = null; 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\GreaterThanOrEqual(0)]
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private float $rate = 0.00; 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\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private ?float $internalRate = null; 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)] #[Assert\GreaterThanOrEqual(0)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Entity'])] #[Serializer\Groups(['Entity'])]
private ?float $fixedRate = null; 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)] #[Assert\GreaterThanOrEqual(0)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Entity'])] #[Serializer\Groups(['Entity'])]
private ?float $hourlyRate = null; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private bool $exported = false; 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] #[Assert\NotNull]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
@@ -193,7 +194,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
*/ */
#[Assert\NotNull] #[Assert\NotNull]
private ?string $billableMode = self::BILLABLE_DEFAULT; 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] #[Assert\NotNull]
private ?string $category = self::WORK; private ?string $category = self::WORK;
/** /**

View File

@@ -17,6 +17,7 @@ use App\WorkingTime\Mode\WorkingTimeModeNone;
use DateTime; use DateTime;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Exception; use Exception;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
@@ -70,7 +71,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
*/ */
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'id', type: 'integer')] #[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 * 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)] #[Assert\Length(max: 60)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
@@ -87,13 +88,13 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
/** /**
* Registration date for the user * 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')] #[Exporter\Expose(label: 'profile.registration_date', type: 'datetime')]
private ?\DateTime $registeredAt = null; private ?\DateTime $registeredAt = null;
/** /**
* An additional title for the user, like the Job position or Department * 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)] #[Assert\Length(max: 50)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[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 * 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'])] #[Assert\Length(max: 255, groups: ['Profile'])]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['User_Entity'])] #[Serializer\Groups(['User_Entity'])]
@@ -110,7 +111,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
/** /**
* API token (password) for this user * 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; private ?string $apiToken = null;
/** /**
* @internal to be set via form, must not be persisted * @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 * @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)] #[Assert\Length(max: 20)]
private ?string $auth = self::AUTH_INTERNAL; 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 * @internal has no database mapping as the value is calculated from a permission
*/ */
private ?bool $isAllowedToSeeAllData = null; 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\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Regex(pattern: '/\//', match: false, groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Regex(pattern: '/\//', match: false, groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Length(min: 2, max: 64, groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Length(min: 2, max: 64, groups: ['Registration', 'UserCreate', 'Profile'])]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private ?string $username = null; 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\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Length(min: 2, max: 180)] #[Assert\Length(min: 2, max: 180)]
#[Assert\Email(mode: 'html5', groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Email(mode: 'html5', groups: ['Registration', 'UserCreate', 'Profile'])]
private ?string $email = null; 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)] #[Assert\Length(max: 30)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'account_number')] #[Exporter\Expose(label: 'account_number')]
private ?string $accountNumber = null; private ?string $accountNumber = null;
#[ORM\Column(name: 'enabled', type: 'boolean', nullable: false)] #[ORM\Column(name: 'enabled', type: Types::BOOLEAN, nullable: false)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private bool $enabled = false; private bool $enabled = false;
/** /**
* Encrypted password. Must be persisted. * 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; private ?string $password = null;
/** /**
* Plain password. Used for model validation, not persisted. * 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\NotBlank(groups: ['Registration', 'PasswordUpdate', 'UserCreate'])]
#[Assert\Length(min: 8, max: 60, groups: ['Registration', 'PasswordUpdate', 'UserCreate', 'ResetPassword', 'ChangePassword'])] #[Assert\Length(min: 8, max: 60, groups: ['Registration', 'PasswordUpdate', 'UserCreate', 'ResetPassword', 'ChangePassword'])]
private ?string $plainPassword = null; 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; private ?DateTime $lastLogin = null;
/** /**
* Random string sent to the user email address in order to verify it. * 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)] #[Assert\Length(max: 180)]
private ?string $confirmationToken = null; 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; private ?\DateTimeImmutable $passwordRequestedAt = null;
/** /**
* List of all role names * 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\Expose]
#[Serializer\Groups(['User_Entity'])] #[Serializer\Groups(['User_Entity'])]
#[Serializer\Type('array<string>')] #[Serializer\Type('array<string>')]
@@ -213,11 +214,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
* If not empty two-factor authentication is enabled. * 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) * 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; 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; 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; private bool $systemAccount = false;
#[ORM\ManyToOne(targetEntity: User::class)] #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]

View File

@@ -11,6 +11,7 @@ namespace App\Entity;
use App\Form\Type\YesNoType; use App\Form\Type\YesNoType;
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay; use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -53,19 +54,19 @@ class UserPreference
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
private ?int $id = null; private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'preferences')] #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'preferences')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull] #[Assert\NotNull]
private ?User $user = null; 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\NotNull]
#[Assert\Length(min: 2, max: 50)] #[Assert\Length(min: 2, max: 50)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]
private string $name; 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)] #[Assert\Length(max: 250)]
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Default'])] #[Serializer\Groups(['Default'])]

View File

@@ -10,6 +10,7 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\WorkingTimeRepository; use App\Repository\WorkingTimeRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -23,25 +24,25 @@ class WorkingTime
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')] #[ORM\Column(name: 'id', type: Types::INTEGER)]
private ?int $id = null; private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)] #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull] #[Assert\NotNull]
private ?User $user = null; private ?User $user = null;
#[ORM\Column(name: 'date', type: 'date', nullable: false)] #[ORM\Column(name: 'date', type: Types::DATE_MUTABLE, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
private \DateTimeInterface $date; private \DateTimeInterface $date;
#[ORM\Column(name: 'expected', type: 'integer', nullable: false)] #[ORM\Column(name: 'expected', type: Types::INTEGER, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
private int $expectedTime = 0; private int $expectedTime = 0;
#[ORM\Column(name: 'actual', type: 'integer', nullable: false)] #[ORM\Column(name: 'actual', type: Types::INTEGER, nullable: false)]
#[Assert\NotNull] #[Assert\NotNull]
private int $actualTime = 0; private int $actualTime = 0;
#[ORM\ManyToOne(targetEntity: User::class)] #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')] #[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')]
private ?User $approvedBy = 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] #[Assert\NotNull]
private ?\DateTimeImmutable $approvedAt = null; private ?\DateTimeImmutable $approvedAt = null;

View File

@@ -14,7 +14,6 @@ use App\Entity\Activity;
/** /**
* Object used to unify the access to budget data in charts. * Object used to unify the access to budget data in charts.
* *
* @internal do not use in plugins, no BC promise given!
* @method Activity getEntity() * @method Activity getEntity()
*/ */
class ActivityBudgetStatisticModel extends BudgetStatisticModel class ActivityBudgetStatisticModel extends BudgetStatisticModel

View File

@@ -14,8 +14,6 @@ use App\Model\Statistic\BudgetStatistic;
/** /**
* Object used to unify the access to budget data in charts. * 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 class BudgetStatisticModel implements BudgetStatisticModelInterface
{ {

View File

@@ -9,9 +9,6 @@
namespace App\Model; namespace App\Model;
/**
* @internal do not use in plugins, no BC promise given!
*/
interface BudgetStatisticModelInterface interface BudgetStatisticModelInterface
{ {
public function isMonthlyBudget(): bool; public function isMonthlyBudget(): bool;

View File

@@ -14,7 +14,6 @@ use App\Entity\Customer;
/** /**
* Object used to unify the access to budget data in charts. * Object used to unify the access to budget data in charts.
* *
* @internal do not use in plugins, no BC promise given!
* @method Customer getEntity() * @method Customer getEntity()
*/ */
class CustomerBudgetStatisticModel extends BudgetStatisticModel class CustomerBudgetStatisticModel extends BudgetStatisticModel

View File

@@ -14,7 +14,6 @@ use App\Entity\Project;
/** /**
* Object used to unify the access to budget data in charts. * Object used to unify the access to budget data in charts.
* *
* @internal do not use in plugins, no BC promise given!
* @method Project getEntity() * @method Project getEntity()
*/ */
class ProjectBudgetStatisticModel extends BudgetStatisticModel class ProjectBudgetStatisticModel extends BudgetStatisticModel

View File

@@ -46,7 +46,7 @@ final class ProjectDuplicationService
$newProject->setEnd(null); $newProject->setEnd(null);
} }
$this->projectService->saveNewProject($newProject); $this->projectService->saveProject($newProject);
foreach ($this->projectRateRepository->getRatesForProject($project) as $rate) { foreach ($this->projectRateRepository->getRatesForProject($project) as $rate) {
$newRate = clone $rate; $newRate = clone $rate;

View File

@@ -56,6 +56,18 @@ final class ProjectService
return $project; 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 public function saveNewProject(Project $project, ?Context $context = null): Project
{ {
if (null !== $project->getId()) { if (null !== $project->getId()) {
@@ -97,6 +109,9 @@ final class ProjectService
} }
} }
/**
* @deprecated since 2.35 - use saveProject() instead
*/
public function updateProject(Project $project): Project public function updateProject(Project $project): Project
{ {
$this->validateProject($project); $this->validateProject($project);
@@ -108,8 +123,12 @@ final class ProjectService
return $project; 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]); return $this->repository->findOneBy(['name' => $name]);
} }

View File

@@ -152,8 +152,6 @@ class ActivityRepository extends EntityRepository
/** /**
* Returns a query builder that is used for ActivityType and your own 'query_builder' option. * Returns a query builder that is used for ActivityType and your own 'query_builder' option.
*
* @internal
*/ */
public function getQueryBuilderForFormType(ActivityFormTypeQuery $query): QueryBuilder public function getQueryBuilderForFormType(ActivityFormTypeQuery $query): QueryBuilder
{ {

View File

@@ -134,8 +134,6 @@ class CustomerRepository extends EntityRepository
/** /**
* Returns a query builder that is used for CustomerType and your own 'query_builder' option. * Returns a query builder that is used for CustomerType and your own 'query_builder' option.
*
* @internal
*/ */
public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder
{ {

View File

@@ -147,8 +147,6 @@ class ProjectRepository extends EntityRepository
/** /**
* Returns a query builder that is used for ProjectType and your own 'query_builder' option. * Returns a query builder that is used for ProjectType and your own 'query_builder' option.
*
* @internal
*/ */
public function getQueryBuilderForFormType(ProjectFormTypeQuery $query): QueryBuilder public function getQueryBuilderForFormType(ProjectFormTypeQuery $query): QueryBuilder
{ {

View File

@@ -18,9 +18,6 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
/**
* @internal
*/
final class TimesheetResult final class TimesheetResult
{ {
private ?TimesheetResultStatistic $statisticCache = null; private ?TimesheetResultStatistic $statisticCache = null;

View File

@@ -26,11 +26,21 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/doc'); $this->assertAccessIsGranted($client, '/api/doc');
self::assertStringContainsString('<title>Kimai', $client->getResponse()->getContent()); $content = $client->getResponse()->getContent();
$result = $client->getCrawler()->filter('script#swagger-data'); self::assertIsString($content);
$swaggerJson = json_decode($result->text(), true); 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 = []; $tags = [];
foreach ($swaggerJson['spec']['paths'] as $path) { foreach ($json['paths'] as $path) {
foreach ($path as $method) { foreach ($path as $method) {
foreach ($method['tags'] as $tag) { foreach ($method['tags'] as $tag) {
$tags[$tag] = $tag; $tags[$tag] = $tag;
@@ -45,13 +55,6 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase
sort($expectedKeys); sort($expectedKeys);
self::assertEquals($expectedKeys, $actual, \sprintf('Expected %s sections in API docs, but found %s.', \count($actual), \count($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 = [ $paths = [
'/api/actions/timesheet/{id}/{view}/{locale}', '/api/actions/timesheet/{id}/{view}/{locale}',
@@ -108,7 +111,7 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase
self::assertEquals('3.0.0', $json['openapi']); self::assertEquals('3.0.0', $json['openapi']);
self::assertArrayHasKey('info', $json); self::assertArrayHasKey('info', $json);
self::assertStringStartsWith('Kimai', $json['info']['title']); 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::assertArrayHasKey('paths', $json);
self::assertEquals($paths, array_keys($json['paths'])); self::assertEquals($paths, array_keys($json['paths']));
@@ -119,10 +122,6 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase
self::assertArrayHasKey('components', $json); self::assertArrayHasKey('components', $json);
self::assertArrayHasKey('schemas', $json['components']); self::assertArrayHasKey('schemas', $json['components']);
self::assertArrayHasKey('securitySchemes', $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 protected function createUrl(string $url): string

View File

@@ -58,19 +58,6 @@ class ActivityServiceTest extends TestCase
return $service; 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 public function testsaveNewActivityHasValidationError(): void
{ {
$constraints = new ConstraintViolationList(); $constraints = new ConstraintViolationList();
@@ -84,7 +71,7 @@ class ActivityServiceTest extends TestCase
$this->expectException(ValidationFailedException::class); $this->expectException(ValidationFailedException::class);
$this->expectExceptionMessage('Validation Failed'); $this->expectExceptionMessage('Validation Failed');
$sut->saveNewActivity(new Activity()); $sut->saveActivity(new Activity());
} }
public function testUpdateDispatchesEvents(): void public function testUpdateDispatchesEvents(): void
@@ -107,7 +94,7 @@ class ActivityServiceTest extends TestCase
$sut = $this->getSut($dispatcher); $sut = $this->getSut($dispatcher);
$sut->updateActivity($project); $sut->saveActivity($project);
} }
public function testcreateNewActivityDispatchesEvents(): void public function testcreateNewActivityDispatchesEvents(): void
@@ -143,7 +130,7 @@ class ActivityServiceTest extends TestCase
$sut = $this->getSut($dispatcher); $sut = $this->getSut($dispatcher);
$activity = new Activity(); $activity = new Activity();
$sut->saveNewActivity($activity); $sut->saveActivity($activity);
} }
public function testcreateNewActivityWithoutCustomer(): void public function testcreateNewActivityWithoutCustomer(): void

View File

@@ -16,7 +16,6 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
/** /**
@@ -62,17 +61,6 @@ class BundleInstallerCommandTest extends KernelTestCase
self::assertEquals(1, $commandTester->getStatusCode()); 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 public function testInvalidNamespaceWillRaiseException(): void
{ {
$this->expectException(LogicException::class); $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 class InstallerWithAssetsCommand extends TestBundleInstallerCommand
{ {
protected function hasAssets(): bool protected function hasAssets(): bool

View File

@@ -69,19 +69,6 @@ class CustomerServiceTest extends TestCase
return new CustomerService($repository, $configuration, $validator, $dispatcher); 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 public function testSaveNewCustomerHasValidationError(): void
{ {
$constraints = new ConstraintViolationList(); $constraints = new ConstraintViolationList();
@@ -95,7 +82,7 @@ class CustomerServiceTest extends TestCase
$this->expectException(ValidationFailedException::class); $this->expectException(ValidationFailedException::class);
$this->expectExceptionMessage('Validation Failed'); $this->expectExceptionMessage('Validation Failed');
$sut->saveNewCustomer(new Customer('foo')); $sut->saveCustomer(new Customer('foo'));
} }
public function testUpdateDispatchesEvents(): void public function testUpdateDispatchesEvents(): void
@@ -118,7 +105,7 @@ class CustomerServiceTest extends TestCase
$sut = $this->getSut($dispatcher); $sut = $this->getSut($dispatcher);
$sut->updateCustomer($Customer); $sut->saveCustomer($Customer);
} }
public function testCreateNewCustomerDispatchesEvents(): void public function testCreateNewCustomerDispatchesEvents(): void
@@ -155,7 +142,7 @@ class CustomerServiceTest extends TestCase
$sut = $this->getSut($dispatcher); $sut = $this->getSut($dispatcher);
$Customer = new Customer('foo'); $Customer = new Customer('foo');
$sut->saveNewCustomer($Customer); $sut->saveCustomer($Customer);
} }
/** /**

View File

@@ -64,19 +64,6 @@ class ProjectServiceTest extends TestCase
return new ProjectService($repository, $configuration, $dispatcher, $validator); 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 public function testSaveNewProjectHasValidationError(): void
{ {
$constraints = new ConstraintViolationList(); $constraints = new ConstraintViolationList();
@@ -90,7 +77,7 @@ class ProjectServiceTest extends TestCase
$this->expectException(ValidationFailedException::class); $this->expectException(ValidationFailedException::class);
$this->expectExceptionMessage('Validation Failed'); $this->expectExceptionMessage('Validation Failed');
$sut->saveNewProject(new Project(), new Context(new User())); $sut->saveProject(new Project(), new Context(new User()));
} }
public function testUpdateDispatchesEvents(): void public function testUpdateDispatchesEvents(): void
@@ -113,7 +100,7 @@ class ProjectServiceTest extends TestCase
$sut = $this->getSut($dispatcher); $sut = $this->getSut($dispatcher);
$sut->updateProject($project); $sut->saveProject($project);
} }
public function testCreateNewProjectDispatchesEvents(): void public function testCreateNewProjectDispatchesEvents(): void
@@ -149,7 +136,7 @@ class ProjectServiceTest extends TestCase
$sut = $this->getSut($dispatcher); $sut = $this->getSut($dispatcher);
$project = new Project(); $project = new Project();
$sut->saveNewProject($project, new Context(new User())); $sut->saveProject($project, new Context(new User()));
self::assertCount(0, $project->getTeams()); self::assertCount(0, $project->getTeams());
} }
@@ -170,7 +157,7 @@ class ProjectServiceTest extends TestCase
$user->addTeam($team2); $user->addTeam($team2);
$project = new Project(); $project = new Project();
$sut->saveNewProject($project, new Context($user)); $sut->saveProject($project, new Context($user));
self::assertCount(2, $project->getTeams()); self::assertCount(2, $project->getTeams());
} }

View File

@@ -187,41 +187,6 @@ parameters:
count: 13 count: 13
path: API/ActivityControllerTest.php 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\\.$#" message: "#^Cannot call method getValue\\(\\) on App\\\\Entity\\\\MetaTableTypeInterface\\|null\\.$#"
count: 1 count: 1