From a918d19fc51bb4441c9d10ec347ed395cc8aef0e Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 11:27:02 +0200 Subject: [PATCH 01/23] 7273: Added /** @var Collection */ to 20 collection properties across 9 entity files --- CHANGELOG.md | 5 + phpstan-baseline.neon | 238 +--------------------------------- src/Entity/Client.php | 2 + src/Entity/Epic.php | 1 + src/Entity/Invoice.php | 1 + src/Entity/InvoiceEntry.php | 2 + src/Entity/Issue.php | 4 + src/Entity/Product.php | 1 + src/Entity/Project.php | 7 + src/Entity/ProjectBilling.php | 1 + src/Entity/Version.php | 1 + 11 files changed, 27 insertions(+), 236 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9bc392ea..ae736b198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Added `Collection` generic typehints to OneToMany/ManyToMany + properties on `Client`, `Epic`, `Invoice`, `InvoiceEntry`, `Issue`, + `Product`, `Project`, `ProjectBilling`, and `Version` entities. Resolves + 20 PHPStan `doctrine.associationType` mismatches (and 19 cascading + inference errors); baseline shrinks from 530 to 491 entries. * Migrated static analysis from Psalm to PHPStan (level 8) with `phpstan/extension-installer`, `phpstan-symfony`, `phpstan-doctrine`, and `phpstan-phpunit`. The `composer code-analysis` entry point is unchanged. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index de766dda9..957e2a208 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -108,36 +108,12 @@ parameters: count: 1 path: src/Entity/Account.php - - - message: '#^Property App\\Entity\\Client\:\:\$invoices type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Client.php - - - - message: '#^Property App\\Entity\\Client\:\:\$invoices with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Client.php - - message: '#^Property App\\Entity\\Client\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType count: 1 path: src/Entity/Client.php - - - message: '#^Property App\\Entity\\Client\:\:\$projects type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Client.php - - - - message: '#^Property App\\Entity\\Client\:\:\$projects with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Client.php - - message: '#^Property App\\Entity\\CybersecurityAgreement\:\:\$serviceAgreement type mapping mismatch\: property can contain App\\Entity\\ServiceAgreement\|null but database expects App\\Entity\\ServiceAgreement\.$#' identifier: doctrine.associationType @@ -157,14 +133,8 @@ parameters: path: src/Entity/DataProvider.php - - message: '#^Property App\\Entity\\Epic\:\:\$issues type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Epic.php - - - - message: '#^Property App\\Entity\\Epic\:\:\$issues with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics + message: '#^Method App\\Entity\\Epic\:\:getIssues\(\) should return Doctrine\\Common\\Collections\\Collection\ but returns Doctrine\\Common\\Collections\\Collection\\.$#' + identifier: return.type count: 1 path: src/Entity/Epic.php @@ -180,18 +150,6 @@ parameters: count: 1 path: src/Entity/Invoice.php - - - message: '#^Property App\\Entity\\Invoice\:\:\$invoiceEntries type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Invoice.php - - - - message: '#^Property App\\Entity\\Invoice\:\:\$invoiceEntries with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Invoice.php - - message: '#^Property App\\Entity\\Invoice\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -222,42 +180,6 @@ parameters: count: 1 path: src/Entity/InvoiceEntry.php - - - message: '#^Property App\\Entity\\InvoiceEntry\:\:\$issueProducts type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/InvoiceEntry.php - - - - message: '#^Property App\\Entity\\InvoiceEntry\:\:\$issueProducts with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/InvoiceEntry.php - - - - message: '#^Property App\\Entity\\InvoiceEntry\:\:\$worklogs type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/InvoiceEntry.php - - - - message: '#^Property App\\Entity\\InvoiceEntry\:\:\$worklogs with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/InvoiceEntry.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$epics type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Issue.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$epics with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Issue.php - - message: '#^Property App\\Entity\\Issue\:\:\$linkToIssue type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -270,18 +192,6 @@ parameters: count: 1 path: src/Entity/Issue.php - - - message: '#^Property App\\Entity\\Issue\:\:\$products type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Issue.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$products with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Issue.php - - message: '#^Property App\\Entity\\Issue\:\:\$projectTrackerId type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -294,30 +204,6 @@ parameters: count: 1 path: src/Entity/Issue.php - - - message: '#^Property App\\Entity\\Issue\:\:\$versions type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Issue.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$versions with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Issue.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$worklogs type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Issue.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$worklogs with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Issue.php - - message: '#^Property App\\Entity\\IssueProduct\:\:\$issue type mapping mismatch\: property can contain App\\Entity\\Issue\|null but database expects App\\Entity\\Issue\.$#' identifier: doctrine.associationType @@ -336,18 +222,6 @@ parameters: count: 1 path: src/Entity/IssueProduct.php - - - message: '#^Property App\\Entity\\Product\:\:\$issues type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Product.php - - - - message: '#^Property App\\Entity\\Product\:\:\$issues with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Product.php - - message: '#^Property App\\Entity\\Product\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -366,72 +240,12 @@ parameters: count: 1 path: src/Entity/Product.php - - - message: '#^Property App\\Entity\\Project\:\:\$clients type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$clients with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$invoices type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$invoices with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$issues type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$issues with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Project.php - - message: '#^Property App\\Entity\\Project\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType count: 1 path: src/Entity/Project.php - - - message: '#^Property App\\Entity\\Project\:\:\$products type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$products with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$projectBillings type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$projectBillings with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Project.php - - message: '#^Property App\\Entity\\Project\:\:\$projectTrackerKey type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -444,42 +258,6 @@ parameters: count: 1 path: src/Entity/Project.php - - - message: '#^Property App\\Entity\\Project\:\:\$versions type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$versions with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$worklogs type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$worklogs with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$invoices type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/ProjectBilling.php - - - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$invoices with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/ProjectBilling.php - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -636,18 +414,6 @@ parameters: count: 1 path: src/Entity/Version.php - - - message: '#^Property App\\Entity\\Version\:\:\$issues type mapping mismatch\: property can contain Doctrine\\Common\\Collections\\Collection but database expects Doctrine\\Common\\Collections\\Collection&iterable\\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Version.php - - - - message: '#^Property App\\Entity\\Version\:\:\$issues with generic interface Doctrine\\Common\\Collections\\Collection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Entity/Version.php - - message: '#^Property App\\Entity\\Version\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType diff --git a/src/Entity/Client.php b/src/Entity/Client.php index c83f29cfe..57d990743 100644 --- a/src/Entity/Client.php +++ b/src/Entity/Client.php @@ -36,9 +36,11 @@ class Client extends AbstractBaseEntity #[ORM\Column(length: 255, nullable: true)] private ?string $ean = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'client', targetEntity: Invoice::class)] private Collection $invoices; + /** @var Collection */ #[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'clients')] private Collection $projects; diff --git a/src/Entity/Epic.php b/src/Entity/Epic.php index d711986d5..6f669d759 100644 --- a/src/Entity/Epic.php +++ b/src/Entity/Epic.php @@ -22,6 +22,7 @@ public function __construct() #[ORM\Column(length: 255)] private ?string $title = null; + /** @var Collection */ #[ORM\ManyToMany(targetEntity: Issue::class, mappedBy: 'epics')] private Collection $issues; diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index e0eb235cd..15b9f03fe 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -61,6 +61,7 @@ class Invoice extends AbstractBaseEntity #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $periodTo = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'invoice', targetEntity: InvoiceEntry::class, cascade: ['remove'])] #[ORM\OrderBy(['index' => Criteria::ASC])] private Collection $invoiceEntries; diff --git a/src/Entity/InvoiceEntry.php b/src/Entity/InvoiceEntry.php index 643ae1286..bd3556608 100644 --- a/src/Entity/InvoiceEntry.php +++ b/src/Entity/InvoiceEntry.php @@ -48,9 +48,11 @@ class InvoiceEntry extends AbstractBaseEntity #[ORM\Column(length: 255, nullable: true)] private ?MaterialNumberEnum $materialNumber = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'invoiceEntry', targetEntity: Worklog::class)] private Collection $worklogs; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'invoiceEntry', targetEntity: IssueProduct::class)] private Collection $issueProducts; diff --git a/src/Entity/Issue.php b/src/Entity/Issue.php index 99acc2b8c..56b166ccf 100644 --- a/src/Entity/Issue.php +++ b/src/Entity/Issue.php @@ -47,12 +47,15 @@ class Issue extends AbstractBaseEntity #[ORM\Column(length: 255, nullable: true)] private ?string $epicName = null; + /** @var Collection */ #[ORM\ManyToMany(targetEntity: Epic::class, inversedBy: 'issues')] private Collection $epics; + /** @var Collection */ #[ORM\ManyToMany(targetEntity: Version::class, inversedBy: 'issues')] private Collection $versions; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'issue', targetEntity: Worklog::class)] private Collection $worklogs; @@ -62,6 +65,7 @@ class Issue extends AbstractBaseEntity #[ORM\ManyToOne(inversedBy: 'issues')] private ?Project $project = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'issue', targetEntity: IssueProduct::class, orphanRemoval: true)] #[ORM\OrderBy(['createdAt' => Criteria::ASC])] private Collection $products; diff --git a/src/Entity/Product.php b/src/Entity/Product.php index 98606f83b..0eafecbba 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -26,6 +26,7 @@ class Product extends AbstractBaseEntity #[Assert\LessThan(1_000_000)] private ?string $price = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'product', targetEntity: IssueProduct::class, orphanRemoval: true)] private Collection $issues; diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 3c60de7ff..c3284ceea 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -21,6 +21,7 @@ class Project extends AbstractBaseEntity #[ORM\Column(length: 255)] private ?string $name = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'project', targetEntity: Invoice::class)] private Collection $invoices; @@ -33,21 +34,26 @@ class Project extends AbstractBaseEntity #[ORM\Column(length: 255, nullable: true)] private ?string $projectTrackerId; + /** @var Collection */ #[ORM\ManyToMany(targetEntity: Client::class, inversedBy: 'projects')] private Collection $clients; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'project', targetEntity: Version::class)] private Collection $versions; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'project', targetEntity: Worklog::class)] private Collection $worklogs; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBilling::class)] private Collection $projectBillings; #[ORM\Column(nullable: true)] private ?bool $include = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'project', targetEntity: Issue::class)] private Collection $issues; @@ -57,6 +63,7 @@ class Project extends AbstractBaseEntity #[ORM\Column(length: 255, nullable: true)] private ?string $projectLeadMail = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'project', targetEntity: Product::class, orphanRemoval: true)] private Collection $products; diff --git a/src/Entity/ProjectBilling.php b/src/Entity/ProjectBilling.php index 9df0da84e..6da67343f 100644 --- a/src/Entity/ProjectBilling.php +++ b/src/Entity/ProjectBilling.php @@ -20,6 +20,7 @@ class ProjectBilling extends AbstractBaseEntity #[ORM\Column(type: Types::DATETIME_MUTABLE)] private ?\DateTimeInterface $periodEnd = null; + /** @var Collection */ #[ORM\OneToMany(mappedBy: 'projectBilling', targetEntity: Invoice::class)] private Collection $invoices; diff --git a/src/Entity/Version.php b/src/Entity/Version.php index a89f78f32..07af3594c 100644 --- a/src/Entity/Version.php +++ b/src/Entity/Version.php @@ -28,6 +28,7 @@ class Version extends AbstractBaseEntity #[ORM\JoinColumn(nullable: false)] private ?Project $project = null; + /** @var Collection */ #[ORM\ManyToMany(targetEntity: Issue::class, mappedBy: 'versions')] private Collection $issues; From a87c37241068dd45a7def1bbf4c5263f6c7fa217 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 11:41:18 +0200 Subject: [PATCH 02/23] Added missing parameter and return types. --- CHANGELOG.md | 16 ++ phpstan-baseline.neon | 234 +----------------- src/Controller/InvoiceController.php | 2 +- src/Controller/ManagementReportController.php | 4 +- src/Controller/OpenIdConnectController.php | 2 +- src/Controller/SubscriptionController.php | 4 +- src/Entity/Invoice.php | 2 +- src/Entity/InvoiceEntry.php | 2 +- src/Repository/IssueRepository.php | 5 +- src/Repository/ProjectRepository.php | 7 +- src/Repository/SubscriptionRepository.php | 11 +- src/Repository/WorklogRepository.php | 10 +- src/Service/ClientHelper.php | 2 +- src/Service/LeantimeApiService.php | 2 +- src/Service/ManagementReportService.php | 11 +- tests/Service/DanishHolidayHelperTest.php | 18 +- tests/Service/HourReportServiceTest.php | 2 +- tests/Service/LeantimeApiServiceTest.php | 8 +- tests/Service/WorkloadReportServiceTest.php | 6 +- 19 files changed, 87 insertions(+), 261 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae736b198..f74fe14cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Added missing parameter and return types across controllers, services, + repositories, and tests (`InvoiceController::generateDescription`, + `ManagementReportController::getInvoicesDataFromDates`, + `OpenIdConnectController::logout`, `SubscriptionController::subscriptionHandler`, + `Invoice::setInvoiceEntryIndexes`, `InvoiceEntry::setInvoiceIndex`, + `ClientHelper::getStandardPrice`, `LeantimeApiService::post`, + `ManagementReportService::generateSpreadsheetCsvResponse`/`calculateYear`, + `IssueRepository::getClosedIssuesFromInterval`, + `ProjectRepository::getProjectTrackerIdsByDataProviders`, + `SubscriptionRepository::findByCustom`/`findOneByCustom`, + `WorklogRepository::find{,Billed}WorklogsByWorkerAndDateRange`, and + several PHPUnit test methods). Clears 37 PHPStan `missingType.return` + and `missingType.parameter` entries; baseline 491 → 454. Tightening + `SubscriptionController::subscriptionHandler` to require `string` + surfaced three pre-existing `string|null` call-site issues, now + recorded in the baseline for follow-up. * Added `Collection` generic typehints to OneToMany/ManyToMany properties on `Client`, `Epic`, `Invoice`, `InvoiceEntry`, `Issue`, `Product`, `Project`, `ProjectBilling`, and `Version` entities. Resolves diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 957e2a208..b046af2a6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -12,12 +12,6 @@ parameters: count: 1 path: src/Command/SubscriptionHandlerCommand.php - - - message: '#^Method App\\Controller\\InvoiceController\:\:generateDescription\(\) has parameter \$defaultInvoiceDescriptionTemplate with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Controller/InvoiceController.php - - message: '#^Method App\\Controller\\IssueController\:\:__construct\(\) has parameter \$options with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -36,24 +30,12 @@ parameters: count: 1 path: src/Controller/ManagementReportController.php - - - message: '#^Method App\\Controller\\ManagementReportController\:\:getInvoicesDataFromDates\(\) has parameter \$dateInterval with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Controller/ManagementReportController.php - - message: '#^Method App\\Controller\\ManagementReportController\:\:getInvoicesDataFromDates\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/Controller/ManagementReportController.php - - - message: '#^Method App\\Controller\\OpenIdConnectController\:\:logout\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/Controller/OpenIdConnectController.php - - message: '#^Method App\\Controller\\PlanningController\:\:createResponse\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -73,20 +55,20 @@ parameters: path: src/Controller/SubscriptionController.php - - message: '#^Method App\\Controller\\SubscriptionController\:\:subscriptionHandler\(\) has parameter \$content with no type specified\.$#' - identifier: missingType.parameter + message: '#^Parameter \#1 \$email of method App\\Repository\\SubscriptionRepository\:\:findByCustom\(\) expects string, string\|null given\.$#' + identifier: argument.type count: 1 path: src/Controller/SubscriptionController.php - - message: '#^Method App\\Controller\\SubscriptionController\:\:subscriptionHandler\(\) has parameter \$subscriptionType with no type specified\.$#' - identifier: missingType.parameter + message: '#^Parameter \#1 \$userEmail of method App\\Controller\\SubscriptionController\:\:subscriptionHandler\(\) expects string, string\|null given\.$#' + identifier: argument.type count: 1 path: src/Controller/SubscriptionController.php - - message: '#^Method App\\Controller\\SubscriptionController\:\:subscriptionHandler\(\) has parameter \$userEmail with no type specified\.$#' - identifier: missingType.parameter + message: '#^Parameter \#1 \$value of static method App\\Enum\\SubscriptionSubjectEnum\:\:tryFrom\(\) expects int\|string, string\|null given\.$#' + identifier: argument.type count: 1 path: src/Controller/SubscriptionController.php @@ -144,12 +126,6 @@ parameters: count: 1 path: src/Entity/Epic.php - - - message: '#^Method App\\Entity\\Invoice\:\:setInvoiceEntryIndexes\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/Entity/Invoice.php - - message: '#^Property App\\Entity\\Invoice\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -162,12 +138,6 @@ parameters: count: 1 path: src/Entity/Invoice.php - - - message: '#^Method App\\Entity\\InvoiceEntry\:\:setInvoiceIndex\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/Entity/InvoiceEntry.php - - message: '#^Property App\\Entity\\InvoiceEntry\:\:\$index type mapping mismatch\: property can contain int\|null but database expects int\.$#' identifier: doctrine.columnType @@ -1110,12 +1080,6 @@ parameters: count: 1 path: src/Repository/IssueRepository.php - - - message: '#^Method App\\Repository\\IssueRepository\:\:getClosedIssuesFromInterval\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/Repository/IssueRepository.php - - message: '#^Method App\\Repository\\IssueRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' identifier: missingType.iterableValue @@ -1242,18 +1206,6 @@ parameters: count: 1 path: src/Repository/ProjectRepository.php - - - message: '#^Method App\\Repository\\ProjectRepository\:\:getProjectTrackerIdsByDataProviders\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/Repository/ProjectRepository.php - - - - message: '#^Method App\\Repository\\ProjectRepository\:\:getProjectTrackerIdsByDataProviders\(\) has parameter \$dataProviders with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectRepository.php - - message: '#^Class App\\Repository\\ProjectVersionBudgetRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1320,42 +1272,6 @@ parameters: count: 1 path: src/Repository/SubscriptionRepository.php - - - message: '#^Method App\\Repository\\SubscriptionRepository\:\:findByCustom\(\) has parameter \$email with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Method App\\Repository\\SubscriptionRepository\:\:findByCustom\(\) has parameter \$urlParams with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Method App\\Repository\\SubscriptionRepository\:\:findByCustom\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Method App\\Repository\\SubscriptionRepository\:\:findOneByCustom\(\) has parameter \$email with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Method App\\Repository\\SubscriptionRepository\:\:findOneByCustom\(\) has parameter \$subscriptionType with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Method App\\Repository\\SubscriptionRepository\:\:findOneByCustom\(\) has parameter \$urlParams with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Repository/SubscriptionRepository.php - - message: '#^Method App\\Repository\\SubscriptionRepository\:\:getFilteredData\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1488,24 +1404,12 @@ parameters: count: 1 path: src/Repository/WorklogRepository.php - - - message: '#^Method App\\Repository\\WorklogRepository\:\:findBilledWorklogsByWorkerAndDateRange\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/Repository/WorklogRepository.php - - message: '#^Method App\\Repository\\WorklogRepository\:\:findByFilterData\(\) return type has no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue count: 1 path: src/Repository/WorklogRepository.php - - - message: '#^Method App\\Repository\\WorklogRepository\:\:findWorklogsByWorkerAndDateRange\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/Repository/WorklogRepository.php - - message: '#^Method App\\Repository\\WorklogRepository\:\:getTimeSpentByWorkerInWeekRange\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1548,12 +1452,6 @@ parameters: count: 1 path: src/Service/ClientHelper.php - - - message: '#^Method App\\Service\\ClientHelper\:\:getStandardPrice\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/Service/ClientHelper.php - - message: '#^Method App\\Service\\DanishHolidayHelper\:\:__construct\(\) has parameter \$nonWorkdays with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1908,30 +1806,12 @@ parameters: count: 1 path: src/Service/LeantimeApiService.php - - - message: '#^Method App\\Service\\LeantimeApiService\:\:post\(\) has parameter \$path with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Service/LeantimeApiService.php - - message: '#^Method App\\Service\\LeantimeApiService\:\:updateAsJob\(\) has parameter \$projectTrackerProjectIds with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/Service/LeantimeApiService.php - - - message: '#^Method App\\Service\\ManagementReportService\:\:calculateYear\(\) has parameter \$quarterValues with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Service/ManagementReportService.php - - - - message: '#^Method App\\Service\\ManagementReportService\:\:generateSpreadsheetCsvResponse\(\) has parameter \$dateInterval with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Service/ManagementReportService.php - - message: '#^Method App\\Service\\ManagementReportService\:\:generateSpreadsheetCsvResponse\(\) has parameter \$groupedInvoices with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -2022,12 +1902,6 @@ parameters: count: 1 path: src/Service/WorkloadReportService.php - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:assertSameDate\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:dataEaster\(\) return type has no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue @@ -2058,60 +1932,12 @@ parameters: count: 1 path: tests/Service/DanishHolidayHelperTest.php - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:testEaster\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:testHmm\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:testHolidayNames\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:testIsBankHoliday\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:testNextBankDay\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:testNextBankDay30\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:testNextNonHoliday\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - message: '#^Parameter \#1 \$expected of method App\\Tests\\Service\\DanishHolidayHelperTest\:\:assertSameDate\(\) expects DateTimeInterface, DateTimeInterface\|null given\.$#' identifier: argument.type count: 3 path: tests/Service/DanishHolidayHelperTest.php - - - message: '#^Result of method PHPUnit\\Framework\\Assert\:\:assertEquals\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - message: '#^Call to an undefined method object\:\:clear\(\)\.$#' identifier: method.notFound @@ -2208,12 +2034,6 @@ parameters: count: 1 path: tests/Service/DateTimeHelperTest.php - - - message: '#^Method App\\Tests\\Service\\HourReportServiceTest\:\:testGetDefaultFromDate\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/HourReportServiceTest.php - - message: '#^Call to an undefined method object\:\:clear\(\)\.$#' identifier: method.notFound @@ -2250,30 +2070,6 @@ parameters: count: 12 path: tests/Service/LeantimeApiServiceTest.php - - - message: '#^Method App\\Tests\\Service\\LeantimeApiServiceTest\:\:getMilestones\(\) has parameter \$modifiedYear with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Method App\\Tests\\Service\\LeantimeApiServiceTest\:\:getProjects\(\) has parameter \$modifiedYear with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Method App\\Tests\\Service\\LeantimeApiServiceTest\:\:getTickets\(\) has parameter \$modifiedYear with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Method App\\Tests\\Service\\LeantimeApiServiceTest\:\:getTimesheets\(\) has parameter \$modifiedYear with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#1 \$dataProviderId of method App\\Service\\LeantimeApiService\:\:deleteAsJob\(\) expects int, int\|null given\.$#' identifier: argument.type @@ -2382,24 +2178,6 @@ parameters: count: 3 path: tests/Service/WorkloadReportServiceTest.php - - - message: '#^Method App\\Tests\\Service\\WorkloadReportServiceTest\:\:testExceptionIsThrownWhenWorkerIdentifierIsEmpty\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/WorkloadReportServiceTest.php - - - - message: '#^Method App\\Tests\\Service\\WorkloadReportServiceTest\:\:testExceptionIsThrownWhenWorkerWorkloadIsUnset\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/WorkloadReportServiceTest.php - - - - message: '#^Method App\\Tests\\Service\\WorkloadReportServiceTest\:\:testGetWorkloadReport\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/Service/WorkloadReportServiceTest.php - - message: '#^Parameter \#2 \$timestamp of function date expects int\|null, int\|false given\.$#' identifier: argument.type diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 9ec40cdc8..dddf5c287 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -201,7 +201,7 @@ public function edit(Request $request, Invoice $invoice, InvoiceRepository $invo } #[Route('/{id}/generate-description', name: 'app_invoices_generate_description', methods: ['GET'])] - public function generateDescription(Invoice $invoice, $defaultInvoiceDescriptionTemplate): JsonResponse + public function generateDescription(Invoice $invoice, string $defaultInvoiceDescriptionTemplate): JsonResponse { $projectLeadName = $invoice->getProject()?->getProjectLeadName() ?? null; $projectLeadMail = $invoice->getProject()?->getProjectLeadMail() ?? null; diff --git a/src/Controller/ManagementReportController.php b/src/Controller/ManagementReportController.php index f62323654..2e5302523 100644 --- a/src/Controller/ManagementReportController.php +++ b/src/Controller/ManagementReportController.php @@ -123,10 +123,12 @@ private function createGroupedInvoices(array $invoices): array } /** + * @param array{dateFrom: string, dateTo: string} $dateInterval + * * @throws \Doctrine\ORM\Exception\NotSupported * @throws \Exception */ - private function getInvoicesDataFromDates($dateInterval, InvoiceRepository $invoiceRepository): array + private function getInvoicesDataFromDates(array $dateInterval, InvoiceRepository $invoiceRepository): array { return $invoiceRepository->getByRecordedDateBetween( new \DateTime($dateInterval['dateFrom']), diff --git a/src/Controller/OpenIdConnectController.php b/src/Controller/OpenIdConnectController.php index c66e40d8f..0dd356aed 100644 --- a/src/Controller/OpenIdConnectController.php +++ b/src/Controller/OpenIdConnectController.php @@ -18,7 +18,7 @@ public function generic(): RedirectResponse * @throws \Exception */ #[Route('/logout', name: 'app_logout', methods: ['GET'])] - public function logout() + public function logout(): never { // controller can be blank: it will never be called! throw new \Exception('Don\'t forget to activate logout in security.yaml'); diff --git a/src/Controller/SubscriptionController.php b/src/Controller/SubscriptionController.php index 3f7d29502..938bc4014 100644 --- a/src/Controller/SubscriptionController.php +++ b/src/Controller/SubscriptionController.php @@ -124,9 +124,11 @@ public function check(User $user, Request $request): Response } /** + * @param array $content + * * @throws NonUniqueResultException */ - private function subscriptionHandler($userEmail, $subscriptionType, $content): JsonResponse + private function subscriptionHandler(string $userEmail, string $subscriptionType, array $content): JsonResponse { $reportType = key($content); $subscription = $this->subscriptionRepository->findOneByCustom($userEmail, $subscriptionType, $content); diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index 15b9f03fe..45185379b 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -260,7 +260,7 @@ public function removeInvoiceEntry(InvoiceEntry $invoiceEntry): self return $this; } - public function setInvoiceEntryIndexes() + public function setInvoiceEntryIndexes(): void { $index = 0; foreach ($this->getInvoiceEntries() as $entry) { diff --git a/src/Entity/InvoiceEntry.php b/src/Entity/InvoiceEntry.php index bd3556608..5bcac5e94 100644 --- a/src/Entity/InvoiceEntry.php +++ b/src/Entity/InvoiceEntry.php @@ -244,7 +244,7 @@ public function setTotalPrice(?float $totalPrice): self #[ORM\PrePersist] #[ORM\PreUpdate] - public function setInvoiceIndex() + public function setInvoiceIndex(): void { $this->getInvoice()?->setInvoiceEntryIndexes(); } diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index 2493f2327..0f9f75aa5 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -87,7 +87,10 @@ public function findEpicOptionsByProject(Project $project): array return array_combine($titles, $ids); } - public function getClosedIssuesFromInterval(Project $project, \DateTimeInterface $periodStart, \DateTimeInterface $periodEnd) + /** + * @return array + */ + public function getClosedIssuesFromInterval(Project $project, \DateTimeInterface $periodStart, \DateTimeInterface $periodEnd): array { $from = new \DateTime($periodStart->format('Y-m-d').' 00:00:00'); $to = new \DateTime($periodEnd->format('Y-m-d').' 23:59:59'); diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index 203b6f615..150587b49 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -92,7 +92,12 @@ public function getFilteredPagination(ProjectFilterData $projectFilterData, int ); } - public function getProjectTrackerIdsByDataProviders(array $dataProviders) + /** + * @param array $dataProviders + * + * @return array + */ + public function getProjectTrackerIdsByDataProviders(array $dataProviders): array { $qb = $this->createQueryBuilder('project'); diff --git a/src/Repository/SubscriptionRepository.php b/src/Repository/SubscriptionRepository.php index 4fb9ae685..f9a8e83a7 100644 --- a/src/Repository/SubscriptionRepository.php +++ b/src/Repository/SubscriptionRepository.php @@ -41,7 +41,12 @@ public function remove(Subscription $entity, bool $flush = false): void } } - public function findByCustom($email, $urlParams): array + /** + * @param array $urlParams + * + * @return array + */ + public function findByCustom(string $email, array $urlParams): array { $qb = $this->createQueryBuilder('s'); @@ -59,9 +64,11 @@ public function findByCustom($email, $urlParams): array } /** + * @param array $urlParams + * * @throws NonUniqueResultException */ - public function findOneByCustom($email, $subscriptionType, $urlParams): ?Subscription + public function findOneByCustom(string $email, string $subscriptionType, array $urlParams): ?Subscription { $qb = $this->createQueryBuilder('s'); diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index 9a0cb9fae..cba375809 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -112,7 +112,10 @@ public function updateProjectByIssue(Issue $issue, Project $project): int ->execute(); } - public function findWorklogsByWorkerAndDateRange(string $workerIdentifier, \DateTime $dateFrom, \DateTime $dateTo) + /** + * @return array + */ + public function findWorklogsByWorkerAndDateRange(string $workerIdentifier, \DateTime $dateFrom, \DateTime $dateTo): array { $qb = $this->createQueryBuilder('worklog'); @@ -239,7 +242,10 @@ public function findBillableWorklogsByWorkerAndDateRange(\DateTime $dateFrom, \D return $qb->getQuery()->getResult(); } - public function findBilledWorklogsByWorkerAndDateRange(string $workerIdentifier, \DateTime $dateFrom, \DateTime $dateTo) + /** + * @return array + */ + public function findBilledWorklogsByWorkerAndDateRange(string $workerIdentifier, \DateTime $dateFrom, \DateTime $dateTo): array { $nonBillableEpics = NonBillableEpicsEnum::getAsArray(); $nonBillableVersions = NonBillableVersionsEnum::getAsArray(); diff --git a/src/Service/ClientHelper.php b/src/Service/ClientHelper.php index 624e3d0ca..77df86d7c 100644 --- a/src/Service/ClientHelper.php +++ b/src/Service/ClientHelper.php @@ -14,7 +14,7 @@ public function __construct( /** * Get standard price from client with fallback to global value. */ - public function getStandardPrice(?Client $client = null) + public function getStandardPrice(?Client $client = null): float { $standardPrice = (float) $this->options['standard_price']; diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index 1aff412b9..334d10357 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -322,7 +322,7 @@ private function fetchFromLeantime(DataProvider $dataProvider, string $type, arr return json_decode($response->getContent(), null, 512, JSON_THROW_ON_ERROR); } - private function post(DataProvider $dataProvider, $path, array $body): ResponseInterface + private function post(DataProvider $dataProvider, string $path, array $body): ResponseInterface { return $this->httpClient->request('POST', $dataProvider->getUrl().$this::API_PATH_DATA.$path, [ 'headers' => [ diff --git a/src/Service/ManagementReportService.php b/src/Service/ManagementReportService.php index ab9c892e6..93a3a8d70 100644 --- a/src/Service/ManagementReportService.php +++ b/src/Service/ManagementReportService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\Entity\Invoice; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use Symfony\Component\HttpFoundation\Response; @@ -15,7 +16,10 @@ public function __construct( ) { } - public function generateSpreadsheetCsvResponse(array $groupedInvoices, $dateInterval): Response + /** + * @param array{dateFrom: string, dateTo: string} $dateInterval + */ + public function generateSpreadsheetCsvResponse(array $groupedInvoices, array $dateInterval): Response { $values = []; // Add header. @@ -61,7 +65,10 @@ public function generateSpreadsheetCsvResponse(array $groupedInvoices, $dateInte return $response; } - private function calculateYear($quarterValues): string + /** + * @param array $quarterValues + */ + private function calculateYear(array $quarterValues): string { $sum = 0; foreach ($quarterValues as $invoice) { diff --git a/tests/Service/DanishHolidayHelperTest.php b/tests/Service/DanishHolidayHelperTest.php index 588d0b45c..41f326015 100644 --- a/tests/Service/DanishHolidayHelperTest.php +++ b/tests/Service/DanishHolidayHelperTest.php @@ -15,7 +15,7 @@ protected function setUp(): void $this->helper = DanishHolidayHelper::getInstance(); } - public function testHmm() + public function testHmm(): void { $this->assertNotEquals( $this->helper->getHolidays(2024), @@ -26,7 +26,7 @@ public function testHmm() /** * @dataProvider dataEaster */ - public function testEaster(int $year, \DateTimeInterface $expected) + public function testEaster(int $year, \DateTimeInterface $expected): void { $this->assertSameDate( $expected, @@ -52,7 +52,7 @@ public static function dataEaster(): iterable ]; } - public function testHolidayNames() + public function testHolidayNames(): void { $year = 2024; $expected = [ @@ -76,7 +76,7 @@ public function testHolidayNames() /** * @dataProvider dataNextNonHoliday */ - public function testNextNonHoliday(\DateTimeInterface $date, ?\DateTimeInterface $expected) + public function testNextNonHoliday(\DateTimeInterface $date, ?\DateTimeInterface $expected): void { $actual = $this->helper->getNextNonHoliday($date); $this->assertSameDate($expected, $actual); @@ -93,7 +93,7 @@ public static function dataNextNonHoliday(): iterable /** * @dataProvider dataNextBankDay */ - public function testNextBankDay(\DateTimeInterface $date, ?\DateTimeInterface $expected) + public function testNextBankDay(\DateTimeInterface $date, ?\DateTimeInterface $expected): void { $actual = $this->helper->getNextBankDay($date); $this->assertSameDate($expected, $actual); @@ -125,7 +125,7 @@ public static function dataNextBankDay(): iterable /** * @dataProvider dataNextBankDay30 */ - public function testNextBankDay30(\DateTimeInterface $date, ?\DateTimeInterface $expected) + public function testNextBankDay30(\DateTimeInterface $date, ?\DateTimeInterface $expected): void { $actual = $this->helper->getNextBankDay($date, 30); $this->assertSameDate($expected, $actual); @@ -152,7 +152,7 @@ public static function dataNextBankDay30(): iterable /** * @dataProvider dataIsBankHoliday */ - public function testIsBankHoliday(\DateTimeInterface $date, bool $expected) + public function testIsBankHoliday(\DateTimeInterface $date, bool $expected): void { $actual = $this->helper->isBankHoliday($date); $this->assertSame($expected, $actual); @@ -196,10 +196,10 @@ public static function dataIsBankHoliday(): iterable ]; } - private function assertSameDate(\DateTimeInterface $expected, \DateTimeInterface $actual, string $message = '') + private function assertSameDate(\DateTimeInterface $expected, \DateTimeInterface $actual, string $message = ''): void { try { - return $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp(), $message); + $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp(), $message); } catch (ExpectationFailedException $exception) { throw new ExpectationFailedException(sprintf('Failed asserting that %s matches expected %s.', $expected->format(\DateTimeInterface::ATOM), $actual->format(\DateTimeInterface::ATOM)), $exception->getComparisonFailure(), $exception); } diff --git a/tests/Service/HourReportServiceTest.php b/tests/Service/HourReportServiceTest.php index fac2dfc63..61999cf0c 100644 --- a/tests/Service/HourReportServiceTest.php +++ b/tests/Service/HourReportServiceTest.php @@ -19,7 +19,7 @@ public function setUp(): void ); } - public function testGetDefaultFromDate() + public function testGetDefaultFromDate(): void { $fromDate = $this->hourReportService->getDefaultFromDate(); diff --git a/tests/Service/LeantimeApiServiceTest.php b/tests/Service/LeantimeApiServiceTest.php index 44378d9a1..9e9848b26 100644 --- a/tests/Service/LeantimeApiServiceTest.php +++ b/tests/Service/LeantimeApiServiceTest.php @@ -429,7 +429,7 @@ private function getDeletedData(): object ', null, 512, JSON_THROW_ON_ERROR); } - private function getProjects($modifiedYear = 2024): object + private function getProjects(int $modifiedYear = 2024): object { return json_decode(' { @@ -454,7 +454,7 @@ private function getProjects($modifiedYear = 2024): object '); } - private function getMilestones($modifiedYear = 2024): object + private function getMilestones(int $modifiedYear = 2024): object { return json_decode(' { @@ -481,7 +481,7 @@ private function getMilestones($modifiedYear = 2024): object '); } - private function getTickets($modifiedYear = 2024): object + private function getTickets(int $modifiedYear = 2024): object { return json_decode(' { @@ -524,7 +524,7 @@ private function getTickets($modifiedYear = 2024): object '); } - private function getTimesheets($modifiedYear = 2024): object + private function getTimesheets(int $modifiedYear = 2024): object { return json_decode(' { diff --git a/tests/Service/WorkloadReportServiceTest.php b/tests/Service/WorkloadReportServiceTest.php index 8426a1e92..ed3064578 100644 --- a/tests/Service/WorkloadReportServiceTest.php +++ b/tests/Service/WorkloadReportServiceTest.php @@ -19,7 +19,7 @@ class WorkloadReportServiceTest extends TestCase /** * @throws Exception */ - public function testGetWorkloadReport() + public function testGetWorkloadReport(): void { $workerMock1 = $this->createMock(Worker::class); $workerMock1->method('getUserIdentifier')->willReturn('test0@test'); @@ -81,7 +81,7 @@ public function testGetWorkloadReport() $this->assertInstanceOf(WorkloadReportData::class, $result); } - public function testExceptionIsThrownWhenWorkerIdentifierIsEmpty() + public function testExceptionIsThrownWhenWorkerIdentifierIsEmpty(): void { $workerMock1 = $this->createMock(Worker::class); $workerMock1->method('getUserIdentifier')->willReturn('test0@test'); @@ -140,7 +140,7 @@ public function testExceptionIsThrownWhenWorkerIdentifierIsEmpty() $workloadReportService->getWorkloadReport(2024, PeriodTypeEnum::WEEK, ViewModeEnum::WORKLOAD); } - public function testExceptionIsThrownWhenWorkerWorkloadIsUnset() + public function testExceptionIsThrownWhenWorkerWorkloadIsUnset(): void { $workerMock1 = $this->createMock(Worker::class); $workerMock1->method('getUserIdentifier')->willReturn('test0@test'); From 46568d38bd7d080c05ff9b424787f1344a55b971 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 11:52:20 +0200 Subject: [PATCH 03/23] 7273: Added generic-class typehints --- CHANGELOG.md | 6 + phpstan-baseline.neon | 354 ------------------ src/Form/AccountType.php | 3 + src/Form/BillableUnbilledHoursReportType.php | 3 + src/Form/ClientFilterType.php | 3 + src/Form/ClientType.php | 3 + src/Form/CombinedServiceAgreementType.php | 3 + src/Form/CybersecurityAgreementType.php | 3 + src/Form/ForecastReportType.php | 3 + src/Form/HourReportType.php | 3 + src/Form/InvoiceEntryType.php | 3 + src/Form/InvoiceEntryWorklogFilterType.php | 3 + src/Form/InvoiceEntryWorklogType.php | 3 + src/Form/InvoiceFilterType.php | 3 + src/Form/InvoiceNewType.php | 3 + src/Form/InvoiceRecordType.php | 3 + src/Form/InvoiceType.php | 3 + src/Form/InvoicingRateReportType.php | 3 + src/Form/IssueFilterType.php | 3 + src/Form/IssueProductType.php | 3 + src/Form/ManagementReportDateIntervalType.php | 3 + src/Form/NameFilterType.php | 3 + src/Form/PlanningType.php | 3 + src/Form/ProductFilterType.php | 3 + src/Form/ProductType.php | 3 + src/Form/ProjectBillingFilterType.php | 3 + src/Form/ProjectBillingRecordType.php | 3 + src/Form/ProjectBillingType.php | 3 + src/Form/ProjectFilterType.php | 3 + src/Form/ProjectType.php | 3 + src/Form/ServiceAgreementFilterType.php | 3 + src/Form/ServiceAgreementType.php | 3 + src/Form/SubscriptionFilterType.php | 3 + src/Form/WorkerGroupType.php | 3 + src/Form/WorkerType.php | 3 + src/Form/WorkloadReportType.php | 3 + src/Model/Reports/HourReportProjectTicket.php | 2 + src/Model/Reports/HourReportWorklog.php | 1 + src/Model/Reports/InvoicingRateReportData.php | 1 + src/Model/Reports/WorkloadReportData.php | 1 + src/Repository/AccountRepository.php | 3 + src/Repository/ClientRepository.php | 3 + src/Repository/InvoiceRepository.php | 3 + src/Repository/IssueRepository.php | 3 + src/Repository/ProductRepository.php | 3 + src/Repository/ProjectBillingRepository.php | 3 + src/Repository/ProjectRepository.php | 3 + src/Repository/ServiceAgreementRepository.php | 3 + src/Repository/WorkerGroupRepository.php | 3 + src/Service/HourReportService.php | 2 +- src/Service/PlanningService.php | 4 +- 51 files changed, 143 insertions(+), 357 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f74fe14cd..8a32ceb0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Added generic-class typehints to clear all 50 PHPStan + `missingType.generics` entries: `@extends AbstractType` on 34 + form types, `@return PaginationInterface` on 9 repository + `getFilteredPagination()` methods, `ArrayCollection` on 5 report + model properties, and `ArrayIterator` on 3 service `@var` + docblocks. Baseline 454 → 394. * Added missing parameter and return types across controllers, services, repositories, and tests (`InvoiceController::generateDescription`, `ManagementReportController::getInvoicesDataFromDates`, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b046af2a6..959176f7b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -450,216 +450,12 @@ parameters: count: 1 path: src/Entity/Worklog.php - - - message: '#^Class App\\Form\\AccountType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/AccountType.php - - - - message: '#^Class App\\Form\\BillableUnbilledHoursReportType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/BillableUnbilledHoursReportType.php - - - - message: '#^Class App\\Form\\ClientFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ClientFilterType.php - - - - message: '#^Class App\\Form\\ClientType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ClientType.php - - message: '#^Method App\\Form\\ClientType\:\:getVersionOptions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/Form/ClientType.php - - - message: '#^Class App\\Form\\CombinedServiceAgreementType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/CombinedServiceAgreementType.php - - - - message: '#^Class App\\Form\\CybersecurityAgreementType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/CybersecurityAgreementType.php - - - - message: '#^Class App\\Form\\ForecastReportType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ForecastReportType.php - - - - message: '#^Class App\\Form\\HourReportType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/HourReportType.php - - - - message: '#^Class App\\Form\\InvoiceEntryType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/InvoiceEntryType.php - - - - message: '#^Class App\\Form\\InvoiceEntryWorklogFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/InvoiceEntryWorklogFilterType.php - - - - message: '#^Class App\\Form\\InvoiceEntryWorklogType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/InvoiceEntryWorklogType.php - - - - message: '#^Class App\\Form\\InvoiceFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/InvoiceFilterType.php - - - - message: '#^Class App\\Form\\InvoiceNewType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/InvoiceNewType.php - - - - message: '#^Class App\\Form\\InvoiceRecordType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/InvoiceRecordType.php - - - - message: '#^Class App\\Form\\InvoiceType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/InvoiceType.php - - - - message: '#^Class App\\Form\\InvoicingRateReportType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/InvoicingRateReportType.php - - - - message: '#^Class App\\Form\\IssueFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/IssueFilterType.php - - - - message: '#^Class App\\Form\\IssueProductType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/IssueProductType.php - - - - message: '#^Class App\\Form\\ManagementReportDateIntervalType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ManagementReportDateIntervalType.php - - - - message: '#^Class App\\Form\\NameFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/NameFilterType.php - - - - message: '#^Class App\\Form\\PlanningType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/PlanningType.php - - - - message: '#^Class App\\Form\\ProductFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ProductFilterType.php - - - - message: '#^Class App\\Form\\ProductType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ProductType.php - - - - message: '#^Class App\\Form\\ProjectBillingFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ProjectBillingFilterType.php - - - - message: '#^Class App\\Form\\ProjectBillingRecordType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ProjectBillingRecordType.php - - - - message: '#^Class App\\Form\\ProjectBillingType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ProjectBillingType.php - - - - message: '#^Class App\\Form\\ProjectFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ProjectFilterType.php - - - - message: '#^Class App\\Form\\ProjectType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ProjectType.php - - - - message: '#^Class App\\Form\\ServiceAgreementFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ServiceAgreementFilterType.php - - - - message: '#^Class App\\Form\\ServiceAgreementType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/ServiceAgreementType.php - - - - message: '#^Class App\\Form\\SubscriptionFilterType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/SubscriptionFilterType.php - - - - message: '#^Class App\\Form\\WorkerGroupType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/WorkerGroupType.php - - - - message: '#^Class App\\Form\\WorkerType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/WorkerType.php - - - - message: '#^Class App\\Form\\WorkloadReportType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/WorkloadReportType.php - - message: '#^Method App\\Message\\LeantimeUpdateMessage\:\:__construct\(\) has parameter \$projectTrackerProjectIds with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -768,36 +564,12 @@ parameters: count: 1 path: src/Model/Reports/HourReportProjectTicket.php - - - message: '#^Property App\\Model\\Reports\\HourReportProjectTicket\:\:\$projectTickets with generic class Doctrine\\Common\\Collections\\ArrayCollection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Model/Reports/HourReportProjectTicket.php - - - - message: '#^Property App\\Model\\Reports\\HourReportProjectTicket\:\:\$timesheets with generic class Doctrine\\Common\\Collections\\ArrayCollection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Model/Reports/HourReportProjectTicket.php - - - - message: '#^Property App\\Model\\Reports\\HourReportWorklog\:\:\$projectTicket with generic class Doctrine\\Common\\Collections\\ArrayCollection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Model/Reports/HourReportWorklog.php - - message: '#^Class App\\Model\\Reports\\InvoicingRateReportData has an uninitialized readonly property \$id\. Assign it in the constructor\.$#' identifier: property.uninitializedReadonly count: 1 path: src/Model/Reports/InvoicingRateReportData.php - - - message: '#^Property App\\Model\\Reports\\InvoicingRateReportData\:\:\$periodAverages with generic class Doctrine\\Common\\Collections\\ArrayCollection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Model/Reports/InvoicingRateReportData.php - - message: '#^Property App\\Model\\Reports\\InvoicingRateReportWorker\:\:\$dataByPeriod type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -816,12 +588,6 @@ parameters: count: 1 path: src/Model/Reports/WorkloadReportData.php - - - message: '#^Property App\\Model\\Reports\\WorkloadReportData\:\:\$periodAverages with generic class Doctrine\\Common\\Collections\\ArrayCollection does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: src/Model/Reports/WorkloadReportData.php - - message: '#^Class App\\Repository\\AccountRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -852,18 +618,6 @@ parameters: count: 1 path: src/Repository/AccountRepository.php - - - message: '#^Method App\\Repository\\AccountRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/AccountRepository.php - - - - message: '#^Method App\\Repository\\AccountRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/AccountRepository.php - - message: '#^Class App\\Repository\\ClientRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -888,18 +642,6 @@ parameters: count: 1 path: src/Repository/ClientRepository.php - - - message: '#^Method App\\Repository\\ClientRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ClientRepository.php - - - - message: '#^Method App\\Repository\\ClientRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/ClientRepository.php - - message: '#^Class App\\Repository\\DataProviderRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1002,18 +744,6 @@ parameters: count: 1 path: src/Repository/InvoiceRepository.php - - - message: '#^Method App\\Repository\\InvoiceRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceRepository.php - - - - message: '#^Method App\\Repository\\InvoiceRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/InvoiceRepository.php - - message: '#^Class App\\Repository\\IssueProductRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1080,18 +810,6 @@ parameters: count: 1 path: src/Repository/IssueRepository.php - - - message: '#^Method App\\Repository\\IssueRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Method App\\Repository\\IssueRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/IssueRepository.php - - message: '#^Method App\\Repository\\IssueRepository\:\:issuesContainingVersion\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1122,18 +840,6 @@ parameters: count: 1 path: src/Repository/ProductRepository.php - - - message: '#^Method App\\Repository\\ProductRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProductRepository.php - - - - message: '#^Method App\\Repository\\ProductRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/ProductRepository.php - - message: '#^Class App\\Repository\\ProjectBillingRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1158,18 +864,6 @@ parameters: count: 1 path: src/Repository/ProjectBillingRepository.php - - - message: '#^Method App\\Repository\\ProjectBillingRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectBillingRepository.php - - - - message: '#^Method App\\Repository\\ProjectBillingRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/ProjectBillingRepository.php - - message: '#^Class App\\Repository\\ProjectRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1194,18 +888,6 @@ parameters: count: 1 path: src/Repository/ProjectRepository.php - - - message: '#^Method App\\Repository\\ProjectRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectRepository.php - - - - message: '#^Method App\\Repository\\ProjectRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/ProjectRepository.php - - message: '#^Class App\\Repository\\ProjectVersionBudgetRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1236,18 +918,6 @@ parameters: count: 1 path: src/Repository/ServiceAgreementRepository.php - - - message: '#^Method App\\Repository\\ServiceAgreementRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ServiceAgreementRepository.php - - - - message: '#^Method App\\Repository\\ServiceAgreementRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/ServiceAgreementRepository.php - - message: '#^Class App\\Repository\\SubscriptionRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1332,18 +1002,6 @@ parameters: count: 1 path: src/Repository/VersionRepository.php - - - message: '#^Method App\\Repository\\WorkerGroupRepository\:\:getFilteredPagination\(\) return type has no value type specified in iterable type Knp\\Component\\Pager\\Pagination\\PaginationInterface\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorkerGroupRepository.php - - - - message: '#^Method App\\Repository\\WorkerGroupRepository\:\:getFilteredPagination\(\) return type with generic interface Knp\\Component\\Pager\\Pagination\\PaginationInterface does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Repository/WorkerGroupRepository.php - - message: '#^Class App\\Repository\\WorkerRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1602,12 +1260,6 @@ parameters: count: 1 path: src/Service/HourReportService.php - - - message: '#^PHPDoc tag @var for variable \$tagsIterator contains generic class ArrayIterator but does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: src/Service/HourReportService.php - - message: '#^Method App\\Service\\InvoiceEntryHelper\:\:__construct\(\) has parameter \$options with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -1848,12 +1500,6 @@ parameters: count: 1 path: src/Service/PlanningService.php - - - message: '#^PHPDoc tag @var for variable \$iterator contains generic class ArrayIterator but does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 2 - path: src/Service/PlanningService.php - - message: '#^Method App\\Service\\ProjectBillingService\:\:getIssuesNotIncludedInProjectBilling\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue diff --git a/src/Form/AccountType.php b/src/Form/AccountType.php index f2c84fe2e..d7114b895 100644 --- a/src/Form/AccountType.php +++ b/src/Form/AccountType.php @@ -8,6 +8,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class AccountType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/BillableUnbilledHoursReportType.php b/src/Form/BillableUnbilledHoursReportType.php index 8fa8a0052..b755cc50f 100644 --- a/src/Form/BillableUnbilledHoursReportType.php +++ b/src/Form/BillableUnbilledHoursReportType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class BillableUnbilledHoursReportType extends AbstractType { public function __construct( diff --git a/src/Form/ClientFilterType.php b/src/Form/ClientFilterType.php index 6d69e9133..cb91f846b 100644 --- a/src/Form/ClientFilterType.php +++ b/src/Form/ClientFilterType.php @@ -8,6 +8,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ClientFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ClientType.php b/src/Form/ClientType.php index 444055faf..6842dd2ee 100644 --- a/src/Form/ClientType.php +++ b/src/Form/ClientType.php @@ -16,6 +16,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Translation\TranslatableMessage; +/** + * @extends AbstractType + */ class ClientType extends AbstractType { public function __construct(private readonly VersionRepository $versionRepository, private readonly ClientRepository $clientRepository) diff --git a/src/Form/CombinedServiceAgreementType.php b/src/Form/CombinedServiceAgreementType.php index 8081d7596..8abb02fd3 100644 --- a/src/Form/CombinedServiceAgreementType.php +++ b/src/Form/CombinedServiceAgreementType.php @@ -7,6 +7,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class CombinedServiceAgreementType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/CybersecurityAgreementType.php b/src/Form/CybersecurityAgreementType.php index 5a63059be..95e045c0a 100644 --- a/src/Form/CybersecurityAgreementType.php +++ b/src/Form/CybersecurityAgreementType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class CybersecurityAgreementType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ForecastReportType.php b/src/Form/ForecastReportType.php index b624cfa0b..8a32d1053 100644 --- a/src/Form/ForecastReportType.php +++ b/src/Form/ForecastReportType.php @@ -10,6 +10,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ForecastReportType extends AbstractType { public function __construct( diff --git a/src/Form/HourReportType.php b/src/Form/HourReportType.php index 749eb3052..f82955130 100644 --- a/src/Form/HourReportType.php +++ b/src/Form/HourReportType.php @@ -16,6 +16,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class HourReportType extends AbstractType { public function __construct( diff --git a/src/Form/InvoiceEntryType.php b/src/Form/InvoiceEntryType.php index 9ba9f40c7..10726d4cd 100644 --- a/src/Form/InvoiceEntryType.php +++ b/src/Form/InvoiceEntryType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class InvoiceEntryType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/InvoiceEntryWorklogFilterType.php b/src/Form/InvoiceEntryWorklogFilterType.php index a85a116f0..c09282272 100644 --- a/src/Form/InvoiceEntryWorklogFilterType.php +++ b/src/Form/InvoiceEntryWorklogFilterType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class InvoiceEntryWorklogFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/InvoiceEntryWorklogType.php b/src/Form/InvoiceEntryWorklogType.php index 4c51cf45b..f72f7c6a5 100644 --- a/src/Form/InvoiceEntryWorklogType.php +++ b/src/Form/InvoiceEntryWorklogType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class InvoiceEntryWorklogType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/InvoiceFilterType.php b/src/Form/InvoiceFilterType.php index 749e29936..0ddff4fc3 100644 --- a/src/Form/InvoiceFilterType.php +++ b/src/Form/InvoiceFilterType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class InvoiceFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/InvoiceNewType.php b/src/Form/InvoiceNewType.php index 6fc7a3938..5f4ac894f 100644 --- a/src/Form/InvoiceNewType.php +++ b/src/Form/InvoiceNewType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class InvoiceNewType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/InvoiceRecordType.php b/src/Form/InvoiceRecordType.php index da2a08cd2..c5e58e1a9 100644 --- a/src/Form/InvoiceRecordType.php +++ b/src/Form/InvoiceRecordType.php @@ -8,6 +8,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class InvoiceRecordType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/InvoiceType.php b/src/Form/InvoiceType.php index 8a847e3e6..3e6d90e3a 100644 --- a/src/Form/InvoiceType.php +++ b/src/Form/InvoiceType.php @@ -12,6 +12,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class InvoiceType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/InvoicingRateReportType.php b/src/Form/InvoicingRateReportType.php index 2ebf0a641..770442ebe 100644 --- a/src/Form/InvoicingRateReportType.php +++ b/src/Form/InvoicingRateReportType.php @@ -11,6 +11,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class InvoicingRateReportType extends AbstractType { public function __construct( diff --git a/src/Form/IssueFilterType.php b/src/Form/IssueFilterType.php index b6d7b969c..2d72fecf5 100644 --- a/src/Form/IssueFilterType.php +++ b/src/Form/IssueFilterType.php @@ -8,6 +8,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class IssueFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/IssueProductType.php b/src/Form/IssueProductType.php index 289b0914d..43df0492b 100644 --- a/src/Form/IssueProductType.php +++ b/src/Form/IssueProductType.php @@ -17,6 +17,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Translation\TranslatableMessage; +/** + * @extends AbstractType + */ class IssueProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ManagementReportDateIntervalType.php b/src/Form/ManagementReportDateIntervalType.php index c9878e2c6..1b572e2ff 100644 --- a/src/Form/ManagementReportDateIntervalType.php +++ b/src/Form/ManagementReportDateIntervalType.php @@ -6,6 +6,9 @@ use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; +/** + * @extends AbstractType + */ class ManagementReportDateIntervalType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/NameFilterType.php b/src/Form/NameFilterType.php index 10be09f24..83ab0f50b 100644 --- a/src/Form/NameFilterType.php +++ b/src/Form/NameFilterType.php @@ -8,6 +8,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class NameFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/PlanningType.php b/src/Form/PlanningType.php index dcdfd92a3..490daabe3 100644 --- a/src/Form/PlanningType.php +++ b/src/Form/PlanningType.php @@ -10,6 +10,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class PlanningType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ProductFilterType.php b/src/Form/ProductFilterType.php index 98cd37e11..eb3021930 100644 --- a/src/Form/ProductFilterType.php +++ b/src/Form/ProductFilterType.php @@ -10,6 +10,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ProductFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ProductType.php b/src/Form/ProductType.php index 54ab2db83..2b1224aa1 100644 --- a/src/Form/ProductType.php +++ b/src/Form/ProductType.php @@ -11,6 +11,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Translation\TranslatableMessage; +/** + * @extends AbstractType + */ class ProductType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ProjectBillingFilterType.php b/src/Form/ProjectBillingFilterType.php index d0205db35..83128ad11 100644 --- a/src/Form/ProjectBillingFilterType.php +++ b/src/Form/ProjectBillingFilterType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ProjectBillingFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ProjectBillingRecordType.php b/src/Form/ProjectBillingRecordType.php index 77c8330ed..18761abaf 100644 --- a/src/Form/ProjectBillingRecordType.php +++ b/src/Form/ProjectBillingRecordType.php @@ -8,6 +8,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ProjectBillingRecordType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ProjectBillingType.php b/src/Form/ProjectBillingType.php index 753b22183..56e46adac 100644 --- a/src/Form/ProjectBillingType.php +++ b/src/Form/ProjectBillingType.php @@ -10,6 +10,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ProjectBillingType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ProjectFilterType.php b/src/Form/ProjectFilterType.php index 98131a323..0983e9147 100644 --- a/src/Form/ProjectFilterType.php +++ b/src/Form/ProjectFilterType.php @@ -9,6 +9,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ProjectFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ProjectType.php b/src/Form/ProjectType.php index 48e201ae8..d7807083d 100644 --- a/src/Form/ProjectType.php +++ b/src/Form/ProjectType.php @@ -8,6 +8,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ProjectType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ServiceAgreementFilterType.php b/src/Form/ServiceAgreementFilterType.php index a31ad8388..fe4ca442b 100644 --- a/src/Form/ServiceAgreementFilterType.php +++ b/src/Form/ServiceAgreementFilterType.php @@ -13,6 +13,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class ServiceAgreementFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ServiceAgreementType.php b/src/Form/ServiceAgreementType.php index c976cefdd..c7388436f 100644 --- a/src/Form/ServiceAgreementType.php +++ b/src/Form/ServiceAgreementType.php @@ -23,6 +23,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Url; +/** + * @extends AbstractType + */ class ServiceAgreementType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/SubscriptionFilterType.php b/src/Form/SubscriptionFilterType.php index 6b7a2dbac..267878526 100644 --- a/src/Form/SubscriptionFilterType.php +++ b/src/Form/SubscriptionFilterType.php @@ -8,6 +8,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class SubscriptionFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/WorkerGroupType.php b/src/Form/WorkerGroupType.php index 887b15208..41f05b012 100644 --- a/src/Form/WorkerGroupType.php +++ b/src/Form/WorkerGroupType.php @@ -10,6 +10,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class WorkerGroupType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/WorkerType.php b/src/Form/WorkerType.php index 110a9cd5c..fb2df93c3 100644 --- a/src/Form/WorkerType.php +++ b/src/Form/WorkerType.php @@ -11,6 +11,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class WorkerType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/WorkloadReportType.php b/src/Form/WorkloadReportType.php index 70961dbe7..737a08a86 100644 --- a/src/Form/WorkloadReportType.php +++ b/src/Form/WorkloadReportType.php @@ -12,6 +12,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class WorkloadReportType extends AbstractType { public function __construct( diff --git a/src/Model/Reports/HourReportProjectTicket.php b/src/Model/Reports/HourReportProjectTicket.php index 624445112..66405e269 100644 --- a/src/Model/Reports/HourReportProjectTicket.php +++ b/src/Model/Reports/HourReportProjectTicket.php @@ -12,7 +12,9 @@ class HourReportProjectTicket public float $totalEstimated; public float $totalSpent; public readonly string $linkToIssue; + /** @var ArrayCollection */ public ArrayCollection $timesheets; + /** @var ArrayCollection */ public ArrayCollection $projectTickets; public function __construct($id, $projectTrackerId, $headline, $totalEstimated, $totalSpent, $linkToIssue) diff --git a/src/Model/Reports/HourReportWorklog.php b/src/Model/Reports/HourReportWorklog.php index fa64d1345..b58787011 100644 --- a/src/Model/Reports/HourReportWorklog.php +++ b/src/Model/Reports/HourReportWorklog.php @@ -8,6 +8,7 @@ class HourReportWorklog { public readonly ?int $id; public readonly float $hours; + /** @var ArrayCollection */ public ArrayCollection $projectTicket; public function __construct(?int $id, float $hours) diff --git a/src/Model/Reports/InvoicingRateReportData.php b/src/Model/Reports/InvoicingRateReportData.php index 44ed02370..caf5cda55 100644 --- a/src/Model/Reports/InvoicingRateReportData.php +++ b/src/Model/Reports/InvoicingRateReportData.php @@ -13,6 +13,7 @@ class InvoicingRateReportData /** @var ArrayCollection */ public ArrayCollection $workers; public int $currentPeriodNumeric; + /** @var ArrayCollection */ public ArrayCollection $periodAverages; public float $totalAverage; public bool $includeIssues; diff --git a/src/Model/Reports/WorkloadReportData.php b/src/Model/Reports/WorkloadReportData.php index b75469f43..1f8a0da84 100644 --- a/src/Model/Reports/WorkloadReportData.php +++ b/src/Model/Reports/WorkloadReportData.php @@ -13,6 +13,7 @@ class WorkloadReportData /** @var ArrayCollection */ public ArrayCollection $workers; public int $currentPeriodNumeric; + /** @var ArrayCollection */ public ArrayCollection $periodAverages; public float $totalAverage; diff --git a/src/Repository/AccountRepository.php b/src/Repository/AccountRepository.php index 0629fff84..d7eeef9f4 100644 --- a/src/Repository/AccountRepository.php +++ b/src/Repository/AccountRepository.php @@ -58,6 +58,9 @@ public function getAllChoices(): array return $accountChoices; } + /** + * @return PaginationInterface + */ public function getFilteredPagination(NameFilterData $accountFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('account'); diff --git a/src/Repository/ClientRepository.php b/src/Repository/ClientRepository.php index eeb59ecf7..86058011a 100644 --- a/src/Repository/ClientRepository.php +++ b/src/Repository/ClientRepository.php @@ -42,6 +42,9 @@ public function remove(Client $entity, bool $flush = false): void } } + /** + * @return PaginationInterface + */ public function getFilteredPagination(ClientFilterData $clientFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('client'); diff --git a/src/Repository/InvoiceRepository.php b/src/Repository/InvoiceRepository.php index 367bc4f47..01644192b 100644 --- a/src/Repository/InvoiceRepository.php +++ b/src/Repository/InvoiceRepository.php @@ -59,6 +59,9 @@ public function getByRecordedDateBetween(\DateTime $from, \DateTime $to): array return $qb->getQuery()->getResult(); } + /** + * @return PaginationInterface + */ public function getFilteredPagination(InvoiceFilterData $invoiceFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('invoice'); diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index 0f9f75aa5..6f80dd968 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -40,6 +40,9 @@ public function save(Issue $entity, bool $flush = false): void } } + /** + * @return PaginationInterface + */ public function getFilteredPagination(IssueFilterData $issueFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('issue'); diff --git a/src/Repository/ProductRepository.php b/src/Repository/ProductRepository.php index 31bfae5f8..ac51cd481 100644 --- a/src/Repository/ProductRepository.php +++ b/src/Repository/ProductRepository.php @@ -26,6 +26,9 @@ public function __construct( parent::__construct($registry, Product::class); } + /** + * @return PaginationInterface + */ public function getFilteredPagination(ProductFilterData $productFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('product'); diff --git a/src/Repository/ProjectBillingRepository.php b/src/Repository/ProjectBillingRepository.php index 0ff435358..443f19a9e 100644 --- a/src/Repository/ProjectBillingRepository.php +++ b/src/Repository/ProjectBillingRepository.php @@ -42,6 +42,9 @@ public function remove(ProjectBilling $entity, bool $flush = false): void } } + /** + * @return PaginationInterface + */ public function getFilteredPagination(ProjectBillingFilterData $projectBillingFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('pb'); diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index 150587b49..963f955ba 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -54,6 +54,9 @@ public function getIncluded(): QueryBuilder return $qb; } + /** + * @return PaginationInterface + */ public function getFilteredPagination(ProjectFilterData $projectFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('project'); diff --git a/src/Repository/ServiceAgreementRepository.php b/src/Repository/ServiceAgreementRepository.php index 28e8ed46d..40a06e4f5 100644 --- a/src/Repository/ServiceAgreementRepository.php +++ b/src/Repository/ServiceAgreementRepository.php @@ -19,6 +19,9 @@ public function __construct(ManagerRegistry $registry, private readonly Paginato parent::__construct($registry, ServiceAgreement::class); } + /** + * @return PaginationInterface + */ public function getFilteredPagination(ServiceAgreementFilterData $serviceAgreementFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('service_agreement'); diff --git a/src/Repository/WorkerGroupRepository.php b/src/Repository/WorkerGroupRepository.php index b11b79fbc..71f066607 100644 --- a/src/Repository/WorkerGroupRepository.php +++ b/src/Repository/WorkerGroupRepository.php @@ -19,6 +19,9 @@ public function __construct(ManagerRegistry $registry, private readonly Paginato parent::__construct($registry, WorkerGroup::class); } + /** + * @return PaginationInterface + */ public function getFilteredPagination(NameFilterData $nameFilterData, int $page = 1): PaginationInterface { $qb = $this->createQueryBuilder('g'); diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 33f83caf0..303e4d554 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -86,7 +86,7 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? $hourReportData->projectTotalSpent += $totalTicketSpent; } - /** @var \ArrayIterator $tagsIterator */ + /** @var \ArrayIterator $tagsIterator */ $tagsIterator = $hourReportData->projectTags->getIterator(); // Sort tags by display name (uasort: keys are epic names, sort by the value's `tag` so 'noTag' lands in alphabetical position). $tagsIterator->uasort(fn ($a, $b) => mb_strtolower($a->tag) <=> mb_strtolower($b->tag)); diff --git a/src/Service/PlanningService.php b/src/Service/PlanningService.php index 407972dce..c886f070d 100644 --- a/src/Service/PlanningService.php +++ b/src/Service/PlanningService.php @@ -317,7 +317,7 @@ private function getOrCreateAssigneeProject(ArrayCollection $projects, string $p */ private function sortAssigneeCollectionByDisplayName(ArrayCollection $collection): ArrayCollection { - /** @var \ArrayIterator $iterator */ + /** @var \ArrayIterator $iterator */ $iterator = $collection->getIterator(); $iterator->uasort(function ($a, $b) { return mb_strtolower($a->displayName) <=> mb_strtolower($b->displayName); @@ -335,7 +335,7 @@ private function sortAssigneeCollectionByDisplayName(ArrayCollection $collection */ private function sortProjectCollectionByDisplayName(ArrayCollection $collection): ArrayCollection { - /** @var \ArrayIterator $iterator */ + /** @var \ArrayIterator $iterator */ $iterator = $collection->getIterator(); $iterator->uasort(function ($a, $b) { return mb_strtolower($a->displayName) <=> mb_strtolower($b->displayName); From ab97436f6d85a9a8a5d27fe5de1976b4a8705e62 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 12:17:58 +0200 Subject: [PATCH 04/23] 7273: Fixed low hanging phpstan issues --- CHANGELOG.md | 10 + phpstan-baseline.neon | 1004 ++--------------- src/Command/SubscriptionHandlerCommand.php | 2 +- src/Controller/IssueController.php | 3 + src/Controller/ManagementReportController.php | 7 + src/Controller/PlanningController.php | 5 + src/Controller/SubscriptionController.php | 3 + src/Entity/ServiceAgreement.php | 1 + src/Entity/Subscription.php | 7 + src/Entity/User.php | 4 + src/Form/ClientType.php | 3 + src/Message/LeantimeUpdateMessage.php | 3 + src/Model/DashboardData.php | 4 + .../DataProvider/DataProviderIssueData.php | 3 + .../InvoiceEntryWorklogsFilterData.php | 1 + src/Model/Invoices/PagedResult.php | 3 + .../BillableUnbilledHoursReportData.php | 1 + .../Reports/InvoicingRateReportWorker.php | 4 +- src/Repository/AccountRepository.php | 8 +- src/Repository/ClientRepository.php | 5 - src/Repository/DataProviderRepository.php | 5 - src/Repository/EpicRepository.php | 5 - src/Repository/InvoiceEntryRepository.php | 5 - src/Repository/InvoiceRepository.php | 8 +- src/Repository/IssueProductRepository.php | 5 - src/Repository/IssueRepository.php | 16 +- src/Repository/ProductRepository.php | 5 - src/Repository/ProjectBillingRepository.php | 5 - src/Repository/ProjectRepository.php | 5 - .../ProjectVersionBudgetRepository.php | 5 - src/Repository/ServiceAgreementRepository.php | 2 + src/Repository/SubscriptionRepository.php | 10 +- src/Repository/UserRepository.php | 5 - src/Repository/VersionRepository.php | 5 - src/Repository/WorkerRepository.php | 8 +- src/Repository/WorklogRepository.php | 15 +- src/Service/BillingService.php | 9 +- src/Service/ClientHelper.php | 3 + src/Service/DanishHolidayHelper.php | 19 + src/Service/DashboardService.php | 6 + src/Service/DateTimeHelper.php | 10 +- src/Service/HourReportService.php | 5 + src/Service/InvoiceEntryHelper.php | 12 + src/Service/InvoicingRateReportService.php | 6 +- src/Service/LeantimeApiService.php | 12 + src/Service/ManagementReportService.php | 1 + src/Service/PlanningService.php | 12 +- src/Service/ProjectBillingService.php | 3 + src/Service/SubscriptionHandlerService.php | 5 +- src/Service/WorkloadReportService.php | 6 +- tests/Service/DanishHolidayHelperTest.php | 15 + tests/Service/DateTimeHelperTest.php | 23 + 52 files changed, 332 insertions(+), 1005 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a32ceb0f..41c1942c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Cleared all 167 PHPStan `missingType.iterableValue` baseline entries. + Removed redundant `@method find*` scaffold docblocks from 17 repositories + (the parent `@extends ServiceEntityRepository` already provides typed + signatures), and typed iterable parameters/returns/properties across + repositories, services, controllers, models, entities, forms, commands, + messages, and tests. Baseline 394 → 268. 28 latent call-site issues + (`argument.type`/`method.nonObject`) were surfaced by the tighter typing + and are now recorded in the baseline for follow-up — e.g., calls on + nullable repository results (`Issue|null`, `Project|null`, + `DateTime|false`) without null checks. * Added generic-class typehints to clear all 50 PHPStan `missingType.generics` entries: `@extends AbstractType` on 34 form types, `@return PaginationInterface` on 9 repository diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 959176f7b..f4c488cf3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -7,50 +7,50 @@ parameters: path: src/Command/ProductsImportCommand.php - - message: '#^Method App\\Command\\SubscriptionHandlerCommand\:\:getLastQuarter\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetCsvResponse\(\) expects array\, array\ given\.$#' + identifier: argument.type count: 1 - path: src/Command/SubscriptionHandlerCommand.php + path: src/Controller/InvoiceController.php - - message: '#^Method App\\Controller\\IssueController\:\:__construct\(\) has parameter \$options with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetCsvResponse\(\) expects array\, list\ given\.$#' + identifier: argument.type count: 1 - path: src/Controller/IssueController.php + path: src/Controller/InvoiceController.php - - message: '#^Method App\\Controller\\ManagementReportController\:\:createGroupedInvoices\(\) has parameter \$invoices with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetHtml\(\) expects array\, array\ given\.$#' + identifier: argument.type count: 1 - path: src/Controller/ManagementReportController.php + path: src/Controller/InvoiceController.php - - message: '#^Method App\\Controller\\ManagementReportController\:\:createGroupedInvoices\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 + message: '#^Cannot call method format\(\) on DateTimeInterface\|null\.$#' + identifier: method.nonObject + count: 2 path: src/Controller/ManagementReportController.php - - message: '#^Method App\\Controller\\ManagementReportController\:\:getInvoicesDataFromDates\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$groupedInvoices of method App\\Service\\ManagementReportService\:\:generateSpreadsheetCsvResponse\(\) expects array\\>, array\\> given\.$#' + identifier: argument.type count: 1 path: src/Controller/ManagementReportController.php - - message: '#^Method App\\Controller\\PlanningController\:\:createResponse\(\) has parameter \$data with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetCsvResponse\(\) expects array\, array\ given\.$#' + identifier: argument.type count: 1 - path: src/Controller/PlanningController.php + path: src/Controller/ProjectBillingController.php - - message: '#^Method App\\Controller\\PlanningController\:\:preparePlanningData\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetHtml\(\) expects array\, array\ given\.$#' + identifier: argument.type count: 1 - path: src/Controller/PlanningController.php + path: src/Controller/ProjectBillingController.php - - message: '#^Method App\\Controller\\SubscriptionController\:\:getFrequencies\(\) has parameter \$subscriptions with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot access property \$value on App\\Enum\\SubscriptionFrequencyEnum\|null\.$#' + identifier: property.nonObject count: 1 path: src/Controller/SubscriptionController.php @@ -312,54 +312,24 @@ parameters: count: 1 path: src/Entity/ServiceAgreement.php - - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$systemOwnerNotices type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Entity/ServiceAgreement.php - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$validFrom type mapping mismatch\: property can contain DateTimeInterface\|null but database expects DateTimeInterface\.$#' identifier: doctrine.columnType count: 1 path: src/Entity/ServiceAgreement.php - - - message: '#^Method App\\Entity\\Subscription\:\:getUrlParams\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Entity/Subscription.php - - - - message: '#^Method App\\Entity\\Subscription\:\:setUrlParams\(\) has parameter \$urlParams with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Entity/Subscription.php - - message: '#^Property App\\Entity\\Subscription\:\:\$email type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType count: 1 path: src/Entity/Subscription.php - - - message: '#^Property App\\Entity\\Subscription\:\:\$urlParams type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Entity/Subscription.php - - message: '#^Method App\\Entity\\User\:\:getUserIdentifier\(\) should return non\-empty\-string but returns string\.$#' identifier: return.type count: 1 path: src/Entity/User.php - - - message: '#^Method App\\Entity\\User\:\:setRoles\(\) has parameter \$roles with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Entity/User.php - - message: '#^Property App\\Entity\\User\:\:\$email type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -372,12 +342,6 @@ parameters: count: 1 path: src/Entity/User.php - - - message: '#^Property App\\Entity\\User\:\:\$roles type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Entity/User.php - - message: '#^Property App\\Entity\\Version\:\:\$isBillable type mapping mismatch\: property can contain bool\|null but database expects bool\.$#' identifier: doctrine.columnType @@ -450,54 +414,6 @@ parameters: count: 1 path: src/Entity/Worklog.php - - - message: '#^Method App\\Form\\ClientType\:\:getVersionOptions\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Form/ClientType.php - - - - message: '#^Method App\\Message\\LeantimeUpdateMessage\:\:__construct\(\) has parameter \$projectTrackerProjectIds with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Message/LeantimeUpdateMessage.php - - - - message: '#^Method App\\Model\\DashboardData\:\:__construct\(\) has parameter \$monthStatuses with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Model/DashboardData.php - - - - message: '#^Method App\\Model\\DashboardData\:\:__construct\(\) has parameter \$weekStatuses with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Model/DashboardData.php - - - - message: '#^Method App\\Model\\DataProvider\\DataProviderIssueData\:\:__construct\(\) has parameter \$epics with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Model/DataProvider/DataProviderIssueData.php - - - - message: '#^Property App\\Model\\Invoices\\InvoiceEntryWorklogsFilterData\:\:\$epics type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Model/Invoices/InvoiceEntryWorklogsFilterData.php - - - - message: '#^Method App\\Model\\Invoices\\PagedResult\:\:__construct\(\) has parameter \$items with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Model/Invoices/PagedResult.php - - - - message: '#^Property App\\Model\\Reports\\BillableUnbilledHoursReportData\:\:\$projectTotals type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Model/Reports/BillableUnbilledHoursReportData.php - - message: '#^Constructor of class App\\Model\\Reports\\ForecastReportWorklogData has an unused parameter \$description\.$#' identifier: constructor.unusedParameter @@ -570,384 +486,12 @@ parameters: count: 1 path: src/Model/Reports/InvoicingRateReportData.php - - - message: '#^Property App\\Model\\Reports\\InvoicingRateReportWorker\:\:\$dataByPeriod type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Model/Reports/InvoicingRateReportWorker.php - - - - message: '#^Property App\\Model\\Reports\\InvoicingRateReportWorker\:\:\$projectData type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Model/Reports/InvoicingRateReportWorker.php - - message: '#^Class App\\Model\\Reports\\WorkloadReportData has an uninitialized readonly property \$id\. Assign it in the constructor\.$#' identifier: property.uninitializedReadonly count: 1 path: src/Model/Reports/WorkloadReportData.php - - - message: '#^Class App\\Repository\\AccountRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/AccountRepository.php - - - - message: '#^Class App\\Repository\\AccountRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/AccountRepository.php - - - - message: '#^Class App\\Repository\\AccountRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/AccountRepository.php - - - - message: '#^Class App\\Repository\\AccountRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/AccountRepository.php - - - - message: '#^Method App\\Repository\\AccountRepository\:\:getAllChoices\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/AccountRepository.php - - - - message: '#^Class App\\Repository\\ClientRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ClientRepository.php - - - - message: '#^Class App\\Repository\\ClientRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ClientRepository.php - - - - message: '#^Class App\\Repository\\ClientRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ClientRepository.php - - - - message: '#^Class App\\Repository\\ClientRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ClientRepository.php - - - - message: '#^Class App\\Repository\\DataProviderRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/DataProviderRepository.php - - - - message: '#^Class App\\Repository\\DataProviderRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/DataProviderRepository.php - - - - message: '#^Class App\\Repository\\DataProviderRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/DataProviderRepository.php - - - - message: '#^Class App\\Repository\\DataProviderRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/DataProviderRepository.php - - - - message: '#^Class App\\Repository\\EpicRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/EpicRepository.php - - - - message: '#^Class App\\Repository\\EpicRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/EpicRepository.php - - - - message: '#^Class App\\Repository\\EpicRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/EpicRepository.php - - - - message: '#^Class App\\Repository\\EpicRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/EpicRepository.php - - - - message: '#^Class App\\Repository\\InvoiceEntryRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceEntryRepository.php - - - - message: '#^Class App\\Repository\\InvoiceEntryRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceEntryRepository.php - - - - message: '#^Class App\\Repository\\InvoiceEntryRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceEntryRepository.php - - - - message: '#^Class App\\Repository\\InvoiceEntryRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceEntryRepository.php - - - - message: '#^Class App\\Repository\\InvoiceRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceRepository.php - - - - message: '#^Class App\\Repository\\InvoiceRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceRepository.php - - - - message: '#^Class App\\Repository\\InvoiceRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceRepository.php - - - - message: '#^Class App\\Repository\\InvoiceRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceRepository.php - - - - message: '#^Method App\\Repository\\InvoiceRepository\:\:getByRecordedDateBetween\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/InvoiceRepository.php - - - - message: '#^Class App\\Repository\\IssueProductRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueProductRepository.php - - - - message: '#^Class App\\Repository\\IssueProductRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueProductRepository.php - - - - message: '#^Class App\\Repository\\IssueProductRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueProductRepository.php - - - - message: '#^Class App\\Repository\\IssueProductRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueProductRepository.php - - - - message: '#^Class App\\Repository\\IssueRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Class App\\Repository\\IssueRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Class App\\Repository\\IssueRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Class App\\Repository\\IssueRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Method App\\Repository\\IssueRepository\:\:findEpicOptionsByProject\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Method App\\Repository\\IssueRepository\:\:findIssuesInDateRange\(\) has parameter \$projects with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Method App\\Repository\\IssueRepository\:\:findIssuesInDateRange\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Method App\\Repository\\IssueRepository\:\:issuesContainingVersion\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Class App\\Repository\\ProductRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProductRepository.php - - - - message: '#^Class App\\Repository\\ProductRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProductRepository.php - - - - message: '#^Class App\\Repository\\ProductRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProductRepository.php - - - - message: '#^Class App\\Repository\\ProductRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProductRepository.php - - - - message: '#^Class App\\Repository\\ProjectBillingRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectBillingRepository.php - - - - message: '#^Class App\\Repository\\ProjectBillingRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectBillingRepository.php - - - - message: '#^Class App\\Repository\\ProjectBillingRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectBillingRepository.php - - - - message: '#^Class App\\Repository\\ProjectBillingRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectBillingRepository.php - - - - message: '#^Class App\\Repository\\ProjectRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectRepository.php - - - - message: '#^Class App\\Repository\\ProjectRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectRepository.php - - - - message: '#^Class App\\Repository\\ProjectRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectRepository.php - - - - message: '#^Class App\\Repository\\ProjectRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectRepository.php - - - - message: '#^Class App\\Repository\\ProjectVersionBudgetRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectVersionBudgetRepository.php - - - - message: '#^Class App\\Repository\\ProjectVersionBudgetRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectVersionBudgetRepository.php - - - - message: '#^Class App\\Repository\\ProjectVersionBudgetRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectVersionBudgetRepository.php - - - - message: '#^Class App\\Repository\\ProjectVersionBudgetRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectVersionBudgetRepository.php - - - - message: '#^Method App\\Repository\\ServiceAgreementRepository\:\:getApiServiceAgreements\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ServiceAgreementRepository.php - - - - message: '#^Class App\\Repository\\SubscriptionRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Class App\\Repository\\SubscriptionRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Class App\\Repository\\SubscriptionRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Class App\\Repository\\SubscriptionRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/SubscriptionRepository.php - - - - message: '#^Method App\\Repository\\SubscriptionRepository\:\:getFilteredData\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/SubscriptionRepository.php - - message: '#^Property App\\Repository\\SubscriptionRepository\:\:\$paginator is never read, only written\.$#' identifier: property.onlyWritten @@ -955,220 +499,28 @@ parameters: path: src/Repository/SubscriptionRepository.php - - message: '#^Class App\\Repository\\UserRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/UserRepository.php - - - - message: '#^Class App\\Repository\\UserRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/UserRepository.php - - - - message: '#^Class App\\Repository\\UserRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/UserRepository.php - - - - message: '#^Class App\\Repository\\UserRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/UserRepository.php - - - - message: '#^Class App\\Repository\\VersionRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/VersionRepository.php - - - - message: '#^Class App\\Repository\\VersionRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/VersionRepository.php - - - - message: '#^Class App\\Repository\\VersionRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/VersionRepository.php - - - - message: '#^Class App\\Repository\\VersionRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/VersionRepository.php - - - - message: '#^Class App\\Repository\\WorkerRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorkerRepository.php - - - - message: '#^Class App\\Repository\\WorkerRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorkerRepository.php - - - - message: '#^Class App\\Repository\\WorkerRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorkerRepository.php - - - - message: '#^Class App\\Repository\\WorkerRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorkerRepository.php - - - - message: '#^Method App\\Repository\\WorkerRepository\:\:findAllIncludedInReports\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorkerRepository.php - - - - message: '#^Class App\\Repository\\WorklogRepository has PHPDoc tag @method for method findBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorklogRepository.php - - - - message: '#^Class App\\Repository\\WorklogRepository has PHPDoc tag @method for method findBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorklogRepository.php - - - - message: '#^Class App\\Repository\\WorklogRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorklogRepository.php - - - - message: '#^Class App\\Repository\\WorklogRepository has PHPDoc tag @method for method findOneBy\(\) parameter \#2 \$orderBy with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorklogRepository.php - - - - message: '#^Method App\\Repository\\WorklogRepository\:\:findBillableWorklogsByWorkerAndDateRange\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorklogRepository.php - - - - message: '#^Method App\\Repository\\WorklogRepository\:\:findByFilterData\(\) return type has no value type specified in iterable type iterable\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorklogRepository.php - - - - message: '#^Method App\\Repository\\WorklogRepository\:\:getTimeSpentByWorkerInWeekRange\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorklogRepository.php - - - - message: '#^Method App\\Repository\\WorklogRepository\:\:getWorklogsAttachedToInvoiceInDateRange\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/WorklogRepository.php - - - - message: '#^Method App\\Service\\BillingService\:\:exportInvoicesToSpreadsheet\(\) has parameter \$invoiceIds with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/BillingService.php - - - - message: '#^Method App\\Service\\BillingService\:\:generateSpreadsheetCsvResponse\(\) has parameter \$ids with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/BillingService.php - - - - message: '#^Method App\\Service\\BillingService\:\:generateSpreadsheetHtml\(\) has parameter \$ids with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/BillingService.php - - - - message: '#^Method App\\Service\\BillingService\:\:getInvoiceRecordableErrors\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/BillingService.php - - - - message: '#^Method App\\Service\\ClientHelper\:\:__construct\(\) has parameter \$options with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/ClientHelper.php - - - - message: '#^Method App\\Service\\DanishHolidayHelper\:\:__construct\(\) has parameter \$nonWorkdays with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/DanishHolidayHelper.php - - - - message: '#^Method App\\Service\\DanishHolidayHelper\:\:buildNames\(\) has parameter \$days with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/DanishHolidayHelper.php - - - - message: '#^Method App\\Service\\DanishHolidayHelper\:\:buildNames\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/DanishHolidayHelper.php - - - - message: '#^Method App\\Service\\DanishHolidayHelper\:\:getBankHolidayNames\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/DanishHolidayHelper.php - - - - message: '#^Method App\\Service\\DanishHolidayHelper\:\:getBankHolidays\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/DanishHolidayHelper.php - - - - message: '#^Method App\\Service\\DanishHolidayHelper\:\:getHolidayNames\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/DanishHolidayHelper.php - - - - message: '#^Property App\\Service\\DanishHolidayHelper\:\:\$bankHolidayNames type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject count: 1 - path: src/Service/DanishHolidayHelper.php + path: src/Service/BillableUnbilledHoursReportService.php - - message: '#^Property App\\Service\\DanishHolidayHelper\:\:\$bankHolidays type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot call method getName\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject count: 1 - path: src/Service/DanishHolidayHelper.php + path: src/Service/BillableUnbilledHoursReportService.php - - message: '#^Property App\\Service\\DanishHolidayHelper\:\:\$holidayNames type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot call method getName\(\) on App\\Entity\\Project\|null\.$#' + identifier: method.nonObject count: 1 - path: src/Service/DanishHolidayHelper.php + path: src/Service/BillableUnbilledHoursReportService.php - - message: '#^Property App\\Service\\DanishHolidayHelper\:\:\$holidays type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot call method getProjectTrackerId\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject count: 1 - path: src/Service/DanishHolidayHelper.php + path: src/Service/BillableUnbilledHoursReportService.php - message: '#^Static property App\\Service\\DanishHolidayHelper\:\:\$instance \(App\\Service\\DanishHolidayHelper\) in empty\(\) is not falsy\.$#' @@ -1176,18 +528,6 @@ parameters: count: 1 path: src/Service/DanishHolidayHelper.php - - - message: '#^Method App\\Service\\DashboardService\:\:getMonthsToDate\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/DashboardService.php - - - - message: '#^Method App\\Service\\DashboardService\:\:getWeeksToDate\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/DashboardService.php - - message: '#^Parameter \#2 \$timestamp of function date expects int\|null, int\|false given\.$#' identifier: argument.type @@ -1213,34 +553,40 @@ parameters: path: src/Service/DateTimeHelper.php - - message: '#^Method App\\Service\\DateTimeHelper\:\:getFirstAndLastDateOfMonth\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Call to an undefined method App\\Entity\\Epic\:\:getName\(\)\.$#' + identifier: method.notFound count: 1 - path: src/Service/DateTimeHelper.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\DateTimeHelper\:\:getFirstAndLastDateOfQuarter\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot call method getEpics\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject + count: 2 + path: src/Service/ForecastReportService.php + + - + message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' + identifier: method.nonObject count: 1 - path: src/Service/DateTimeHelper.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\DateTimeHelper\:\:getFirstAndLastDateOfWeek\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject count: 1 - path: src/Service/DateTimeHelper.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\DateTimeHelper\:\:getFirstAndLastDateOfYear\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot call method getProjectTrackerKey\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject count: 1 - path: src/Service/DateTimeHelper.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\DateTimeHelper\:\:getWeeksOfYear\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Cannot call method getVersions\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject count: 1 - path: src/Service/DateTimeHelper.php + path: src/Service/ForecastReportService.php - message: '#^Negated boolean expression is always false\.$#' @@ -1249,62 +595,74 @@ parameters: path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\HourReportService\:\:processTimesheetsData\(\) has parameter \$worklogs with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$projectId of class App\\Model\\Reports\\ForecastReportProjectData constructor expects string, int\\|int\<1, max\> given\.$#' + identifier: argument.type count: 1 - path: src/Service/HourReportService.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\HourReportService\:\:processTimesheetsData\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#2 \$array of function array_map expects array, Doctrine\\Common\\Collections\\Collection\ given\.$#' + identifier: argument.type count: 1 - path: src/Service/HourReportService.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\InvoiceEntryHelper\:\:__construct\(\) has parameter \$options with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Property App\\Model\\Reports\\ForecastReportIssueData\:\:\$issueId \(string\) does not accept string\|null\.$#' + identifier: assign.propertyType count: 1 - path: src/Service/InvoiceEntryHelper.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\InvoiceEntryHelper\:\:getAccounts\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Property App\\Model\\Reports\\ForecastReportIssueData\:\:\$issueLink \(string\) does not accept string\|null\.$#' + identifier: assign.propertyType count: 1 - path: src/Service/InvoiceEntryHelper.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\InvoiceEntryHelper\:\:resolveOptions\(\) has parameter \$options with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Property App\\Model\\Reports\\ForecastReportIssueVersionData\:\:\$worklogs \(array\\) does not accept array\\.$#' + identifier: assign.propertyType count: 1 - path: src/Service/InvoiceEntryHelper.php + path: src/Service/ForecastReportService.php - - message: '#^Method App\\Service\\InvoiceEntryHelper\:\:resolveOptions\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Property App\\Model\\Reports\\ForecastReportWorklogData\:\:\$description \(string\) does not accept string\|null\.$#' + identifier: assign.propertyType count: 1 - path: src/Service/InvoiceEntryHelper.php + path: src/Service/ForecastReportService.php - - message: '#^Property App\\Service\\InvoiceEntryHelper\:\:\$options type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/InvoiceEntryHelper.php + message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject + count: 2 + path: src/Service/InvoicingRateReportService.php - - message: '#^Method App\\Service\\InvoicingRateReportService\:\:getDatesOfPeriod\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 + message: '#^Cannot call method getName\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject + count: 2 + path: src/Service/InvoicingRateReportService.php + + - + message: '#^Cannot call method getName\(\) on App\\Entity\\Project\|null\.$#' + identifier: method.nonObject + count: 2 + path: src/Service/InvoicingRateReportService.php + + - + message: '#^Cannot call method getProjectTrackerId\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject + count: 2 path: src/Service/InvoicingRateReportService.php - - message: '#^Method App\\Service\\InvoicingRateReportService\:\:getPeriods\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$key of method Doctrine\\Common\\Collections\\ArrayCollection\\:\:set\(\) expects string, int given\.$#' + identifier: argument.type count: 1 path: src/Service/InvoicingRateReportService.php - - message: '#^Method App\\Service\\InvoicingRateReportService\:\:getWorklogs\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#2 \$value of method Doctrine\\Common\\Collections\\ArrayCollection\\>\:\:set\(\) expects array\, array\\> given\.$#' + identifier: argument.type count: 1 path: src/Service/InvoicingRateReportService.php @@ -1441,89 +799,35 @@ parameters: path: src/Service/LeantimeApiService.php - - message: '#^Method App\\Service\\LeantimeApiService\:\:fetchFromLeantime\(\) has parameter \$params with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Method App\\Service\\LeantimeApiService\:\:getEnabledLeantimeDataProviders\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Method App\\Service\\LeantimeApiService\:\:post\(\) has parameter \$body with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$dataProviderId of class App\\Message\\LeantimeDeleteMessage constructor expects int, int\|null given\.$#' + identifier: argument.type count: 1 path: src/Service/LeantimeApiService.php - - message: '#^Method App\\Service\\LeantimeApiService\:\:updateAsJob\(\) has parameter \$projectTrackerProjectIds with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#4 \$dataProviderId of class App\\Message\\LeantimeUpdateMessage constructor expects int, int\|null given\.$#' + identifier: argument.type count: 1 path: src/Service/LeantimeApiService.php - - message: '#^Method App\\Service\\ManagementReportService\:\:generateSpreadsheetCsvResponse\(\) has parameter \$groupedInvoices with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/ManagementReportService.php - - - - message: '#^Method App\\Service\\PlanningService\:\:getAssigneeData\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Method App\\Entity\\Issue\:\:getHoursRemaining\(\) invoked with 1 parameter, 0 required\.$#' + identifier: arguments.count count: 1 path: src/Service/PlanningService.php - - message: '#^Method App\\Service\\PlanningService\:\:getOrCreateAssignee\(\) has parameter \$assigneeData with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/PlanningService.php - - - - message: '#^Method App\\Service\\PlanningService\:\:processIssuesForWeek\(\) has parameter \$issues with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/PlanningService.php - - - - message: '#^Method App\\Service\\PlanningService\:\:sortIssuesByWeek\(\) has parameter \$allIssues with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/PlanningService.php - - - - message: '#^Method App\\Service\\PlanningService\:\:sortIssuesByWeek\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#2 \$week of method App\\Service\\PlanningService\:\:processIssuesForWeek\(\) expects int, int\|string given\.$#' + identifier: argument.type count: 1 path: src/Service/PlanningService.php - - - message: '#^Method App\\Service\\ProjectBillingService\:\:getIssuesNotIncludedInProjectBilling\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/ProjectBillingService.php - - message: '#^Call to method App\\Repository\\VersionRepository\:\:findOneBy\(\) \- entity App\\Entity\\Version does not have a field named \$versionId\.$#' identifier: doctrine.findOneByArgument count: 1 path: src/Service/SubscriptionHandlerService.php - - - message: '#^Method App\\Service\\SubscriptionHandlerService\:\:prepareMailData\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/SubscriptionHandlerService.php - - - - message: '#^Method App\\Service\\SubscriptionHandlerService\:\:sendNotification\(\) has parameter \$notification with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/SubscriptionHandlerService.php - - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable @@ -1531,53 +835,11 @@ parameters: path: src/Service/SubscriptionHandlerService.php - - message: '#^Method App\\Service\\WorkloadReportService\:\:getDatesOfPeriod\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/WorkloadReportService.php - - - - message: '#^Method App\\Service\\WorkloadReportService\:\:getPeriods\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Service/WorkloadReportService.php - - - - message: '#^Method App\\Service\\WorkloadReportService\:\:getWorklogs\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue + message: '#^Parameter \#1 \$key of method Doctrine\\Common\\Collections\\ArrayCollection\\:\:set\(\) expects string, int given\.$#' + identifier: argument.type count: 1 path: src/Service/WorkloadReportService.php - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:dataEaster\(\) return type has no value type specified in iterable type iterable\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:dataIsBankHoliday\(\) return type has no value type specified in iterable type iterable\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:dataNextBankDay\(\) return type has no value type specified in iterable type iterable\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:dataNextBankDay30\(\) return type has no value type specified in iterable type iterable\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DanishHolidayHelperTest\:\:dataNextNonHoliday\(\) return type has no value type specified in iterable type iterable\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DanishHolidayHelperTest.php - - message: '#^Parameter \#1 \$expected of method App\\Tests\\Service\\DanishHolidayHelperTest\:\:assertSameDate\(\) expects DateTimeInterface, DateTimeInterface\|null given\.$#' identifier: argument.type @@ -1626,60 +888,6 @@ parameters: count: 1 path: tests/Service/DataProviderServiceTest.php - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:monthNameDataProvider\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:monthYearProvider\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:testGetFirstAndLastDateOfMonth\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:testGetFirstAndLastDateOfWeek\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:testGetFirstAndLastDateOfYear\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:testGetWeeksOfYear\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:weekYearProvider\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:weeksOfYearProvider\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - - - message: '#^Method App\\Tests\\Service\\DateTimeHelperTest\:\:yearProvider\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Service/DateTimeHelperTest.php - - message: '#^Call to an undefined method object\:\:clear\(\)\.$#' identifier: method.notFound diff --git a/src/Command/SubscriptionHandlerCommand.php b/src/Command/SubscriptionHandlerCommand.php index 64d78a4df..6578060a0 100644 --- a/src/Command/SubscriptionHandlerCommand.php +++ b/src/Command/SubscriptionHandlerCommand.php @@ -121,7 +121,7 @@ private function handleSubscription(Subscription $subscription, \DateTime $fromD * * @param \DateTime $dateNow the current date * - * @return array an array containing the start and end dates of the last quarter + * @return array{fromDate: \DateTime, toDate: \DateTime} an array containing the start and end dates of the last quarter */ private function getLastQuarter(\DateTime $dateNow): array { diff --git a/src/Controller/IssueController.php b/src/Controller/IssueController.php index 372796b9d..02afef502 100644 --- a/src/Controller/IssueController.php +++ b/src/Controller/IssueController.php @@ -21,6 +21,9 @@ #[IsGranted('ROLE_PRODUCT_MANAGER')] class IssueController extends AbstractController { + /** + * @param array $options + */ public function __construct( private readonly array $options, ) { diff --git a/src/Controller/ManagementReportController.php b/src/Controller/ManagementReportController.php index 2e5302523..30595abfb 100644 --- a/src/Controller/ManagementReportController.php +++ b/src/Controller/ManagementReportController.php @@ -88,6 +88,11 @@ public function export(Request $request, ManagementReportService $managementRepo return $managementReportService->generateSpreadsheetCsvResponse($this->createGroupedInvoices($invoices), $dateInterval); } + /** + * @param array $invoices + * + * @return array> + */ private function createGroupedInvoices(array $invoices): array { $groupedInvoices = []; @@ -125,6 +130,8 @@ private function createGroupedInvoices(array $invoices): array /** * @param array{dateFrom: string, dateTo: string} $dateInterval * + * @return array + * * @throws \Doctrine\ORM\Exception\NotSupported * @throws \Exception */ diff --git a/src/Controller/PlanningController.php b/src/Controller/PlanningController.php index 29899a980..6dd327527 100644 --- a/src/Controller/PlanningController.php +++ b/src/Controller/PlanningController.php @@ -112,6 +112,9 @@ public function syncAllIssues(Request $request, LeantimeApiService $leantimeApiS return new Response('Sync done.', 200); } + /** + * @param array $data + */ private function createResponse(string $mode, array $data): Response { return $this->render('planning/planning.html.twig', [ @@ -124,6 +127,8 @@ private function createResponse(string $mode, array $data): Response } /** + * @return array + * * @throws \Exception */ private function preparePlanningData(Request $request, bool $holidayPlanning = false): array diff --git a/src/Controller/SubscriptionController.php b/src/Controller/SubscriptionController.php index 938bc4014..54185bf20 100644 --- a/src/Controller/SubscriptionController.php +++ b/src/Controller/SubscriptionController.php @@ -155,6 +155,9 @@ private function subscriptionHandler(string $userEmail, string $subscriptionType return new JsonResponse(['action' => 'subscribed', 'frequencies' => $frequencies], 200); } + /** + * @param array $subscriptions + */ private function getFrequencies(array $subscriptions): string { $frequencies = []; diff --git a/src/Entity/ServiceAgreement.php b/src/Entity/ServiceAgreement.php index f2834f67e..7dfff77ff 100644 --- a/src/Entity/ServiceAgreement.php +++ b/src/Entity/ServiceAgreement.php @@ -48,6 +48,7 @@ class ServiceAgreement extends AbstractBaseEntity #[ORM\Column] private ?bool $isActive = null; + /** @var array */ #[ORM\Column(type: Types::JSON)] private array $systemOwnerNotices = []; diff --git a/src/Entity/Subscription.php b/src/Entity/Subscription.php index 1c40d2c0e..62aad81c3 100644 --- a/src/Entity/Subscription.php +++ b/src/Entity/Subscription.php @@ -17,6 +17,7 @@ class Subscription extends AbstractBaseEntity #[ORM\Column(type: 'string', nullable: true, enumType: SubscriptionSubjectEnum::class)] private ?SubscriptionSubjectEnum $subject = null; + /** @var array|null */ #[ORM\Column(type: 'json', nullable: true)] private ?array $urlParams = null; @@ -55,11 +56,17 @@ public function setSubject(SubscriptionSubjectEnum $subject): self return $this; } + /** + * @return array|null + */ public function getUrlParams(): ?array { return $this->urlParams; } + /** + * @param array|null $urlParams + */ public function setUrlParams(?array $urlParams): self { $this->urlParams = $urlParams; diff --git a/src/Entity/User.php b/src/Entity/User.php index 89df9b7a6..c1a47fc31 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -17,6 +17,7 @@ class User implements UserInterface #[ORM\Column(length: 180, unique: true)] private ?string $email = null; + /** @var array */ #[ORM\Column] private array $roles = []; @@ -64,6 +65,9 @@ public function getRoles(): array return array_unique($roles); } + /** + * @param array $roles + */ public function setRoles(array $roles): self { $this->roles = $roles; diff --git a/src/Form/ClientType.php b/src/Form/ClientType.php index 6842dd2ee..7deaae7c3 100644 --- a/src/Form/ClientType.php +++ b/src/Form/ClientType.php @@ -109,6 +109,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]); } + /** + * @return array + */ private function getVersionOptions(?Client $client): array { $versions = $this->versionRepository->findAll(); diff --git a/src/Message/LeantimeUpdateMessage.php b/src/Message/LeantimeUpdateMessage.php index ffffd97ee..1d6518164 100644 --- a/src/Message/LeantimeUpdateMessage.php +++ b/src/Message/LeantimeUpdateMessage.php @@ -4,6 +4,9 @@ readonly class LeantimeUpdateMessage { + /** + * @param array|null $projectTrackerProjectIds + */ public function __construct( public string $className, public int $start, diff --git a/src/Model/DashboardData.php b/src/Model/DashboardData.php index d32ddf17e..9ff9b1fd3 100644 --- a/src/Model/DashboardData.php +++ b/src/Model/DashboardData.php @@ -4,6 +4,10 @@ class DashboardData { + /** + * @param array $monthStatuses + * @param array $weekStatuses + */ public function __construct( public readonly float $workHours, public readonly int $year, diff --git a/src/Model/DataProvider/DataProviderIssueData.php b/src/Model/DataProvider/DataProviderIssueData.php index 51bde7ab7..c3dff8f89 100644 --- a/src/Model/DataProvider/DataProviderIssueData.php +++ b/src/Model/DataProvider/DataProviderIssueData.php @@ -6,6 +6,9 @@ class DataProviderIssueData { + /** + * @param array $epics + */ public function __construct( public string $projectTrackerId, public int $dataProviderId, diff --git a/src/Model/Invoices/InvoiceEntryWorklogsFilterData.php b/src/Model/Invoices/InvoiceEntryWorklogsFilterData.php index 4848bd2f6..5579c6f15 100644 --- a/src/Model/Invoices/InvoiceEntryWorklogsFilterData.php +++ b/src/Model/Invoices/InvoiceEntryWorklogsFilterData.php @@ -12,5 +12,6 @@ class InvoiceEntryWorklogsFilterData public ?string $worker = null; public ?Version $version = null; public ?bool $onlyAvailable = true; + /** @var array|null */ public ?array $epics = []; } diff --git a/src/Model/Invoices/PagedResult.php b/src/Model/Invoices/PagedResult.php index c9db977cd..b67b54598 100644 --- a/src/Model/Invoices/PagedResult.php +++ b/src/Model/Invoices/PagedResult.php @@ -4,6 +4,9 @@ class PagedResult { + /** + * @param array $items + */ public function __construct( public readonly array $items, public readonly int $startAt, diff --git a/src/Model/Reports/BillableUnbilledHoursReportData.php b/src/Model/Reports/BillableUnbilledHoursReportData.php index 541abbc42..01fcbfbea 100644 --- a/src/Model/Reports/BillableUnbilledHoursReportData.php +++ b/src/Model/Reports/BillableUnbilledHoursReportData.php @@ -10,6 +10,7 @@ class BillableUnbilledHoursReportData /** @var ArrayCollection */ public ArrayCollection $projectData; + /** @var array */ public array $projectTotals; public int|float $totalHoursForAllProjects; diff --git a/src/Model/Reports/InvoicingRateReportWorker.php b/src/Model/Reports/InvoicingRateReportWorker.php index 2d72ec2f1..8099507e8 100644 --- a/src/Model/Reports/InvoicingRateReportWorker.php +++ b/src/Model/Reports/InvoicingRateReportWorker.php @@ -11,10 +11,10 @@ class InvoicingRateReportWorker public float $average; - /** @var ArrayCollection */ + /** @var ArrayCollection> */ public ArrayCollection $dataByPeriod; - /** @var ArrayCollection */ + /** @var ArrayCollection> */ public ArrayCollection $projectData; public function __construct(Worker $worker) diff --git a/src/Repository/AccountRepository.php b/src/Repository/AccountRepository.php index d7eeef9f4..a7c624e4f 100644 --- a/src/Repository/AccountRepository.php +++ b/src/Repository/AccountRepository.php @@ -11,11 +11,6 @@ /** * @extends ServiceEntityRepository - * - * @method Account|null find($id, $lockMode = null, $lockVersion = null) - * @method Account|null findOneBy(array $criteria, array $orderBy = null) - * @method Account[] findAll() - * @method Account[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class AccountRepository extends ServiceEntityRepository { @@ -42,6 +37,9 @@ public function remove(Account $entity, bool $flush = false): void } } + /** + * @return array + */ public function getAllChoices(): array { $accounts = $this->findAll(); diff --git a/src/Repository/ClientRepository.php b/src/Repository/ClientRepository.php index 86058011a..4f19f767b 100644 --- a/src/Repository/ClientRepository.php +++ b/src/Repository/ClientRepository.php @@ -11,11 +11,6 @@ /** * @extends ServiceEntityRepository - * - * @method Client|null find($id, $lockMode = null, $lockVersion = null) - * @method Client|null findOneBy(array $criteria, array $orderBy = null) - * @method Client[] findAll() - * @method Client[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class ClientRepository extends ServiceEntityRepository { diff --git a/src/Repository/DataProviderRepository.php b/src/Repository/DataProviderRepository.php index 9fdca1ea2..05b933aac 100644 --- a/src/Repository/DataProviderRepository.php +++ b/src/Repository/DataProviderRepository.php @@ -8,11 +8,6 @@ /** * @extends ServiceEntityRepository - * - * @method DataProvider|null find($id, $lockMode = null, $lockVersion = null) - * @method DataProvider|null findOneBy(array $criteria, array $orderBy = null) - * @method DataProvider[] findAll() - * @method DataProvider[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class DataProviderRepository extends ServiceEntityRepository { diff --git a/src/Repository/EpicRepository.php b/src/Repository/EpicRepository.php index e3e3dc5c2..9d1b7f9dd 100644 --- a/src/Repository/EpicRepository.php +++ b/src/Repository/EpicRepository.php @@ -8,11 +8,6 @@ /** * @extends ServiceEntityRepository - * - * @method Epic|null find($id, $lockMode = null, $lockVersion = null) - * @method Epic|null findOneBy(array $criteria, array $orderBy = null) - * @method Epic[] findAll() - * @method Epic[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class EpicRepository extends ServiceEntityRepository { diff --git a/src/Repository/InvoiceEntryRepository.php b/src/Repository/InvoiceEntryRepository.php index 9d2cd40c7..00828f604 100644 --- a/src/Repository/InvoiceEntryRepository.php +++ b/src/Repository/InvoiceEntryRepository.php @@ -8,11 +8,6 @@ /** * @extends ServiceEntityRepository - * - * @method InvoiceEntry|null find($id, $lockMode = null, $lockVersion = null) - * @method InvoiceEntry|null findOneBy(array $criteria, array $orderBy = null) - * @method findAll() - * @method findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class InvoiceEntryRepository extends ServiceEntityRepository { diff --git a/src/Repository/InvoiceRepository.php b/src/Repository/InvoiceRepository.php index 01644192b..26b4d3d33 100644 --- a/src/Repository/InvoiceRepository.php +++ b/src/Repository/InvoiceRepository.php @@ -11,11 +11,6 @@ /** * @extends ServiceEntityRepository - * - * @method Invoice|null find($id, $lockMode = null, $lockVersion = null) - * @method Invoice|null findOneBy(array $criteria, array $orderBy = null) - * @method findAll() - * @method findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class InvoiceRepository extends ServiceEntityRepository { @@ -42,6 +37,9 @@ public function remove(Invoice $entity, bool $flush = false): void } } + /** + * @return array + */ public function getByRecordedDateBetween(\DateTime $from, \DateTime $to): array { $parameters = [ diff --git a/src/Repository/IssueProductRepository.php b/src/Repository/IssueProductRepository.php index d3790a08f..5deef4115 100644 --- a/src/Repository/IssueProductRepository.php +++ b/src/Repository/IssueProductRepository.php @@ -8,11 +8,6 @@ /** * @extends ServiceEntityRepository - * - * @method IssueProduct|null find($id, $lockMode = null, $lockVersion = null) - * @method IssueProduct|null findOneBy(array $criteria, array $orderBy = null) - * @method IssueProduct[] findAll() - * @method IssueProduct[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class IssueProductRepository extends ServiceEntityRepository { diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index 6f80dd968..a4ad5bbf5 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -16,11 +16,6 @@ /** * @extends ServiceEntityRepository - * - * @method Issue|null find($id, $lockMode = null, $lockVersion = null) - * @method Issue|null findOneBy(array $criteria, array $orderBy = null) - * @method Issue[] findAll() - * @method Issue[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class IssueRepository extends ServiceEntityRepository { @@ -74,6 +69,9 @@ public function remove(Issue $entity, bool $flush = false): void } } + /** + * @return array + */ public function findEpicOptionsByProject(Project $project): array { $qb = $this->createQueryBuilder('issue'); @@ -111,6 +109,9 @@ public function getClosedIssuesFromInterval(Project $project, \DateTimeInterface return $qb->getQuery()->execute(); } + /** + * @return array + */ public function issuesContainingVersion(Version $version): array { $qb = $this->createQueryBuilder('issue') @@ -120,6 +121,11 @@ public function issuesContainingVersion(Version $version): array return $qb->getQuery()->getResult(); } + /** + * @param array|null $projects + * + * @return array + */ public function findIssuesInDateRange(string $startDate, string $endDate, ?WorkerGroup $group = null, ?array $projects = null): array { $qb = $this->createQueryBuilder('i') diff --git a/src/Repository/ProductRepository.php b/src/Repository/ProductRepository.php index ac51cd481..2238b3f4c 100644 --- a/src/Repository/ProductRepository.php +++ b/src/Repository/ProductRepository.php @@ -11,11 +11,6 @@ /** * @extends ServiceEntityRepository - * - * @method Product|null find($id, $lockMode = null, $lockVersion = null) - * @method Product|null findOneBy(array $criteria, array $orderBy = null) - * @method Product[] findAll() - * @method Product[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class ProductRepository extends ServiceEntityRepository { diff --git a/src/Repository/ProjectBillingRepository.php b/src/Repository/ProjectBillingRepository.php index 443f19a9e..93a5671c9 100644 --- a/src/Repository/ProjectBillingRepository.php +++ b/src/Repository/ProjectBillingRepository.php @@ -11,11 +11,6 @@ /** * @extends ServiceEntityRepository - * - * @method ProjectBilling|null find($id, $lockMode = null, $lockVersion = null) - * @method ProjectBilling|null findOneBy(array $criteria, array $orderBy = null) - * @method ProjectBilling[] findAll() - * @method ProjectBilling[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class ProjectBillingRepository extends ServiceEntityRepository { diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index 963f955ba..3761988b0 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -12,11 +12,6 @@ /** * @extends ServiceEntityRepository - * - * @method Project|null find($id, $lockMode = null, $lockVersion = null) - * @method Project|null findOneBy(array $criteria, array $orderBy = null) - * @method findAll() - * @method findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class ProjectRepository extends ServiceEntityRepository { diff --git a/src/Repository/ProjectVersionBudgetRepository.php b/src/Repository/ProjectVersionBudgetRepository.php index 0ada3de84..ac83bf1db 100644 --- a/src/Repository/ProjectVersionBudgetRepository.php +++ b/src/Repository/ProjectVersionBudgetRepository.php @@ -8,11 +8,6 @@ /** * @extends ServiceEntityRepository - * - * @method ProjectVersionBudget|null find($id, $lockMode = null, $lockVersion = null) - * @method ProjectVersionBudget|null findOneBy(array $criteria, array $orderBy = null) - * @method findAll() - * @method findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class ProjectVersionBudgetRepository extends ServiceEntityRepository { diff --git a/src/Repository/ServiceAgreementRepository.php b/src/Repository/ServiceAgreementRepository.php index 40a06e4f5..8ea0f09dc 100644 --- a/src/Repository/ServiceAgreementRepository.php +++ b/src/Repository/ServiceAgreementRepository.php @@ -77,6 +77,8 @@ public function getFilteredPagination(ServiceAgreementFilterData $serviceAgreeme /** * Retrieves a list of API service agreements along with their associated cybersecurity agreements. + * + * @return array> */ public function getApiServiceAgreements(): array { diff --git a/src/Repository/SubscriptionRepository.php b/src/Repository/SubscriptionRepository.php index f9a8e83a7..1f8f8b8da 100644 --- a/src/Repository/SubscriptionRepository.php +++ b/src/Repository/SubscriptionRepository.php @@ -10,12 +10,7 @@ /** * @extends ServiceEntityRepository - * - * @method Subscription|null find($id, $lockMode = null, $lockVersion = null) - * @method Subscription|null findOneBy(array $criteria, array $orderBy = null) - * @method Subscription[] findAll() - * @method Subscription[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) - * */ + */ class SubscriptionRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry, private readonly PaginatorInterface $paginator) @@ -92,6 +87,9 @@ public function findOneByCustom(string $email, string $subscriptionType, array $ * Due to the way searching is implemented in the controller, * the repository does not return a paginated element. */ + /** + * @return array + */ public function getFilteredData(string $email): array { $qb = $this->createQueryBuilder('subscription') diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 4e249716e..583d25d4d 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -8,11 +8,6 @@ /** * @extends ServiceEntityRepository - * - * @method User|null find($id, $lockMode = null, $lockVersion = null) - * @method User|null findOneBy(array $criteria, array $orderBy = null) - * @method User[] findAll() - * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class UserRepository extends ServiceEntityRepository { diff --git a/src/Repository/VersionRepository.php b/src/Repository/VersionRepository.php index 19195d95c..1814e4517 100644 --- a/src/Repository/VersionRepository.php +++ b/src/Repository/VersionRepository.php @@ -8,11 +8,6 @@ /** * @extends ServiceEntityRepository - * - * @method Version|null find($id, $lockMode = null, $lockVersion = null) - * @method Version|null findOneBy(array $criteria, array $orderBy = null) - * @method Version[] findAll() - * @method Version[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class VersionRepository extends ServiceEntityRepository { diff --git a/src/Repository/WorkerRepository.php b/src/Repository/WorkerRepository.php index ca5c47f58..20e8d5810 100644 --- a/src/Repository/WorkerRepository.php +++ b/src/Repository/WorkerRepository.php @@ -8,11 +8,6 @@ /** * @extends ServiceEntityRepository - * - * @method Worker|null find($id, $lockMode = null, $lockVersion = null) - * @method Worker|null findOneBy(array $criteria, array $orderBy = null) - * @method Worker[] findAll() - * @method Worker[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class WorkerRepository extends ServiceEntityRepository { @@ -39,6 +34,9 @@ public function remove(Worker $entity, bool $flush = false): void } } + /** + * @return array + */ public function findAllIncludedInReports(): array { return $this->createQueryBuilder('w') diff --git a/src/Repository/WorklogRepository.php b/src/Repository/WorklogRepository.php index cba375809..b2ca816ea 100644 --- a/src/Repository/WorklogRepository.php +++ b/src/Repository/WorklogRepository.php @@ -15,11 +15,6 @@ /** * @extends ServiceEntityRepository - * - * @method Worklog|null find($id, $lockMode = null, $lockVersion = null) - * @method Worklog|null findOneBy(array $criteria, array $orderBy = null) - * @method findAll() - * @method findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class WorklogRepository extends ServiceEntityRepository { @@ -46,6 +41,9 @@ public function remove(Worklog $entity, bool $flush = false): void } } + /** + * @return iterable + */ public function findByFilterData(Project $project, InvoiceEntry $invoiceEntry, InvoiceEntryWorklogsFilterData $filterData): iterable { $qb = $this->createQueryBuilder('worklog'); @@ -138,7 +136,7 @@ public function findWorklogsByWorkerAndDateRange(string $workerIdentifier, \Date * @param \DateTimeInterface $to The ending date time * @param string $groupBy The function to group by, accepts 'week', 'month' and 'year' * - * @return array An array of results containing total time spent, week number, and worker, indexed by week/month/year number + * @return array> An array of results containing total time spent, week number, and worker, indexed by week/month/year number */ public function getTimeSpentByWorkerInWeekRange( string $workerEmail, @@ -191,6 +189,9 @@ public function getTimeSpentByWorkerInWeekRange( * * @return array an array of worklogs matching the specified criteria */ + /** + * @return array + */ public function findBillableWorklogsByWorkerAndDateRange(\DateTime $dateFrom, \DateTime $dateTo, ?string $workerIdentifier = null, mixed $isBilled = null): array { $nonBillableEpics = NonBillableEpicsEnum::getAsArray(); @@ -283,6 +284,8 @@ public function findBilledWorklogsByWorkerAndDateRange(string $workerIdentifier, } /** + * @return array{total_count: int, pages_count: float, current_page: int, page_size: int, paginator: Paginator} + * * @throws \Exception */ public function getWorklogsAttachedToInvoiceInDateRange(\DateTimeInterface $periodStart, \DateTimeInterface $periodEnd, int $page = 1, int $pageSize = 50): array diff --git a/src/Service/BillingService.php b/src/Service/BillingService.php index 9bfdb8c15..1086a62eb 100644 --- a/src/Service/BillingService.php +++ b/src/Service/BillingService.php @@ -125,6 +125,9 @@ public function recordInvoice(Invoice $invoice, string $confirmation = ConfirmDa $this->invoiceRepository->save($invoice, $flush); } + /** + * @return array + */ // TODO: Replace with exceptions. public function getInvoiceRecordableErrors(Invoice $invoice): array { @@ -156,7 +159,7 @@ public function getInvoiceRecordableErrors(Invoice $invoice): array /** * Create a spreadsheet response from an array of invoice ids. * - * @param array $ids array of invoice ids + * @param array $ids array of invoice ids * * @throws EconomicsException */ @@ -193,6 +196,8 @@ public function generateSpreadsheetCsvResponse(array $ids): Response /** * Create spreadsheet html from an array of invoice ids. * + * @param array $ids + * * @throws EconomicsException */ public function generateSpreadsheetHtml(array $ids): bool|string @@ -239,7 +244,7 @@ public function generateSpreadsheetHtml(array $ids): bool|string /** * Export the selected invoices (by id) to csv. * - * @param array $invoiceIds array of invoice ids that should be exported + * @param array $invoiceIds array of invoice ids that should be exported * * @throws EconomicsException */ diff --git a/src/Service/ClientHelper.php b/src/Service/ClientHelper.php index 77df86d7c..60398c163 100644 --- a/src/Service/ClientHelper.php +++ b/src/Service/ClientHelper.php @@ -6,6 +6,9 @@ class ClientHelper { + /** + * @param array $options + */ public function __construct( private readonly array $options, ) { diff --git a/src/Service/DanishHolidayHelper.php b/src/Service/DanishHolidayHelper.php index ebd5081a1..6a9216964 100644 --- a/src/Service/DanishHolidayHelper.php +++ b/src/Service/DanishHolidayHelper.php @@ -26,14 +26,21 @@ public static function getInstance(): self return self::$instance; } + /** + * @param array $nonWorkdays + */ private function __construct( private readonly array $nonWorkdays = [self::SATURDAY, self::SUNDAY]) { } + /** @var array> */ private array $holidays = []; + /** @var array> */ private array $holidayNames = []; + /** @var array> */ private array $bankHolidays = []; + /** @var array> */ private array $bankHolidayNames = []; /** @@ -67,6 +74,8 @@ public function getHolidays(int $year): array /** * Get holiday names indexed by formatted date. + * + * @return array */ public function getHolidayNames(int $year): array { @@ -81,6 +90,8 @@ public function getHolidayNames(int $year): array * Get bank holidays. * * @see https://www.nationalbanken.dk/da/vores-arbejde/stabile-priser-pengepolitik-og-dansk-oekonomi/banklukkedage + * + * @return array */ public function getBankHolidays(int $year): array { @@ -98,6 +109,9 @@ public function getBankHolidays(int $year): array return $this->bankHolidays[$year]; } + /** + * @return array + */ public function getBankHolidayNames(int $year): array { if (!isset($this->bankHolidayNames[$year])) { @@ -177,6 +191,11 @@ private function getYear(\DateTimeInterface $date): int return (int) $date->format('Y'); } + /** + * @param array $days + * + * @return array + */ private function buildNames(array $days): array { $names = []; diff --git a/src/Service/DashboardService.php b/src/Service/DashboardService.php index 465e84b60..eba7373e7 100644 --- a/src/Service/DashboardService.php +++ b/src/Service/DashboardService.php @@ -97,6 +97,9 @@ public function getUserDashboard(User $user, ?int $year = null): ?DashboardData return new DashboardData($yearStatus, $year, $weekNorm, $monthStatuses, $weekStatuses); } + /** + * @return array + */ private function getWeeksToDate(): array { $currentWeek = (int) date('W'); @@ -108,6 +111,9 @@ private function getWeeksToDate(): array return $weeksToDate; } + /** + * @return array + */ private function getMonthsToDate(): array { $currentMonth = (int) date('m'); diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index e34e5bb8e..0ab7d78f6 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -14,7 +14,7 @@ public function __construct( * @param int $weekNumber the week number for which to retrieve the dates * @param int $year the year for which to retrieve the dates * - * @return array an array containing the first and last date of the week + * @return array{dateFrom: \DateTime, dateTo: \DateTime} an array containing the first and last date of the week */ public function getFirstAndLastDateOfWeek(int $weekNumber, int $year): array { @@ -33,7 +33,7 @@ public function getFirstAndLastDateOfWeek(int $weekNumber, int $year): array * @param int $monthNumber the month number (1-12) * @param int $year the year * - * @return array an array containing the first and last date of the specified month and year + * @return array{dateFrom: \DateTime, dateTo: \DateTime} an array containing the first and last date of the specified month and year */ public function getFirstAndLastDateOfMonth(int $monthNumber, int $year): array { @@ -70,7 +70,7 @@ public function getWeekdaysBetween(\DateTime $dateFrom, \DateTime $dateTo): int * * @param int $year the year for which to retrieve the week numbers * - * @return array an array of week numbers + * @return array an array of week numbers */ public function getWeeksOfYear(int $year): array { @@ -112,7 +112,7 @@ public function getMonthName(int $monthNumber): string * * @param int $year the year * - * @return array an array containing the first and last date of the specified year + * @return array{dateFrom: \DateTime, dateTo: \DateTime} an array containing the first and last date of the specified year */ public function getFirstAndLastDateOfYear(int $year): array { @@ -131,7 +131,7 @@ public function getFirstAndLastDateOfYear(int $year): array * @param int $year the year * @param int $quarter the quarter (1-4) * - * @return array an array containing the first and last date of the specified quarter + * @return array{dateFrom: \DateTime, dateTo: \DateTime} an array containing the first and last date of the specified quarter */ public function getFirstAndLastDateOfQuarter(int $year, int $quarter): array { diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index 303e4d554..ea24fb2f2 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -95,6 +95,11 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? return $hourReportData; } + /** + * @param array $worklogs + * + * @return array{0: array, 1: float|int} + */ private function processTimesheetsData(array $worklogs, ?\DateTimeInterface $fromDate = null, ?\DateTimeInterface $toDate = null): array { $timesheets = []; diff --git a/src/Service/InvoiceEntryHelper.php b/src/Service/InvoiceEntryHelper.php index 66863d875..67ab5e8e3 100644 --- a/src/Service/InvoiceEntryHelper.php +++ b/src/Service/InvoiceEntryHelper.php @@ -9,8 +9,12 @@ class InvoiceEntryHelper { + /** @var array */ private readonly array $options; + /** + * @param array $options + */ public function __construct( array $options, ) { @@ -95,6 +99,9 @@ public function isEditable(InvoiceEntry $entry): bool || count($this->getAccountOptions(null)) > 1; } + /** + * @return array> + */ private function getAccounts(?string $account): array { $accounts = $this->options['accounts'] ?? []; @@ -109,6 +116,11 @@ private function getAccounts(?string $account): array return $accounts; } + /** + * @param array $options + * + * @return array + */ private function resolveOptions(array $options): array { return (new OptionsResolver()) diff --git a/src/Service/InvoicingRateReportService.php b/src/Service/InvoicingRateReportService.php index 3e3ef4c6f..5997ca7e6 100644 --- a/src/Service/InvoicingRateReportService.php +++ b/src/Service/InvoicingRateReportService.php @@ -197,7 +197,7 @@ private function getCurrentPeriodNumeric(PeriodTypeEnum $viewMode): int * @param int $year the year for the period * @param PeriodTypeEnum $viewMode the view mode to determine the dates of the period * - * @return array an array of dates for the given period + * @return array{dateFrom: \DateTime, dateTo: \DateTime} an array of dates for the given period */ private function getDatesOfPeriod(int $period, int $year, PeriodTypeEnum $viewMode): array { @@ -230,7 +230,7 @@ private function getReadablePeriod(int $period, PeriodTypeEnum $viewMode): strin * @param PeriodTypeEnum $viewMode the view mode to determine the periods * @param int $year the year containing the periods * - * @return array an array of periods + * @return array an array of periods */ private function getPeriods(PeriodTypeEnum $viewMode, int $year): array { @@ -247,7 +247,7 @@ private function getPeriods(PeriodTypeEnum $viewMode, int $year): array * @param InvoicingRateReportViewModeEnum $viewMode defines the view mode * @param string $workerIdentifier the worker's identifier * - * @return array the list of workloads matching the criteria defined by the parameters + * @return array> the list of workloads matching the criteria defined by the parameters */ private function getWorklogs(InvoicingRateReportViewModeEnum $viewMode, string $workerIdentifier, \DateTime $dateFrom, \DateTime $dateTo): array { diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index 334d10357..e4138e04e 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -148,6 +148,9 @@ public function deleteAsJob(int $dataProviderId, bool $asyncJobQueue = false, ?\ } } + /** + * @param array|null $projectTrackerProjectIds + */ public function updateAsJob(string $className, int $startId, int $limit, int $dataProviderId, ?array $projectTrackerProjectIds = null, bool $asyncJobQueue = false, ?\DateTimeInterface $modifiedAfter = null, bool $disableModifiedAtCheck = false): void { $dataProvider = $this->dataProviderRepository->find($dataProviderId); @@ -315,6 +318,9 @@ private function getWorkerUpsertFromResult(object $result, int $dataProviderId, ); } + /** + * @param array $params + */ private function fetchFromLeantime(DataProvider $dataProvider, string $type, array $params): object { $response = $this->post($dataProvider, $type, $params); @@ -322,6 +328,9 @@ private function fetchFromLeantime(DataProvider $dataProvider, string $type, arr return json_decode($response->getContent(), null, 512, JSON_THROW_ON_ERROR); } + /** + * @param array $body + */ private function post(DataProvider $dataProvider, string $path, array $body): ResponseInterface { return $this->httpClient->request('POST', $dataProvider->getUrl().$this::API_PATH_DATA.$path, [ @@ -354,6 +363,9 @@ private function convertStatusToEnum(string $statusString): IssueStatusEnum }; } + /** + * @return array + */ private function getEnabledLeantimeDataProviders(): array { return $this->dataProviderRepository->findBy(['class' => LeantimeApiService::class, 'enabled' => true]); diff --git a/src/Service/ManagementReportService.php b/src/Service/ManagementReportService.php index 93a3a8d70..f4da8cf42 100644 --- a/src/Service/ManagementReportService.php +++ b/src/Service/ManagementReportService.php @@ -17,6 +17,7 @@ public function __construct( } /** + * @param array> $groupedInvoices * @param array{dateFrom: string, dateTo: string} $dateInterval */ public function generateSpreadsheetCsvResponse(array $groupedInvoices, array $dateInterval): Response diff --git a/src/Service/PlanningService.php b/src/Service/PlanningService.php index c886f070d..d3b56106b 100644 --- a/src/Service/PlanningService.php +++ b/src/Service/PlanningService.php @@ -127,9 +127,9 @@ private function buildPlanningWeeks(PlanningData $planning, int $year, bool $pla /** * Sorts issues by week. * - * @param array $allIssues the array of all issues to sort + * @param array $allIssues the array of all issues to sort * - * @return array the sorted array of issues + * @return array> the sorted array of issues */ private function sortIssuesByWeek(array $allIssues): array { @@ -155,6 +155,9 @@ private function sortIssuesByWeek(array $allIssues): array return $weekIssues; } + /** + * @param array $issues + */ private function processIssuesForWeek(PlanningData $planning, int $week, array $issues, ?bool $holidayPlanning = false): void { foreach ($issues as $issueData) { @@ -210,6 +213,8 @@ private function processIssuesForWeek(PlanningData $planning, int $week, array $ /** * Get the assignee key and display name. + * + * @return array{key: string, displayName: string, weekNorm: int|float} */ private function getAssigneeData(IssueEntity $issue): array { @@ -239,7 +244,8 @@ private function getAssigneeData(IssueEntity $issue): array /** * Gets or creates an Assignee object in an ArrayCollection. * - * @param ArrayCollection $assignees the ArrayCollection containing the Assignee objects + * @param ArrayCollection $assignees the ArrayCollection containing the Assignee objects + * @param array{key: string, displayName: string, weekNorm: int|float} $assigneeData * * @return Assignee the retrieved or created Assignee object */ diff --git a/src/Service/ProjectBillingService.php b/src/Service/ProjectBillingService.php index cef1b1c38..4ecfb7246 100644 --- a/src/Service/ProjectBillingService.php +++ b/src/Service/ProjectBillingService.php @@ -40,6 +40,9 @@ public function __construct( * @throws EconomicsException * @throws \Exception */ + /** + * @return array + */ public function getIssuesNotIncludedInProjectBilling(ProjectBilling $projectBilling): array { $project = $projectBilling->getProject(); diff --git a/src/Service/SubscriptionHandlerService.php b/src/Service/SubscriptionHandlerService.php index 4fc5f70ee..0d1ed9c3f 100644 --- a/src/Service/SubscriptionHandlerService.php +++ b/src/Service/SubscriptionHandlerService.php @@ -42,7 +42,7 @@ public function __construct( /** * Send a notification email. * - * @param array $notification the notification data + * @param array $notification the notification data * * @throws TransportExceptionInterface */ @@ -157,6 +157,9 @@ private function getVersion(?int $versionId): ?Version * @throws SyntaxError * @throws \Exception */ + /** + * @return array + */ private function prepareMailData(Subscription $subscription, \DateTime $fromDate, \DateTime $toDate, Project $project, HourReportData $reportData, string $email): array { $renderedReport = $this->environment->render('subscription/subscription_hour_report.html.twig', [ diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 799e6932a..639627982 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -179,7 +179,7 @@ private function getCurrentPeriodNumeric(PeriodTypeEnum $viewMode, int $year): i * @param int $year the year for the period * @param PeriodTypeEnum $viewMode the view mode to determine the dates of the period * - * @return array an array of dates for the given period + * @return array{dateFrom: \DateTime, dateTo: \DateTime} an array of dates for the given period */ private function getDatesOfPeriod(int $period, int $year, PeriodTypeEnum $viewMode): array { @@ -212,7 +212,7 @@ private function getReadablePeriod(int $period, PeriodTypeEnum $viewMode): strin * @param PeriodTypeEnum $viewMode the view mode to determine the periods * @param int $year the year containing the periods * - * @return array an array of periods + * @return array an array of periods */ private function getPeriods(PeriodTypeEnum $viewMode, int $year): array { @@ -229,7 +229,7 @@ private function getPeriods(PeriodTypeEnum $viewMode, int $year): array * @param ViewModeEnum $viewMode defines the view mode * @param string $workerIdentifier the worker's identifier * - * @return array the list of workloads matching the criteria defined by the parameters + * @return array the list of workloads matching the criteria defined by the parameters */ private function getWorklogs(ViewModeEnum $viewMode, string $workerIdentifier, \DateTime $dateFrom, \DateTime $dateTo): array { diff --git a/tests/Service/DanishHolidayHelperTest.php b/tests/Service/DanishHolidayHelperTest.php index 41f326015..b9bdcd21d 100644 --- a/tests/Service/DanishHolidayHelperTest.php +++ b/tests/Service/DanishHolidayHelperTest.php @@ -34,6 +34,9 @@ public function testEaster(int $year, \DateTimeInterface $expected): void ); } + /** + * @return iterable + */ public static function dataEaster(): iterable { yield '[2023]' => [ @@ -82,6 +85,9 @@ public function testNextNonHoliday(\DateTimeInterface $date, ?\DateTimeInterface $this->assertSameDate($expected, $actual); } + /** + * @return iterable + */ public static function dataNextNonHoliday(): iterable { yield '2024-03-31' => [ @@ -99,6 +105,9 @@ public function testNextBankDay(\DateTimeInterface $date, ?\DateTimeInterface $e $this->assertSameDate($expected, $actual); } + /** + * @return iterable + */ public static function dataNextBankDay(): iterable { yield '2024-03-29' => [ @@ -131,6 +140,9 @@ public function testNextBankDay30(\DateTimeInterface $date, ?\DateTimeInterface $this->assertSameDate($expected, $actual); } + /** + * @return iterable + */ public static function dataNextBankDay30(): iterable { yield '2024-03-29' => [ @@ -158,6 +170,9 @@ public function testIsBankHoliday(\DateTimeInterface $date, bool $expected): voi $this->assertSame($expected, $actual); } + /** + * @return iterable + */ public static function dataIsBankHoliday(): iterable { yield '2024-12-31' => [ diff --git a/tests/Service/DateTimeHelperTest.php b/tests/Service/DateTimeHelperTest.php index 4a2264f76..db234529b 100644 --- a/tests/Service/DateTimeHelperTest.php +++ b/tests/Service/DateTimeHelperTest.php @@ -18,6 +18,8 @@ public function setUp(): void /** * @dataProvider weekYearProvider + * + * @param array{dateFrom: \DateTime, dateTo: \DateTime} $expected */ public function testGetFirstAndLastDateOfWeek(int $weekNumber, int $year, array $expected): void { @@ -25,6 +27,9 @@ public function testGetFirstAndLastDateOfWeek(int $weekNumber, int $year, array $this->assertEquals($expected, $result); } + /** + * @return array + */ public static function weekYearProvider(): array { return [ @@ -65,6 +70,8 @@ public static function weekYearProvider(): array /** * @dataProvider monthYearProvider + * + * @param array{dateFrom: \DateTime, dateTo: \DateTime} $expected */ public function testGetFirstAndLastDateOfMonth(int $monthNumber, int $year, array $expected): void { @@ -72,6 +79,9 @@ public function testGetFirstAndLastDateOfMonth(int $monthNumber, int $year, arra $this->assertEquals($expected, $result); } + /** + * @return array + */ public static function monthYearProvider(): array { return [ @@ -112,6 +122,8 @@ public static function monthYearProvider(): array /** * @dataProvider weeksOfYearProvider + * + * @param array $expected */ public function testGetWeeksOfYear(int $year, array $expected): void { @@ -119,6 +131,9 @@ public function testGetWeeksOfYear(int $year, array $expected): void $this->assertEquals($expected, $result); } + /** + * @return array}> + */ public static function weeksOfYearProvider(): array { return [ @@ -146,6 +161,9 @@ public function testGetMonthName(int $monthNumber, string $expectedMonthName): v $this->assertEquals($expectedMonthName, $monthName); } + /** + * @return array + */ public static function monthNameDataProvider(): array { return [ @@ -166,6 +184,8 @@ public static function monthNameDataProvider(): array /** * @dataProvider yearProvider + * + * @param array{dateFrom: \DateTime, dateTo: \DateTime} $expected */ public function testGetFirstAndLastDateOfYear(int $year, array $expected): void { @@ -173,6 +193,9 @@ public function testGetFirstAndLastDateOfYear(int $year, array $expected): void $this->assertEquals($expected, $result); } + /** + * @return array + */ public static function yearProvider(): array { return [ From 034d11f39845eb2ab909927d4dffae95cecc7f22 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 12:27:09 +0200 Subject: [PATCH 05/23] 7273: Fixed trivial misc issues --- CHANGELOG.md | 9 ++++ phpstan-baseline.neon | 46 +------------------ src/Entity/Epic.php | 2 +- .../Reports/ForecastReportWorklogData.php | 2 +- src/Repository/SubscriptionRepository.php | 3 +- src/Service/DanishHolidayHelper.php | 2 +- src/Service/ForecastReportService.php | 2 +- src/Service/SubscriptionHandlerService.php | 1 - 8 files changed, 16 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c1942c4..e3b2d5099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Cleaned up small PHPStan issues: corrected `Epic::getIssues()` PHPDoc + return type (`Collection`, was `Epic`), switched + `DanishHolidayHelper::getInstance()` from `empty()` to `isset()` on the + typed singleton property, removed unused `PaginatorInterface $paginator` + from `SubscriptionRepository::__construct()`, dropped the unused + `$worklogId`/`$description` parameters from `ForecastReportWorklogData` + (caller updated; properties were already set via direct assignment), + and removed the unreachable `break;` after a `throw` in + `SubscriptionHandlerService::handleSubscription()`. Baseline 268 → 261. * Cleared all 167 PHPStan `missingType.iterableValue` baseline entries. Removed redundant `@method find*` scaffold docblocks from 17 repositories (the parent `@extends ServiceEntityRepository` already provides typed diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f4c488cf3..8f5fa9341 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -114,12 +114,6 @@ parameters: count: 1 path: src/Entity/DataProvider.php - - - message: '#^Method App\\Entity\\Epic\:\:getIssues\(\) should return Doctrine\\Common\\Collections\\Collection\ but returns Doctrine\\Common\\Collections\\Collection\\.$#' - identifier: return.type - count: 1 - path: src/Entity/Epic.php - - message: '#^Property App\\Entity\\Epic\:\:\$title type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -414,30 +408,6 @@ parameters: count: 1 path: src/Entity/Worklog.php - - - message: '#^Constructor of class App\\Model\\Reports\\ForecastReportWorklogData has an unused parameter \$description\.$#' - identifier: constructor.unusedParameter - count: 1 - path: src/Model/Reports/ForecastReportWorklogData.php - - - - message: '#^Constructor of class App\\Model\\Reports\\ForecastReportWorklogData has an unused parameter \$worklogId\.$#' - identifier: constructor.unusedParameter - count: 1 - path: src/Model/Reports/ForecastReportWorklogData.php - - - - message: '#^Method App\\Model\\Reports\\ForecastReportWorklogData\:\:__construct\(\) has parameter \$description with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Model/Reports/ForecastReportWorklogData.php - - - - message: '#^Method App\\Model\\Reports\\ForecastReportWorklogData\:\:__construct\(\) has parameter \$worklogId with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Model/Reports/ForecastReportWorklogData.php - - message: '#^Class App\\Model\\Reports\\HourReportData has an uninitialized readonly property \$id\. Assign it in the constructor\.$#' identifier: property.uninitializedReadonly @@ -492,12 +462,6 @@ parameters: count: 1 path: src/Model/Reports/WorkloadReportData.php - - - message: '#^Property App\\Repository\\SubscriptionRepository\:\:\$paginator is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: src/Repository/SubscriptionRepository.php - - message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' identifier: method.nonObject @@ -523,8 +487,8 @@ parameters: path: src/Service/BillableUnbilledHoursReportService.php - - message: '#^Static property App\\Service\\DanishHolidayHelper\:\:\$instance \(App\\Service\\DanishHolidayHelper\) in empty\(\) is not falsy\.$#' - identifier: empty.property + message: '#^Static property App\\Service\\DanishHolidayHelper\:\:\$instance \(App\\Service\\DanishHolidayHelper\) in isset\(\) is not nullable\.$#' + identifier: isset.property count: 1 path: src/Service/DanishHolidayHelper.php @@ -828,12 +792,6 @@ parameters: count: 1 path: src/Service/SubscriptionHandlerService.php - - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable - count: 1 - path: src/Service/SubscriptionHandlerService.php - - message: '#^Parameter \#1 \$key of method Doctrine\\Common\\Collections\\ArrayCollection\\:\:set\(\) expects string, int given\.$#' identifier: argument.type diff --git a/src/Entity/Epic.php b/src/Entity/Epic.php index 6f669d759..8dee5985a 100644 --- a/src/Entity/Epic.php +++ b/src/Entity/Epic.php @@ -44,7 +44,7 @@ public function setTitle(string $title): static } /** - * @return Collection + * @return Collection */ public function getIssues(): Collection { diff --git a/src/Model/Reports/ForecastReportWorklogData.php b/src/Model/Reports/ForecastReportWorklogData.php index 5b7181c35..34dbab286 100644 --- a/src/Model/Reports/ForecastReportWorklogData.php +++ b/src/Model/Reports/ForecastReportWorklogData.php @@ -9,7 +9,7 @@ class ForecastReportWorklogData public float $invoiced = 0.0; public float $invoicedAndRecorded = 0.0; - public function __construct($worklogId, $description) + public function __construct() { } } diff --git a/src/Repository/SubscriptionRepository.php b/src/Repository/SubscriptionRepository.php index 1f8f8b8da..cf75cd64f 100644 --- a/src/Repository/SubscriptionRepository.php +++ b/src/Repository/SubscriptionRepository.php @@ -6,14 +6,13 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ManagerRegistry; -use Knp\Component\Pager\PaginatorInterface; /** * @extends ServiceEntityRepository */ class SubscriptionRepository extends ServiceEntityRepository { - public function __construct(ManagerRegistry $registry, private readonly PaginatorInterface $paginator) + public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Subscription::class); } diff --git a/src/Service/DanishHolidayHelper.php b/src/Service/DanishHolidayHelper.php index 6a9216964..e2fb2fc98 100644 --- a/src/Service/DanishHolidayHelper.php +++ b/src/Service/DanishHolidayHelper.php @@ -19,7 +19,7 @@ final class DanishHolidayHelper public static function getInstance(): self { - if (empty(self::$instance)) { + if (!isset(self::$instance)) { self::$instance = new self(); } diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index d601dcec9..59f17ff36 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -135,7 +135,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa // Add the worklog entry in the version if it does not exist if (!isset($currentVersion->worklogs[$worklogId])) { - $currentVersion->worklogs[$worklogId] = new ForecastReportWorklogData($worklogId, $description); + $currentVersion->worklogs[$worklogId] = new ForecastReportWorklogData(); $currentVersion->worklogs[$worklogId]->worker = $workerName; $currentVersion->worklogs[$worklogId]->description = $description; } diff --git a/src/Service/SubscriptionHandlerService.php b/src/Service/SubscriptionHandlerService.php index 0d1ed9c3f..d9cecbf89 100644 --- a/src/Service/SubscriptionHandlerService.php +++ b/src/Service/SubscriptionHandlerService.php @@ -105,7 +105,6 @@ public function handleSubscription(Subscription $subscription, \DateTime $fromDa break; default: throw new \Exception('Report type is not yet supported'); - break; } } From 87456204e43b954cf4bb6c487dd9b1032e3ccc33 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 12:40:11 +0200 Subject: [PATCH 06/23] 7273: Fixed 16 specific PHPStan-flagged issues, mostly real bugs the type surface had been hiding --- CHANGELOG.md | 28 +++++ phpstan-baseline.neon | 100 +----------------- src/Command/ProductsImportCommand.php | 3 - src/Controller/SubscriptionController.php | 6 +- src/Entity/User.php | 6 +- .../ForecastReportIssueVersionData.php | 2 +- src/Model/Reports/HourReportData.php | 1 - src/Model/Reports/InvoicingRateReportData.php | 1 - src/Model/Reports/WorkloadReportData.php | 1 - src/Service/DanishHolidayHelper.php | 2 +- src/Service/DateTimeHelper.php | 1 + src/Service/ForecastReportService.php | 10 +- src/Service/LeantimeApiService.php | 2 + src/Service/PlanningService.php | 2 +- src/Service/SubscriptionHandlerService.php | 2 +- tests/Service/WorkloadReportServiceTest.php | 7 +- 16 files changed, 53 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b2d5099..63db54c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Fixed 16 specific PHPStan-flagged issues, mostly real bugs the type + surface had been hiding: + - `ForecastReportService` substitutes `'[no issue id]'`/`'[no issue link]'`/`''` + fallbacks for nullable issue/worklog fields when assigning to non-null + model properties; removed an always-false `!$currentProject` guard + that PHPStan proved unreachable. + - `ForecastReportIssueVersionData::$worklogs` typed `array` + to reflect that keys come from `Worklog::getId()` (`?int`). + - Removed dead `readonly string $id` declarations on `HourReportData`, + `WorkloadReportData`, `InvoicingRateReportData` (never assigned, never read). + - Added `default => throw` arms to incomplete `match` expressions in + `DateTimeHelper::getFirstAndLastDateOfQuarter` and `LeantimeApiService`. + - Fixed `SubscriptionHandlerService::getVersion()` looking up + `VersionRepository` by non-existent `versionId` field; use + `find($versionId)` against the primary key instead. + - `User::getUserIdentifier()` now throws on empty email (Symfony's + `UserInterface` requires `non-empty-string`). + - `SubscriptionController::getFrequencies()` skips subscriptions with + a null `frequency` instead of dereferencing. + - `PlanningService` dropped extra argument to `Issue::getHoursRemaining()` + (method takes 0 parameters). + - `ProductsImportCommand` removed an unreachable `null === $row` check. + - `DanishHolidayHelper::$instance` `@var` corrected to `?DanishHolidayHelper` + to reflect that the singleton is unset before first call. + - `WorkloadReportServiceTest::testGetWorkloadReport` replaced redundant + `assertInstanceOf` calls (always-true on already-narrowed type) with + assertions on `$result->viewmode` that match the period type passed in. + Baseline 261 → 243. * Cleaned up small PHPStan issues: corrected `Epic::getIssues()` PHPDoc return type (`Collection`, was `Epic`), switched `DanishHolidayHelper::getInstance()` from `empty()` to `isset()` on the diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8f5fa9341..98bb8d232 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,11 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Strict comparison using \=\=\= between null and OpenSpout\\Common\\Entity\\Row will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: src/Command/ProductsImportCommand.php - - message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetCsvResponse\(\) expects array\, array\ given\.$#' identifier: argument.type @@ -48,12 +42,6 @@ parameters: count: 1 path: src/Controller/ProjectBillingController.php - - - message: '#^Cannot access property \$value on App\\Enum\\SubscriptionFrequencyEnum\|null\.$#' - identifier: property.nonObject - count: 1 - path: src/Controller/SubscriptionController.php - - message: '#^Parameter \#1 \$email of method App\\Repository\\SubscriptionRepository\:\:findByCustom\(\) expects string, string\|null given\.$#' identifier: argument.type @@ -318,12 +306,6 @@ parameters: count: 1 path: src/Entity/Subscription.php - - - message: '#^Method App\\Entity\\User\:\:getUserIdentifier\(\) should return non\-empty\-string but returns string\.$#' - identifier: return.type - count: 1 - path: src/Entity/User.php - - message: '#^Property App\\Entity\\User\:\:\$email type mapping mismatch\: property can contain string\|null but database expects string\.$#' identifier: doctrine.columnType @@ -408,12 +390,6 @@ parameters: count: 1 path: src/Entity/Worklog.php - - - message: '#^Class App\\Model\\Reports\\HourReportData has an uninitialized readonly property \$id\. Assign it in the constructor\.$#' - identifier: property.uninitializedReadonly - count: 1 - path: src/Model/Reports/HourReportData.php - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$headline with no type specified\.$#' identifier: missingType.parameter @@ -450,18 +426,6 @@ parameters: count: 1 path: src/Model/Reports/HourReportProjectTicket.php - - - message: '#^Class App\\Model\\Reports\\InvoicingRateReportData has an uninitialized readonly property \$id\. Assign it in the constructor\.$#' - identifier: property.uninitializedReadonly - count: 1 - path: src/Model/Reports/InvoicingRateReportData.php - - - - message: '#^Class App\\Model\\Reports\\WorkloadReportData has an uninitialized readonly property \$id\. Assign it in the constructor\.$#' - identifier: property.uninitializedReadonly - count: 1 - path: src/Model/Reports/WorkloadReportData.php - - message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' identifier: method.nonObject @@ -486,12 +450,6 @@ parameters: count: 1 path: src/Service/BillableUnbilledHoursReportService.php - - - message: '#^Static property App\\Service\\DanishHolidayHelper\:\:\$instance \(App\\Service\\DanishHolidayHelper\) in isset\(\) is not nullable\.$#' - identifier: isset.property - count: 1 - path: src/Service/DanishHolidayHelper.php - - message: '#^Parameter \#2 \$timestamp of function date expects int\|null, int\|false given\.$#' identifier: argument.type @@ -510,12 +468,6 @@ parameters: count: 1 path: src/Service/DateTimeHelper.php - - - message: '#^Match expression does not handle remaining values\: int\\|int\<5, max\>$#' - identifier: match.unhandled - count: 1 - path: src/Service/DateTimeHelper.php - - message: '#^Call to an undefined method App\\Entity\\Epic\:\:getName\(\)\.$#' identifier: method.notFound @@ -552,12 +504,6 @@ parameters: count: 1 path: src/Service/ForecastReportService.php - - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 1 - path: src/Service/ForecastReportService.php - - message: '#^Parameter \#1 \$projectId of class App\\Model\\Reports\\ForecastReportProjectData constructor expects string, int\\|int\<1, max\> given\.$#' identifier: argument.type @@ -570,30 +516,6 @@ parameters: count: 1 path: src/Service/ForecastReportService.php - - - message: '#^Property App\\Model\\Reports\\ForecastReportIssueData\:\:\$issueId \(string\) does not accept string\|null\.$#' - identifier: assign.propertyType - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Property App\\Model\\Reports\\ForecastReportIssueData\:\:\$issueLink \(string\) does not accept string\|null\.$#' - identifier: assign.propertyType - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Property App\\Model\\Reports\\ForecastReportIssueVersionData\:\:\$worklogs \(array\\) does not accept array\\.$#' - identifier: assign.propertyType - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Property App\\Model\\Reports\\ForecastReportWorklogData\:\:\$description \(string\) does not accept string\|null\.$#' - identifier: assign.propertyType - count: 1 - path: src/Service/ForecastReportService.php - - message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' identifier: method.nonObject @@ -757,8 +679,8 @@ parameters: path: src/Service/LeantimeApiService.php - - message: '#^Match expression does not handle remaining value\: string$#' - identifier: match.unhandled + message: '#^Match arm comparison between ''timesheets'' and ''timesheets'' is always true\.$#' + identifier: match.alwaysTrue count: 1 path: src/Service/LeantimeApiService.php @@ -774,24 +696,12 @@ parameters: count: 1 path: src/Service/LeantimeApiService.php - - - message: '#^Method App\\Entity\\Issue\:\:getHoursRemaining\(\) invoked with 1 parameter, 0 required\.$#' - identifier: arguments.count - count: 1 - path: src/Service/PlanningService.php - - message: '#^Parameter \#2 \$week of method App\\Service\\PlanningService\:\:processIssuesForWeek\(\) expects int, int\|string given\.$#' identifier: argument.type count: 1 path: src/Service/PlanningService.php - - - message: '#^Call to method App\\Repository\\VersionRepository\:\:findOneBy\(\) \- entity App\\Entity\\Version does not have a field named \$versionId\.$#' - identifier: doctrine.findOneByArgument - count: 1 - path: src/Service/SubscriptionHandlerService.php - - message: '#^Parameter \#1 \$key of method Doctrine\\Common\\Collections\\ArrayCollection\\:\:set\(\) expects string, int given\.$#' identifier: argument.type @@ -984,12 +894,6 @@ parameters: count: 1 path: tests/Service/ProjectBillingServiceTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\WorkloadReportData'' and App\\Model\\Reports\\WorkloadReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 3 - path: tests/Service/WorkloadReportServiceTest.php - - message: '#^Parameter \#2 \$timestamp of function date expects int\|null, int\|false given\.$#' identifier: argument.type diff --git a/src/Command/ProductsImportCommand.php b/src/Command/ProductsImportCommand.php index 31c951649..dc673e28b 100644 --- a/src/Command/ProductsImportCommand.php +++ b/src/Command/ProductsImportCommand.php @@ -60,9 +60,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($reader->getSheetIterator() as $sheet) { $headers = null; foreach ($sheet->getRowIterator() as $row) { - if (null === $row) { - continue; - } if (null === $headers) { $headers = $getRowAsStrings($row); if (!in_array($headerName, $headers)) { diff --git a/src/Controller/SubscriptionController.php b/src/Controller/SubscriptionController.php index 54185bf20..6f945db9d 100644 --- a/src/Controller/SubscriptionController.php +++ b/src/Controller/SubscriptionController.php @@ -162,7 +162,11 @@ private function getFrequencies(array $subscriptions): string { $frequencies = []; foreach ($subscriptions as $subscription) { - $frequencies[] = $subscription->getFrequency()->value; + $frequency = $subscription->getFrequency(); + if (null === $frequency) { + continue; + } + $frequencies[] = $frequency->value; } // Getting the order from Enum diff --git a/src/Entity/User.php b/src/Entity/User.php index c1a47fc31..a043ff8ba 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -52,7 +52,11 @@ public function setEmail(string $email): self */ public function getUserIdentifier(): string { - return (string) $this->email; + if (null === $this->email || '' === $this->email) { + throw new \LogicException('User identifier requires a non-empty email.'); + } + + return $this->email; } /** diff --git a/src/Model/Reports/ForecastReportIssueVersionData.php b/src/Model/Reports/ForecastReportIssueVersionData.php index 60bf79109..2b0eca020 100644 --- a/src/Model/Reports/ForecastReportIssueVersionData.php +++ b/src/Model/Reports/ForecastReportIssueVersionData.php @@ -8,7 +8,7 @@ class ForecastReportIssueVersionData public string $issueVersionIdentifier; public float $invoiced = 0.0; public float $invoicedAndRecorded = 0.0; - /** @var array */ + /** @var array */ public array $worklogs = []; public function __construct(string $issueVersion) diff --git a/src/Model/Reports/HourReportData.php b/src/Model/Reports/HourReportData.php index be68959b9..54089c05d 100644 --- a/src/Model/Reports/HourReportData.php +++ b/src/Model/Reports/HourReportData.php @@ -6,7 +6,6 @@ class HourReportData { - public readonly string $id; public float $projectTotalSpent; public float $projectTotalEstimated; /** @var ArrayCollection */ diff --git a/src/Model/Reports/InvoicingRateReportData.php b/src/Model/Reports/InvoicingRateReportData.php index caf5cda55..4cc2bdeba 100644 --- a/src/Model/Reports/InvoicingRateReportData.php +++ b/src/Model/Reports/InvoicingRateReportData.php @@ -6,7 +6,6 @@ class InvoicingRateReportData { - public readonly string $id; public readonly string $viewmode; /** @var ArrayCollection */ public ArrayCollection $period; diff --git a/src/Model/Reports/WorkloadReportData.php b/src/Model/Reports/WorkloadReportData.php index 1f8a0da84..e03bd2803 100644 --- a/src/Model/Reports/WorkloadReportData.php +++ b/src/Model/Reports/WorkloadReportData.php @@ -6,7 +6,6 @@ class WorkloadReportData { - public readonly string $id; public readonly string $viewmode; /** @var ArrayCollection */ public ArrayCollection $period; diff --git a/src/Service/DanishHolidayHelper.php b/src/Service/DanishHolidayHelper.php index e2fb2fc98..21f9c6316 100644 --- a/src/Service/DanishHolidayHelper.php +++ b/src/Service/DanishHolidayHelper.php @@ -13,7 +13,7 @@ final class DanishHolidayHelper public const SUNDAY = 7; /** - * @var DanishHolidayHelper + * @var ?DanishHolidayHelper */ private static $instance; diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index 0ab7d78f6..87ea2433c 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -141,6 +141,7 @@ public function getFirstAndLastDateOfQuarter(int $year, int $quarter): array 2 => 4, 3 => 7, 4 => 10, + default => throw new \InvalidArgumentException(sprintf('Quarter must be 1-4, got %d', $quarter)), }; $lastMonth = $firstMonth + 2; diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 59f17ff36..f42822c38 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -62,10 +62,6 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa // Get current project from forecast $currentProject = $forecastReportData->projects[$projectId]; - if (!$currentProject) { - throw new \Exception('Project instance was not found'); - } - // Calculate worklog time in hours $worklogTime = ($worklog->getTimeSpentSeconds() / 3600); @@ -79,8 +75,8 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa } // Get issue details from the worklog - $issueId = $worklog->getIssue()->getProjectTrackerKey(); - $issueLink = $worklog->getIssue()->getLinkToIssue(); + $issueId = $worklog->getIssue()->getProjectTrackerKey() ?? '[no issue id]'; + $issueLink = $worklog->getIssue()->getLinkToIssue() ?? '[no issue link]'; if ($worklog->getIssue()->getEpics()->count() > 0) { $issueTag = implode(',', array_map(fn ($epic) => $epic->getName(), $worklog->getIssue()->getEpics())); @@ -131,7 +127,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa $worklogId = $worklog->getId(); $workerEmail = $worklog->getWorker(); $workerName = $workerNameMapping[$workerEmail] ?? '[no worker]'; - $description = $worklog->getDescription(); + $description = $worklog->getDescription() ?? ''; // Add the worklog entry in the version if it does not exist if (!isset($currentVersion->worklogs[$worklogId])) { diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index e4138e04e..f84a6ea0b 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -134,6 +134,7 @@ public function deleteAsJob(int $dataProviderId, bool $asyncJobQueue = false, ?\ self::MILESTONES => Version::class, self::TICKETS => Issue::class, self::TIMESHEETS => Worklog::class, + default => throw new \InvalidArgumentException(sprintf('Unsupported type: %s', $type)), }; foreach ($results->{$type} as $result) { @@ -180,6 +181,7 @@ public function updateAsJob(string $className, int $startId, int $limit, int $da Issue::class => self::TICKETS, Worklog::class => self::TIMESHEETS, Worker::class => self::WORKERS, + default => throw new \InvalidArgumentException(sprintf('Unsupported class: %s', $className)), }; // Get data from Leantime. diff --git a/src/Service/PlanningService.php b/src/Service/PlanningService.php index d3b56106b..2afd4e7cb 100644 --- a/src/Service/PlanningService.php +++ b/src/Service/PlanningService.php @@ -171,7 +171,7 @@ private function processIssuesForWeek(PlanningData $planning, int $week, array $ } $projectKey = (string) $issueProject->getProjectTrackerId(); $projectDisplayName = $issueProject->getName() ?? self::UNNAMED_STR; - $hoursRemaining = $issueData->getHoursRemaining($issueData); + $hoursRemaining = $issueData->getHoursRemaining(); $assigneeData = $this->getAssigneeData($issueData); $assignee = $this->getOrCreateAssignee($planning->assignees, $assigneeData); diff --git a/src/Service/SubscriptionHandlerService.php b/src/Service/SubscriptionHandlerService.php index d9cecbf89..5b474a022 100644 --- a/src/Service/SubscriptionHandlerService.php +++ b/src/Service/SubscriptionHandlerService.php @@ -135,7 +135,7 @@ private function getProject(?int $projectId): ?Project */ private function getVersion(?int $versionId): ?Version { - return $versionId ? $this->versionRepository->findOneBy(['versionId' => $versionId]) : null; + return $versionId ? $this->versionRepository->find($versionId) : null; } /** diff --git a/tests/Service/WorkloadReportServiceTest.php b/tests/Service/WorkloadReportServiceTest.php index ed3064578..e29b2a18e 100644 --- a/tests/Service/WorkloadReportServiceTest.php +++ b/tests/Service/WorkloadReportServiceTest.php @@ -4,7 +4,6 @@ use App\Entity\Worker; use App\Entity\Worklog; -use App\Model\Reports\WorkloadReportData; use App\Model\Reports\WorkloadReportPeriodTypeEnum as PeriodTypeEnum; use App\Model\Reports\WorkloadReportViewModeEnum as ViewModeEnum; use App\Repository\WorkerRepository; @@ -72,13 +71,13 @@ public function testGetWorkloadReport(): void $workloadReportService = new WorkloadReportService($workerRepoMock, $worklogRepoMock, $dateTimeHelperMock); $result = $workloadReportService->getWorkloadReport(2024, PeriodTypeEnum::WEEK, ViewModeEnum::WORKLOAD); - $this->assertInstanceOf(WorkloadReportData::class, $result); + $this->assertSame(PeriodTypeEnum::WEEK->value, $result->viewmode); $result = $workloadReportService->getWorkloadReport(2024, PeriodTypeEnum::MONTH, ViewModeEnum::WORKLOAD); - $this->assertInstanceOf(WorkloadReportData::class, $result); + $this->assertSame(PeriodTypeEnum::MONTH->value, $result->viewmode); $result = $workloadReportService->getWorkloadReport(2024, PeriodTypeEnum::YEAR, ViewModeEnum::WORKLOAD); - $this->assertInstanceOf(WorkloadReportData::class, $result); + $this->assertSame(PeriodTypeEnum::YEAR->value, $result->viewmode); } public function testExceptionIsThrownWhenWorkerIdentifierIsEmpty(): void From 07962e6d1e68ab62e16f758802f4626fca64f863 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 16:34:53 +0200 Subject: [PATCH 07/23] 7273: Aligned Doctrine entity property types with database column nullability --- CHANGELOG.md | 23 +++ migrations/Version20260517131038.php | 41 +++++ phpstan-baseline.neon | 256 +-------------------------- src/Entity/Account.php | 4 +- src/Entity/Client.php | 2 +- src/Entity/DataProvider.php | 4 +- src/Entity/Epic.php | 2 +- src/Entity/Invoice.php | 4 +- src/Entity/InvoiceEntry.php | 2 +- src/Entity/Issue.php | 8 +- src/Entity/IssueProduct.php | 2 +- src/Entity/Product.php | 4 +- src/Entity/Project.php | 6 +- src/Entity/ProjectBilling.php | 8 +- src/Entity/ProjectVersionBudget.php | 6 +- src/Entity/ServiceAgreement.php | 8 +- src/Entity/Subscription.php | 2 +- src/Entity/User.php | 4 +- src/Entity/Version.php | 6 +- src/Entity/Worker.php | 4 +- src/Entity/WorkerGroup.php | 4 +- src/Entity/Worklog.php | 10 +- 22 files changed, 111 insertions(+), 299 deletions(-) create mode 100644 migrations/Version20260517131038.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 63db54c00..2c7be6684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Aligned Doctrine entity property types with database column nullability + (PHPStan `doctrine.columnType`/`doctrine.associationType`, Pattern B — + 43 entries cleared, baseline 243 → 201). Two-part change: + - **Migration `Version20260517131038`**: made 18 columns nullable to + match property types on entities that legitimately allow null — + the four synced entities (`issue`, `project`, `version`, `worklog`) + where fields arrive incrementally from external trackers, plus the + DateTime fields `project_billing.period_{start,end}` and + `service_agreement.valid_from`. + - **Property defaults**: converted 25 `?T $prop = null` declarations to + `T $prop = default` on app-managed entities (`Account`, `Client`, + `DataProvider`, `Epic`, `Invoice`, `InvoiceEntry`, `IssueProduct`, + `Product`, `ProjectBilling`, `ProjectVersionBudget`, + `ServiceAgreement`, `Subscription`, `User`, `Worker`, `WorkerGroup`). + Defaults: `''` for strings, `false` for booleans, `0`/`0.0` for + numerics, `'0'` for the decimal-as-string `Product::$price`, and + `HostingProviderEnum::ADM` for `ServiceAgreement::$hostingProvider` + (no natural zero exists for enums; pick a default and rely on + callers/validators to set the intended value before persist). + - Switched two `__toString()` methods (`Worker`, `WorkerGroup`) from + `??` to `?:` to preserve the empty-string-falls-through behavior + now that the properties can't be `null`. + * Fixed 16 specific PHPStan-flagged issues, mostly real bugs the type surface had been hiding: - `ForecastReportService` substitutes `'[no issue id]'`/`'[no issue link]'`/`''` diff --git a/migrations/Version20260517131038.php b/migrations/Version20260517131038.php new file mode 100644 index 000000000..de73a57d7 --- /dev/null +++ b/migrations/Version20260517131038.php @@ -0,0 +1,41 @@ +addSql('ALTER TABLE issue CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE project_tracker_id project_tracker_id VARCHAR(255) DEFAULT NULL, CHANGE project_tracker_key project_tracker_key VARCHAR(255) DEFAULT NULL, CHANGE link_to_issue link_to_issue VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE project CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE project_tracker_project_url project_tracker_project_url VARCHAR(255) DEFAULT NULL, CHANGE project_tracker_key project_tracker_key VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE project_billing CHANGE period_start period_start DATETIME DEFAULT NULL, CHANGE period_end period_end DATETIME DEFAULT NULL'); + $this->addSql('ALTER TABLE service_agreement CHANGE valid_from valid_from DATETIME DEFAULT NULL'); + $this->addSql('ALTER TABLE version CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE project_tracker_id project_tracker_id VARCHAR(255) DEFAULT NULL, CHANGE is_billable is_billable TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE worklog CHANGE worklog_id worklog_id INT DEFAULT NULL, CHANGE worker worker VARCHAR(255) DEFAULT NULL, CHANGE time_spent_seconds time_spent_seconds INT DEFAULT NULL, CHANGE started started DATETIME DEFAULT NULL, CHANGE project_tracker_issue_id project_tracker_issue_id VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE project CHANGE name name VARCHAR(255) NOT NULL, CHANGE project_tracker_project_url project_tracker_project_url VARCHAR(255) NOT NULL, CHANGE project_tracker_key project_tracker_key VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE worklog CHANGE worklog_id worklog_id INT NOT NULL, CHANGE worker worker VARCHAR(255) NOT NULL, CHANGE time_spent_seconds time_spent_seconds INT NOT NULL, CHANGE started started DATETIME NOT NULL, CHANGE project_tracker_issue_id project_tracker_issue_id VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE project_billing CHANGE period_start period_start DATETIME NOT NULL, CHANGE period_end period_end DATETIME NOT NULL'); + $this->addSql('ALTER TABLE version CHANGE name name VARCHAR(255) NOT NULL, CHANGE project_tracker_id project_tracker_id VARCHAR(255) NOT NULL, CHANGE is_billable is_billable TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE service_agreement CHANGE valid_from valid_from DATETIME NOT NULL'); + $this->addSql('ALTER TABLE issue CHANGE name name VARCHAR(255) NOT NULL, CHANGE project_tracker_id project_tracker_id VARCHAR(255) NOT NULL, CHANGE project_tracker_key project_tracker_key VARCHAR(255) NOT NULL, CHANGE link_to_issue link_to_issue VARCHAR(255) NOT NULL'); + } +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 98bb8d232..c9fe3a456 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -66,96 +66,18 @@ parameters: count: 1 path: src/DataFixtures/AppFixtures.php - - - message: '#^Property App\\Entity\\Account\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Account.php - - - - message: '#^Property App\\Entity\\Account\:\:\$value type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Account.php - - - - message: '#^Property App\\Entity\\Client\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Client.php - - message: '#^Property App\\Entity\\CybersecurityAgreement\:\:\$serviceAgreement type mapping mismatch\: property can contain App\\Entity\\ServiceAgreement\|null but database expects App\\Entity\\ServiceAgreement\.$#' identifier: doctrine.associationType count: 1 path: src/Entity/CybersecurityAgreement.php - - - message: '#^Property App\\Entity\\DataProvider\:\:\$class type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/DataProvider.php - - - - message: '#^Property App\\Entity\\DataProvider\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/DataProvider.php - - - - message: '#^Property App\\Entity\\Epic\:\:\$title type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Epic.php - - - - message: '#^Property App\\Entity\\Invoice\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Invoice.php - - - - message: '#^Property App\\Entity\\Invoice\:\:\$recorded type mapping mismatch\: property can contain bool\|null but database expects bool\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Invoice.php - - - - message: '#^Property App\\Entity\\InvoiceEntry\:\:\$index type mapping mismatch\: property can contain int\|null but database expects int\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/InvoiceEntry.php - - message: '#^Property App\\Entity\\InvoiceEntry\:\:\$invoice type mapping mismatch\: property can contain App\\Entity\\Invoice\|null but database expects App\\Entity\\Invoice\.$#' identifier: doctrine.associationType count: 1 path: src/Entity/InvoiceEntry.php - - - message: '#^Property App\\Entity\\Issue\:\:\$linkToIssue type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Issue.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Issue.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$projectTrackerId type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Issue.php - - - - message: '#^Property App\\Entity\\Issue\:\:\$projectTrackerKey type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Issue.php - - message: '#^Property App\\Entity\\IssueProduct\:\:\$issue type mapping mismatch\: property can contain App\\Entity\\Issue\|null but database expects App\\Entity\\Issue\.$#' identifier: doctrine.associationType @@ -168,120 +90,24 @@ parameters: count: 1 path: src/Entity/IssueProduct.php - - - message: '#^Property App\\Entity\\IssueProduct\:\:\$quantity type mapping mismatch\: property can contain float\|null but database expects float\|int\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/IssueProduct.php - - - - message: '#^Property App\\Entity\\Product\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Product.php - - - - message: '#^Property App\\Entity\\Product\:\:\$price type mapping mismatch\: property can contain string\|null but database expects float\|int\|string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Product.php - - message: '#^Property App\\Entity\\Product\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' identifier: doctrine.associationType count: 1 path: src/Entity/Product.php - - - message: '#^Property App\\Entity\\Project\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$projectTrackerKey type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\Project\:\:\$projectTrackerProjectUrl type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Project.php - - - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ProjectBilling.php - - - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$periodEnd type mapping mismatch\: property can contain DateTimeInterface\|null but database expects DateTimeInterface\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ProjectBilling.php - - - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$periodStart type mapping mismatch\: property can contain DateTimeInterface\|null but database expects DateTimeInterface\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ProjectBilling.php - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' identifier: doctrine.associationType count: 1 path: src/Entity/ProjectBilling.php - - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$recorded type mapping mismatch\: property can contain bool\|null but database expects bool\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ProjectBilling.php - - - - message: '#^Property App\\Entity\\ProjectVersionBudget\:\:\$budget type mapping mismatch\: property can contain float\|null but database expects float\|int\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ProjectVersionBudget.php - - - - message: '#^Property App\\Entity\\ProjectVersionBudget\:\:\$projectId type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ProjectVersionBudget.php - - - - message: '#^Property App\\Entity\\ProjectVersionBudget\:\:\$versionId type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ProjectVersionBudget.php - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$client type mapping mismatch\: property can contain App\\Entity\\Client\|null but database expects App\\Entity\\Client\.$#' identifier: doctrine.associationType count: 1 path: src/Entity/ServiceAgreement.php - - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$hostingProvider type mapping mismatch\: property can contain App\\Enum\\HostingProviderEnum\|null but database expects App\\Enum\\HostingProviderEnum\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ServiceAgreement.php - - - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$isActive type mapping mismatch\: property can contain bool\|null but database expects bool\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ServiceAgreement.php - - - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$price type mapping mismatch\: property can contain float\|null but database expects float\|int\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ServiceAgreement.php - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' identifier: doctrine.associationType @@ -295,101 +121,23 @@ parameters: path: src/Entity/ServiceAgreement.php - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$validFrom type mapping mismatch\: property can contain DateTimeInterface\|null but database expects DateTimeInterface\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/ServiceAgreement.php - - - - message: '#^Property App\\Entity\\Subscription\:\:\$email type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Subscription.php - - - - message: '#^Property App\\Entity\\User\:\:\$email type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType + message: '#^Strict comparison using \=\=\= between null and string will always evaluate to false\.$#' + identifier: identical.alwaysFalse count: 1 path: src/Entity/User.php - - - message: '#^Property App\\Entity\\User\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/User.php - - - - message: '#^Property App\\Entity\\Version\:\:\$isBillable type mapping mismatch\: property can contain bool\|null but database expects bool\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Version.php - - - - message: '#^Property App\\Entity\\Version\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Version.php - - message: '#^Property App\\Entity\\Version\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' identifier: doctrine.associationType count: 1 path: src/Entity/Version.php - - - message: '#^Property App\\Entity\\Version\:\:\$projectTrackerId type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Version.php - - - - message: '#^Property App\\Entity\\Worker\:\:\$email type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Worker.php - - - - message: '#^Property App\\Entity\\WorkerGroup\:\:\$name type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/WorkerGroup.php - - message: '#^Property App\\Entity\\Worklog\:\:\$issue type mapping mismatch\: property can contain App\\Entity\\Issue\|null but database expects App\\Entity\\Issue\.$#' identifier: doctrine.associationType count: 1 path: src/Entity/Worklog.php - - - message: '#^Property App\\Entity\\Worklog\:\:\$projectTrackerIssueId type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Worklog.php - - - - message: '#^Property App\\Entity\\Worklog\:\:\$started type mapping mismatch\: property can contain DateTimeInterface\|null but database expects DateTimeInterface\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Worklog.php - - - - message: '#^Property App\\Entity\\Worklog\:\:\$timeSpentSeconds type mapping mismatch\: property can contain int\|null but database expects int\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Worklog.php - - - - message: '#^Property App\\Entity\\Worklog\:\:\$worker type mapping mismatch\: property can contain string\|null but database expects string\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Worklog.php - - - - message: '#^Property App\\Entity\\Worklog\:\:\$worklogId type mapping mismatch\: property can contain int\|null but database expects int\.$#' - identifier: doctrine.columnType - count: 1 - path: src/Entity/Worklog.php - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$headline with no type specified\.$#' identifier: missingType.parameter diff --git a/src/Entity/Account.php b/src/Entity/Account.php index 1f895c6d4..3e22f3e73 100644 --- a/src/Entity/Account.php +++ b/src/Entity/Account.php @@ -12,10 +12,10 @@ class Account extends AbstractBaseEntity use DataProviderTrait; #[ORM\Column(length: 255)] - private ?string $name = null; + private string $name = ''; #[ORM\Column(length: 255)] - private ?string $value = null; + private string $value = ''; #[ORM\Column(length: 255, nullable: true)] private ?string $projectTrackerId = null; diff --git a/src/Entity/Client.php b/src/Entity/Client.php index 57d990743..64bd2fe97 100644 --- a/src/Entity/Client.php +++ b/src/Entity/Client.php @@ -19,7 +19,7 @@ class Client extends AbstractBaseEntity use SoftDeleteableEntity; #[ORM\Column(length: 255)] - private ?string $name = null; + private string $name = ''; #[ORM\Column(length: 255, nullable: true)] private ?string $contact = null; diff --git a/src/Entity/DataProvider.php b/src/Entity/DataProvider.php index 8cb130b1e..f64a94dde 100644 --- a/src/Entity/DataProvider.php +++ b/src/Entity/DataProvider.php @@ -9,7 +9,7 @@ class DataProvider extends AbstractBaseEntity { #[ORM\Column(length: 255, unique: true)] - private ?string $name = null; + private string $name = ''; #[ORM\Column(length: 255, nullable: true)] private ?string $url = null; @@ -18,7 +18,7 @@ class DataProvider extends AbstractBaseEntity private ?string $secret = null; #[ORM\Column(length: 255)] - private ?string $class = null; + private string $class = ''; #[ORM\Column(nullable: true)] private ?bool $enableClientSync = null; diff --git a/src/Entity/Epic.php b/src/Entity/Epic.php index 8dee5985a..e7e2b43c1 100644 --- a/src/Entity/Epic.php +++ b/src/Entity/Epic.php @@ -20,7 +20,7 @@ public function __construct() private ?int $id = null; #[ORM\Column(length: 255)] - private ?string $title = null; + private string $title = ''; /** @var Collection */ #[ORM\ManyToMany(targetEntity: Issue::class, mappedBy: 'epics')] diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index 45185379b..df5efcd0f 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -14,13 +14,13 @@ class Invoice extends AbstractBaseEntity { #[ORM\Column(length: 255)] - private ?string $name = null; + private string $name = ''; #[ORM\Column(length: 255, nullable: true)] private ?string $description = null; #[ORM\Column] - private ?bool $recorded = null; + private bool $recorded = false; #[ORM\Column(nullable: true)] private ?int $customerAccountId = null; diff --git a/src/Entity/InvoiceEntry.php b/src/Entity/InvoiceEntry.php index 5bcac5e94..f45a80a9c 100644 --- a/src/Entity/InvoiceEntry.php +++ b/src/Entity/InvoiceEntry.php @@ -19,7 +19,7 @@ class InvoiceEntry extends AbstractBaseEntity private ?Invoice $invoice = null; #[ORM\Column(type: Types::INTEGER, name: 'entry_index')] - private ?int $index = null; + private int $index = 0; // TODO: Remove since it is unused. #[ORM\Column(length: 255, nullable: true)] diff --git a/src/Entity/Issue.php b/src/Entity/Issue.php index 56b166ccf..e77e653e0 100644 --- a/src/Entity/Issue.php +++ b/src/Entity/Issue.php @@ -21,7 +21,7 @@ class Issue extends AbstractBaseEntity use DataProviderTrait; use SynchronizedEntityTrait; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $name = null; #[ORM\Column(type: 'string', nullable: true, enumType: IssueStatusEnum::class)] @@ -33,10 +33,10 @@ class Issue extends AbstractBaseEntity #[ORM\Column(length: 255, nullable: true)] private ?string $accountId = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $projectTrackerId = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $projectTrackerKey = null; // TODO: Deprecated. Remove in 4.0.0. @@ -82,7 +82,7 @@ class Issue extends AbstractBaseEntity #[ORM\Column(length: 255, nullable: true)] private ?string $worker = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $linkToIssue = null; public function __construct() diff --git a/src/Entity/IssueProduct.php b/src/Entity/IssueProduct.php index 39e7f51f7..29e7832e5 100644 --- a/src/Entity/IssueProduct.php +++ b/src/Entity/IssueProduct.php @@ -18,7 +18,7 @@ class IssueProduct extends AbstractBaseEntity private ?Product $product = null; #[ORM\Column] - private ?float $quantity; + private float $quantity = 0.0; #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $description = null; diff --git a/src/Entity/Product.php b/src/Entity/Product.php index 0eafecbba..bbdd68e26 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -14,7 +14,7 @@ class Product extends AbstractBaseEntity { #[ORM\Column(length: 255)] #[Assert\NotBlank] - private ?string $name = null; + private string $name = ''; #[ORM\ManyToOne(inversedBy: 'products')] #[ORM\JoinColumn(nullable: false)] @@ -24,7 +24,7 @@ class Product extends AbstractBaseEntity #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2)] #[Assert\GreaterThanOrEqual(0)] #[Assert\LessThan(1_000_000)] - private ?string $price = null; + private string $price = '0'; /** @var Collection */ #[ORM\OneToMany(mappedBy: 'product', targetEntity: IssueProduct::class, orphanRemoval: true)] diff --git a/src/Entity/Project.php b/src/Entity/Project.php index c3284ceea..db2a5b092 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -18,17 +18,17 @@ class Project extends AbstractBaseEntity use DataProviderTrait; use SynchronizedEntityTrait; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $name = null; /** @var Collection */ #[ORM\OneToMany(mappedBy: 'project', targetEntity: Invoice::class)] private Collection $invoices; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $projectTrackerProjectUrl; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $projectTrackerKey; #[ORM\Column(length: 255, nullable: true)] diff --git a/src/Entity/ProjectBilling.php b/src/Entity/ProjectBilling.php index 6da67343f..e71b61a90 100644 --- a/src/Entity/ProjectBilling.php +++ b/src/Entity/ProjectBilling.php @@ -12,12 +12,12 @@ class ProjectBilling extends AbstractBaseEntity { #[ORM\Column(length: 255)] - private ?string $name = null; + private string $name = ''; - #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $periodStart = null; - #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $periodEnd = null; /** @var Collection */ @@ -29,7 +29,7 @@ class ProjectBilling extends AbstractBaseEntity private ?Project $project = null; #[ORM\Column] - private ?bool $recorded = null; + private bool $recorded = false; #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $description = null; diff --git a/src/Entity/ProjectVersionBudget.php b/src/Entity/ProjectVersionBudget.php index 744b91fbd..5c059bc0a 100644 --- a/src/Entity/ProjectVersionBudget.php +++ b/src/Entity/ProjectVersionBudget.php @@ -9,13 +9,13 @@ class ProjectVersionBudget extends AbstractBaseEntity { #[ORM\Column(length: 255)] - private ?string $projectId = null; + private string $projectId = ''; #[ORM\Column(length: 255)] - private ?string $versionId = null; + private string $versionId = ''; #[ORM\Column] - private ?float $budget = null; + private float $budget = 0.0; public function getProjectId(): ?string { diff --git a/src/Entity/ServiceAgreement.php b/src/Entity/ServiceAgreement.php index 7dfff77ff..431dab16d 100644 --- a/src/Entity/ServiceAgreement.php +++ b/src/Entity/ServiceAgreement.php @@ -27,26 +27,26 @@ class ServiceAgreement extends AbstractBaseEntity private ?CybersecurityAgreement $cybersecurityAgreement = null; #[ORM\Column(enumType: HostingProviderEnum::class)] - private ?HostingProviderEnum $hostingProvider = null; + private HostingProviderEnum $hostingProvider = HostingProviderEnum::ADM; #[ORM\Column(length: 255, nullable: true)] private ?string $documentUrl = null; #[ORM\Column] - private ?float $price = null; + private float $price = 0.0; #[ORM\ManyToOne(targetEntity: Worker::class)] #[ORM\JoinColumn(nullable: false)] private ?Worker $projectLead = null; - #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $validFrom = null; #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $validTo = null; #[ORM\Column] - private ?bool $isActive = null; + private bool $isActive = false; /** @var array */ #[ORM\Column(type: Types::JSON)] diff --git a/src/Entity/Subscription.php b/src/Entity/Subscription.php index 62aad81c3..219efcb30 100644 --- a/src/Entity/Subscription.php +++ b/src/Entity/Subscription.php @@ -12,7 +12,7 @@ class Subscription extends AbstractBaseEntity { #[ORM\Column(length: 180, unique: false)] - private ?string $email = null; + private string $email = ''; #[ORM\Column(type: 'string', nullable: true, enumType: SubscriptionSubjectEnum::class)] private ?SubscriptionSubjectEnum $subject = null; diff --git a/src/Entity/User.php b/src/Entity/User.php index a043ff8ba..1e8dbc38e 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -15,14 +15,14 @@ class User implements UserInterface private ?int $id = null; #[ORM\Column(length: 180, unique: true)] - private ?string $email = null; + private string $email = ''; /** @var array */ #[ORM\Column] private array $roles = []; #[ORM\Column(length: 255)] - private ?string $name = null; + private string $name = ''; public function __construct() { diff --git a/src/Entity/Version.php b/src/Entity/Version.php index 07af3594c..cbe423db4 100644 --- a/src/Entity/Version.php +++ b/src/Entity/Version.php @@ -18,10 +18,10 @@ class Version extends AbstractBaseEntity use DataProviderTrait; use SynchronizedEntityTrait; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $name = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $projectTrackerId = null; #[ORM\ManyToOne(inversedBy: 'versions')] @@ -32,7 +32,7 @@ class Version extends AbstractBaseEntity #[ORM\ManyToMany(targetEntity: Issue::class, mappedBy: 'versions')] private Collection $issues; - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?bool $isBillable = true; public function __construct() diff --git a/src/Entity/Worker.php b/src/Entity/Worker.php index 7f95c2489..8d7fbb324 100644 --- a/src/Entity/Worker.php +++ b/src/Entity/Worker.php @@ -17,7 +17,7 @@ class Worker private ?int $id = null; #[ORM\Column(length: 180, unique: true)] - private ?string $email = null; + private string $email = ''; #[ORM\Column(length: 180, nullable: true)] private ?float $workload = null; @@ -41,7 +41,7 @@ public function __construct() public function __toString(): string { - return (string) ($this->name ?? $this->email ?? $this->id); + return (string) ($this->name ?: $this->email ?: $this->id); } public function getId(): ?int diff --git a/src/Entity/WorkerGroup.php b/src/Entity/WorkerGroup.php index 4d04540ab..4a3bdec03 100644 --- a/src/Entity/WorkerGroup.php +++ b/src/Entity/WorkerGroup.php @@ -17,7 +17,7 @@ class WorkerGroup private ?int $id = null; #[ORM\Column(length: 255, unique: true)] - private ?string $name = null; + private string $name = ''; /** * @var Collection @@ -49,7 +49,7 @@ public function setName(string $name): static public function __toString(): string { - return (string) ($this->name ?? $this->id); + return (string) ($this->name ?: $this->id); } /** diff --git a/src/Entity/Worklog.php b/src/Entity/Worklog.php index 95d65c0e9..bba01a670 100644 --- a/src/Entity/Worklog.php +++ b/src/Entity/Worklog.php @@ -21,7 +21,7 @@ class Worklog extends AbstractBaseEntity use SynchronizedEntityTrait; // TODO: Rename to projectTrackerId. - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $worklogId = null; #[ORM\ManyToOne(inversedBy: 'worklogs')] @@ -34,13 +34,13 @@ class Worklog extends AbstractBaseEntity #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $worker = null; - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $timeSpentSeconds = null; - #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $started = null; #[ORM\ManyToOne(inversedBy: 'worklogs')] @@ -54,7 +54,7 @@ class Worklog extends AbstractBaseEntity #[ORM\JoinColumn(nullable: false)] private ?Issue $issue = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $projectTrackerIssueId = null; #[ORM\Column(type: 'string', nullable: true, enumType: BillableKindsEnum::class)] From 46879ef9142f6f90498357fb67bc88fa2aacc905 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 17:23:02 +0200 Subject: [PATCH 08/23] 7273: Relaxed 11 ManyToOne foreign keys from `NOT NULL` to `NULL` to match the property types they bind to. --- CHANGELOG.md | 13 ++++++ migrations/Version20260517151632.php | 45 ++++++++++++++++++ phpstan-baseline.neon | 66 --------------------------- src/Entity/CybersecurityAgreement.php | 2 +- src/Entity/InvoiceEntry.php | 2 +- src/Entity/IssueProduct.php | 4 +- src/Entity/Product.php | 2 +- src/Entity/ProjectBilling.php | 2 +- src/Entity/ServiceAgreement.php | 6 +-- src/Entity/Version.php | 2 +- src/Entity/Worklog.php | 2 +- 11 files changed, 69 insertions(+), 77 deletions(-) create mode 100644 migrations/Version20260517151632.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c7be6684..7c8be7a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Migration `Version20260517151632`: relaxed 11 ManyToOne foreign keys + from `NOT NULL` to `NULL` to match the property types they bind to + (PHPStan `doctrine.associationType`, Pattern C — 11 entries cleared, + baseline 201 → 190). Affected: `cybersecurity_agreement.service_agreement_id`, + `invoice_entry.invoice_id`, `issue_product.{issue_id,product_id}`, + `product.project_id`, `project_billing.project_id`, + `service_agreement.{project_id,client_id,project_lead_id}`, + `version.project_id`, `worklog.issue_id`. Application-layer validation + (`#[Assert\NotNull]` on `Product::$project`, form validators on the + others) continues to enforce required-ness; the DB now reflects the + truthful "may be null in-memory mid-construction" state instead of a + constraint that PHP couldn't satisfy. + * Aligned Doctrine entity property types with database column nullability (PHPStan `doctrine.columnType`/`doctrine.associationType`, Pattern B — 43 entries cleared, baseline 243 → 201). Two-part change: diff --git a/migrations/Version20260517151632.php b/migrations/Version20260517151632.php new file mode 100644 index 000000000..e13f2d9c2 --- /dev/null +++ b/migrations/Version20260517151632.php @@ -0,0 +1,45 @@ +addSql('ALTER TABLE cybersecurity_agreement CHANGE service_agreement_id service_agreement_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE invoice_entry CHANGE invoice_id invoice_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE issue_product CHANGE issue_id issue_id INT DEFAULT NULL, CHANGE product_id product_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE product CHANGE project_id project_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE project_billing CHANGE project_id project_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE service_agreement CHANGE project_id project_id INT DEFAULT NULL, CHANGE client_id client_id INT DEFAULT NULL, CHANGE project_lead_id project_lead_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE version CHANGE project_id project_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE worklog CHANGE issue_id issue_id INT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE invoice_entry CHANGE invoice_id invoice_id INT NOT NULL'); + $this->addSql('ALTER TABLE worklog CHANGE issue_id issue_id INT NOT NULL'); + $this->addSql('ALTER TABLE product CHANGE project_id project_id INT NOT NULL'); + $this->addSql('ALTER TABLE issue_product CHANGE issue_id issue_id INT NOT NULL, CHANGE product_id product_id INT NOT NULL'); + $this->addSql('ALTER TABLE project_billing CHANGE project_id project_id INT NOT NULL'); + $this->addSql('ALTER TABLE version CHANGE project_id project_id INT NOT NULL'); + $this->addSql('ALTER TABLE service_agreement CHANGE project_id project_id INT NOT NULL, CHANGE client_id client_id INT NOT NULL, CHANGE project_lead_id project_lead_id INT NOT NULL'); + $this->addSql('ALTER TABLE cybersecurity_agreement CHANGE service_agreement_id service_agreement_id INT NOT NULL'); + } +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c9fe3a456..904b8101b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -66,78 +66,12 @@ parameters: count: 1 path: src/DataFixtures/AppFixtures.php - - - message: '#^Property App\\Entity\\CybersecurityAgreement\:\:\$serviceAgreement type mapping mismatch\: property can contain App\\Entity\\ServiceAgreement\|null but database expects App\\Entity\\ServiceAgreement\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/CybersecurityAgreement.php - - - - message: '#^Property App\\Entity\\InvoiceEntry\:\:\$invoice type mapping mismatch\: property can contain App\\Entity\\Invoice\|null but database expects App\\Entity\\Invoice\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/InvoiceEntry.php - - - - message: '#^Property App\\Entity\\IssueProduct\:\:\$issue type mapping mismatch\: property can contain App\\Entity\\Issue\|null but database expects App\\Entity\\Issue\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/IssueProduct.php - - - - message: '#^Property App\\Entity\\IssueProduct\:\:\$product type mapping mismatch\: property can contain App\\Entity\\Product\|null but database expects App\\Entity\\Product\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/IssueProduct.php - - - - message: '#^Property App\\Entity\\Product\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Product.php - - - - message: '#^Property App\\Entity\\ProjectBilling\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/ProjectBilling.php - - - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$client type mapping mismatch\: property can contain App\\Entity\\Client\|null but database expects App\\Entity\\Client\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/ServiceAgreement.php - - - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/ServiceAgreement.php - - - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$projectLead type mapping mismatch\: property can contain App\\Entity\\Worker\|null but database expects App\\Entity\\Worker\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/ServiceAgreement.php - - message: '#^Strict comparison using \=\=\= between null and string will always evaluate to false\.$#' identifier: identical.alwaysFalse count: 1 path: src/Entity/User.php - - - message: '#^Property App\\Entity\\Version\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Version.php - - - - message: '#^Property App\\Entity\\Worklog\:\:\$issue type mapping mismatch\: property can contain App\\Entity\\Issue\|null but database expects App\\Entity\\Issue\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/Worklog.php - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$headline with no type specified\.$#' identifier: missingType.parameter diff --git a/src/Entity/CybersecurityAgreement.php b/src/Entity/CybersecurityAgreement.php index 34a9d473c..4ed07f8c1 100644 --- a/src/Entity/CybersecurityAgreement.php +++ b/src/Entity/CybersecurityAgreement.php @@ -14,7 +14,7 @@ class CybersecurityAgreement private ?int $id = null; #[ORM\ManyToOne(targetEntity: ServiceAgreement::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?ServiceAgreement $serviceAgreement = null; #[ORM\Column(nullable: true)] diff --git a/src/Entity/InvoiceEntry.php b/src/Entity/InvoiceEntry.php index f45a80a9c..e1cf71fa0 100644 --- a/src/Entity/InvoiceEntry.php +++ b/src/Entity/InvoiceEntry.php @@ -15,7 +15,7 @@ class InvoiceEntry extends AbstractBaseEntity { #[ORM\ManyToOne(inversedBy: 'invoiceEntries')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Invoice $invoice = null; #[ORM\Column(type: Types::INTEGER, name: 'entry_index')] diff --git a/src/Entity/IssueProduct.php b/src/Entity/IssueProduct.php index 29e7832e5..cd51a289a 100644 --- a/src/Entity/IssueProduct.php +++ b/src/Entity/IssueProduct.php @@ -10,11 +10,11 @@ class IssueProduct extends AbstractBaseEntity { #[ORM\ManyToOne(inversedBy: 'products')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Issue $issue = null; #[ORM\ManyToOne(inversedBy: 'issues')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Product $product = null; #[ORM\Column] diff --git a/src/Entity/Product.php b/src/Entity/Product.php index bbdd68e26..20271d304 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -17,7 +17,7 @@ class Product extends AbstractBaseEntity private string $name = ''; #[ORM\ManyToOne(inversedBy: 'products')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] #[Assert\NotNull] private ?Project $project = null; diff --git a/src/Entity/ProjectBilling.php b/src/Entity/ProjectBilling.php index e71b61a90..366d3ec96 100644 --- a/src/Entity/ProjectBilling.php +++ b/src/Entity/ProjectBilling.php @@ -25,7 +25,7 @@ class ProjectBilling extends AbstractBaseEntity private Collection $invoices; #[ORM\ManyToOne(inversedBy: 'projectBillings')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Project $project = null; #[ORM\Column] diff --git a/src/Entity/ServiceAgreement.php b/src/Entity/ServiceAgreement.php index 431dab16d..39c0d960a 100644 --- a/src/Entity/ServiceAgreement.php +++ b/src/Entity/ServiceAgreement.php @@ -15,11 +15,11 @@ class ServiceAgreement extends AbstractBaseEntity { #[ORM\ManyToOne(targetEntity: Project::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Project $project = null; #[ORM\ManyToOne(targetEntity: Client::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Client $client = null; #[ORM\ManyToOne(targetEntity: CybersecurityAgreement::class)] @@ -36,7 +36,7 @@ class ServiceAgreement extends AbstractBaseEntity private float $price = 0.0; #[ORM\ManyToOne(targetEntity: Worker::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Worker $projectLead = null; #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] diff --git a/src/Entity/Version.php b/src/Entity/Version.php index cbe423db4..e04c41eb7 100644 --- a/src/Entity/Version.php +++ b/src/Entity/Version.php @@ -25,7 +25,7 @@ class Version extends AbstractBaseEntity private ?string $projectTrackerId = null; #[ORM\ManyToOne(inversedBy: 'versions')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Project $project = null; /** @var Collection */ diff --git a/src/Entity/Worklog.php b/src/Entity/Worklog.php index bba01a670..454762192 100644 --- a/src/Entity/Worklog.php +++ b/src/Entity/Worklog.php @@ -51,7 +51,7 @@ class Worklog extends AbstractBaseEntity private ?int $billedSeconds = null; #[ORM\ManyToOne(inversedBy: 'worklogs')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Issue $issue = null; #[ORM\Column(length: 255, nullable: true)] From 3257d79586abe9fd55887770caff7801607e296f Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 17:27:39 +0200 Subject: [PATCH 09/23] 7273: Typed parameters --- CHANGELOG.md | 9 +++++ phpstan-baseline.neon | 36 ------------------- src/Model/Reports/HourReportProjectTicket.php | 2 +- src/Service/HourReportService.php | 8 ++--- 4 files changed, 14 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8be7a94..5b638cd34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Typed `HourReportProjectTicket::__construct()` parameters (PHPStan + `missingType.parameter`, 6 entries — baseline 190 → 184). The + constructor now requires `string $id, string $projectTrackerId, + string $headline, float $totalEstimated, float $totalSpent, + string $linkToIssue` to match the property types. Caller in + `HourReportService::getHourReport()` coerces `Issue` getter return + values (`(string) $issue->getId()`, `?? ''` fallbacks for nullable + trackers/name/link) — same pattern as `ForecastReportService`. + * Migration `Version20260517151632`: relaxed 11 ManyToOne foreign keys from `NOT NULL` to `NULL` to match the property types they bind to (PHPStan `doctrine.associationType`, Pattern C — 11 entries cleared, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 904b8101b..a25b3aa00 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -72,42 +72,6 @@ parameters: count: 1 path: src/Entity/User.php - - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$headline with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Model/Reports/HourReportProjectTicket.php - - - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$id with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Model/Reports/HourReportProjectTicket.php - - - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$linkToIssue with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Model/Reports/HourReportProjectTicket.php - - - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$projectTrackerId with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Model/Reports/HourReportProjectTicket.php - - - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$totalEstimated with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Model/Reports/HourReportProjectTicket.php - - - - message: '#^Method App\\Model\\Reports\\HourReportProjectTicket\:\:__construct\(\) has parameter \$totalSpent with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/Model/Reports/HourReportProjectTicket.php - - message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' identifier: method.nonObject diff --git a/src/Model/Reports/HourReportProjectTicket.php b/src/Model/Reports/HourReportProjectTicket.php index 66405e269..a6e3efcc4 100644 --- a/src/Model/Reports/HourReportProjectTicket.php +++ b/src/Model/Reports/HourReportProjectTicket.php @@ -17,7 +17,7 @@ class HourReportProjectTicket /** @var ArrayCollection */ public ArrayCollection $projectTickets; - public function __construct($id, $projectTrackerId, $headline, $totalEstimated, $totalSpent, $linkToIssue) + public function __construct(string $id, string $projectTrackerId, string $headline, float $totalEstimated, float $totalSpent, string $linkToIssue) { $this->id = $id; $this->projectTrackerId = $projectTrackerId; diff --git a/src/Service/HourReportService.php b/src/Service/HourReportService.php index ea24fb2f2..99179527c 100644 --- a/src/Service/HourReportService.php +++ b/src/Service/HourReportService.php @@ -50,12 +50,12 @@ public function getHourReport(Project $project, ?\DateTimeInterface $fromDate, ? } $projectTicket = new HourReportProjectTicket( - $issue->getId(), - $issue->getProjectTrackerId(), - $issue->getName(), + (string) $issue->getId(), + $issue->getProjectTrackerId() ?? '', + $issue->getName() ?? '', $totalTicketEstimated, $totalTicketSpent, - $issue->getLinkToIssue() + $issue->getLinkToIssue() ?? '' ); $projectTicket->timesheets->add($timesheets); From 55b61fdbde23b9c0948642c631aeca10e0e6e380 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 17:40:08 +0200 Subject: [PATCH 10/23] 7273: Report services now skip worklogs with no associated project or issue rather than dereferencing nullable getters --- CHANGELOG.md | 12 +++ phpstan-baseline.neon | 84 ------------------- .../BillableUnbilledHoursReportService.php | 15 +++- src/Service/ForecastReportService.php | 21 +++-- src/Service/InvoicingRateReportService.php | 22 +++-- 5 files changed, 53 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b638cd34..ad2c78456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Report services now skip worklogs with no associated project or issue + rather than dereferencing nullable getters (PHPStan `method.nonObject`, + 13 entries — baseline 184 → 165). Affected services: + `ForecastReportService::getForecastReport()`, + `BillableUnbilledHoursReportService::getReport()`, + `InvoicingRateReportService::getInvoicingRateReport()`. Each foreach + body caches `$project = $worklog->getProject()` and + `$issue = $worklog->getIssue()` once at the top, `continue`s when + either is null, and uses the narrowed references throughout — also + cleared 6 cascading `argument.type`/`property.notFound` follow-ons + that had been hiding behind the chained calls. + * Typed `HourReportProjectTicket::__construct()` parameters (PHPStan `missingType.parameter`, 6 entries — baseline 190 → 184). The constructor now requires `string $id, string $projectTrackerId, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a25b3aa00..35a5b27ba 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -72,30 +72,6 @@ parameters: count: 1 path: src/Entity/User.php - - - message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/BillableUnbilledHoursReportService.php - - - - message: '#^Cannot call method getName\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/BillableUnbilledHoursReportService.php - - - - message: '#^Cannot call method getName\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/BillableUnbilledHoursReportService.php - - - - message: '#^Cannot call method getProjectTrackerId\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/BillableUnbilledHoursReportService.php - - message: '#^Parameter \#2 \$timestamp of function date expects int\|null, int\|false given\.$#' identifier: argument.type @@ -120,72 +96,12 @@ parameters: count: 1 path: src/Service/ForecastReportService.php - - - message: '#^Cannot call method getEpics\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 2 - path: src/Service/ForecastReportService.php - - - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Cannot call method getProjectTrackerKey\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Cannot call method getVersions\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/ForecastReportService.php - - message: '#^Parameter \#1 \$projectId of class App\\Model\\Reports\\ForecastReportProjectData constructor expects string, int\\|int\<1, max\> given\.$#' identifier: argument.type count: 1 path: src/Service/ForecastReportService.php - - - message: '#^Parameter \#2 \$array of function array_map expects array, Doctrine\\Common\\Collections\\Collection\ given\.$#' - identifier: argument.type - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Cannot call method getLinkToIssue\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 2 - path: src/Service/InvoicingRateReportService.php - - - - message: '#^Cannot call method getName\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 2 - path: src/Service/InvoicingRateReportService.php - - - - message: '#^Cannot call method getName\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 2 - path: src/Service/InvoicingRateReportService.php - - - - message: '#^Cannot call method getProjectTrackerId\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 2 - path: src/Service/InvoicingRateReportService.php - - message: '#^Parameter \#1 \$key of method Doctrine\\Common\\Collections\\ArrayCollection\\:\:set\(\) expects string, int given\.$#' identifier: argument.type diff --git a/src/Service/BillableUnbilledHoursReportService.php b/src/Service/BillableUnbilledHoursReportService.php index ae370457b..89693b373 100644 --- a/src/Service/BillableUnbilledHoursReportService.php +++ b/src/Service/BillableUnbilledHoursReportService.php @@ -37,16 +37,23 @@ public function getBillableUnbilledHoursReport( $totalHoursForAllProjects = 0; foreach ($billableWorklogs as $billableWorklog) { - $projectName = $billableWorklog->getProject()->getName(); - $issueName = $billableWorklog->getIssue()->getName(); + $project = $billableWorklog->getProject(); + $issue = $billableWorklog->getIssue(); + + if (null === $project || null === $issue) { + continue; + } + + $projectName = $project->getName(); + $issueName = $issue->getName(); // Initialize issue data if not already set if (!isset($projectData[$projectName][$issueName])) { $projectData[$projectName][$issueName] = [ 'worklogs' => [], 'totalHours' => 0, - 'id' => $billableWorklog->getIssue()->getProjectTrackerId(), - 'linkToIssue' => $billableWorklog->getIssue()->getLinkToIssue(), + 'id' => $issue->getProjectTrackerId(), + 'linkToIssue' => $issue->getLinkToIssue(), ]; } diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index f42822c38..082e6afe0 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -48,7 +48,14 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa foreach ($invoiceAttachedWorklogs['paginator'] as $worklog) { // Loop through each worklog - $projectId = $worklog->getProject()->getId(); + $project = $worklog->getProject(); + $issue = $worklog->getIssue(); + + if (null === $project || null === $issue) { + continue; + } + + $projectId = $project->getId(); if (!$projectId) { throw new \Exception('Project id is null'); @@ -56,7 +63,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa // If the project isn't already in the forecast, add it if (!isset($forecastReportData->projects[$projectId])) { $newForecastReportProjectData = new ForecastReportProjectData($projectId); - $newForecastReportProjectData->projectName = $worklog->getProject()?->getName() ?? '[no project name]'; + $newForecastReportProjectData->projectName = $project->getName() ?? '[no project name]'; $forecastReportData->projects[$projectId] = $newForecastReportProjectData; } // Get current project from forecast @@ -75,11 +82,11 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa } // Get issue details from the worklog - $issueId = $worklog->getIssue()->getProjectTrackerKey() ?? '[no issue id]'; - $issueLink = $worklog->getIssue()->getLinkToIssue() ?? '[no issue link]'; + $issueId = $issue->getProjectTrackerKey() ?? '[no issue id]'; + $issueLink = $issue->getLinkToIssue() ?? '[no issue link]'; - if ($worklog->getIssue()->getEpics()->count() > 0) { - $issueTag = implode(',', array_map(fn ($epic) => $epic->getName(), $worklog->getIssue()->getEpics())); + if ($issue->getEpics()->count() > 0) { + $issueTag = implode(',', array_map(fn ($epic) => $epic->getName(), $issue->getEpics()->toArray())); } else { $issueTag = '[no tag]'; } @@ -101,7 +108,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa } // Get version details from the issue - $issueVersions = $worklog->getIssue()->getVersions(); + $issueVersions = $issue->getVersions(); $issueVersion = count($issueVersions) > 0 ? implode(', ', array_map(function ($version) { return $version->getName(); }, $issueVersions->toArray())) : '[no version]'; $issueVersionIdentifier = $issueTag.$issueVersion; diff --git a/src/Service/InvoicingRateReportService.php b/src/Service/InvoicingRateReportService.php index 5997ca7e6..4875ce2cf 100644 --- a/src/Service/InvoicingRateReportService.php +++ b/src/Service/InvoicingRateReportService.php @@ -101,12 +101,17 @@ public function getInvoicingRateReport( // Tally up billable logged hours in gathered worklogs for current period $loggedBillableHours = 0; foreach ($billableWorklogs as $billableWorklog) { - $projectName = $billableWorklog->getProject()->getName(); - $issueName = $billableWorklog->getIssue()->getName(); + $project = $billableWorklog->getProject(); + $issue = $billableWorklog->getIssue(); + if (null === $project || null === $issue) { + continue; + } + $projectName = $project->getName(); + $issueName = $issue->getName(); $workerProjects[$projectName][$period]['loggedBillableHours'] = ($workerProjects[$projectName][$period]['loggedBillableHours'] ?? 0) + ($billableWorklog->getTimeSpentSeconds() * self::SECONDS_TO_HOURS); if ($includeIssues) { $workerProjects[$projectName][$issueName][$period]['loggedBillableHours'] = ($workerProjects[$projectName][$issueName][$period]['loggedBillableHours'] ?? 0) + ($billableWorklog->getTimeSpentSeconds() * self::SECONDS_TO_HOURS); - $workerProjects[$projectName][$issueName]['linkToissue'][$billableWorklog->getIssue()->getProjectTrackerId()] = $billableWorklog->getIssue()->getLinkToIssue(); + $workerProjects[$projectName][$issueName]['linkToissue'][$issue->getProjectTrackerId()] = $issue->getLinkToIssue(); } $loggedBillableHours += ($billableWorklog->getTimeSpentSeconds() * self::SECONDS_TO_HOURS); } @@ -114,12 +119,17 @@ public function getInvoicingRateReport( // Tally up billed logged hours in gathered worklogs for current period $loggedBilledHours = 0; foreach ($billedWorklogs as $billedWorklog) { - $projectName = $billedWorklog->getProject()->getName(); - $issueName = $billedWorklog->getIssue()->getName(); + $project = $billedWorklog->getProject(); + $issue = $billedWorklog->getIssue(); + if (null === $project || null === $issue) { + continue; + } + $projectName = $project->getName(); + $issueName = $issue->getName(); $workerProjects[$projectName][$period]['loggedBilledHours'] = ($workerProjects[$projectName][$period]['loggedBilledHours'] ?? 0) + ($billedWorklog->getTimeSpentSeconds() * self::SECONDS_TO_HOURS); if ($includeIssues) { $workerProjects[$projectName][$issueName][$period]['loggedBilledHours'] = ($workerProjects[$projectName][$issueName][$period]['loggedBilledHours'] ?? 0) + ($billedWorklog->getTimeSpentSeconds() * self::SECONDS_TO_HOURS); - $workerProjects[$projectName][$issueName]['linkToissue'][$billedWorklog->getIssue()->getProjectTrackerId()] = $billedWorklog->getIssue()->getLinkToIssue(); + $workerProjects[$projectName][$issueName]['linkToissue'][$issue->getProjectTrackerId()] = $issue->getLinkToIssue(); } $loggedBilledHours += ($billedWorklog->getTimeSpentSeconds() * self::SECONDS_TO_HOURS); } From 42bc2bbe23bee544da9cbd48f7993e4d80758a3a Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 21:23:25 +0200 Subject: [PATCH 11/23] 7273: Fixed more phpstan raised issues --- CHANGELOG.md | 40 ++++ phpstan-baseline.neon | 226 +++--------------- src/Controller/InvoiceController.php | 11 +- src/Controller/ManagementReportController.php | 11 +- src/Controller/ProjectBillingController.php | 14 +- src/Controller/SubscriptionController.php | 6 + src/Service/LeantimeApiService.php | 14 +- tests/Service/DanishHolidayHelperTest.php | 12 +- tests/Service/DataProviderServiceTest.php | 17 +- tests/Service/LeantimeApiServiceTest.php | 59 +++-- tests/Service/ProjectBillingServiceTest.php | 7 +- tests/Service/WorkloadReportServiceTest.php | 15 +- 12 files changed, 191 insertions(+), 241 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2c78456..b281a391f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Test fixtures and controller endpoints tightened to satisfy PHPStan + call-site checks (`argument.type`, `method.notFound`, `method.nonObject` — + baseline 163 → 75). Highlights: + - `LeantimeApiServiceTest`, `DataProviderServiceTest`, and + `ProjectBillingServiceTest` now `\assert(... instanceof ...)` after each + `$container->get()` so PHPStan knows the concrete service type; this + cascaded through ~20 previously-untyped `->persist()`/`->flush()`/ + `->findOneBy()` calls on `object`. Also fixed a `@var BillingService + $projectBillingService` typo in `ProjectBillingServiceTest` that mis-typed + `$billingService`, hiding 4 method-not-found errors. + - `LeantimeApiServiceTest` now passes `'64'`/`'65'`/etc. (string) instead of + `64`/`65` to `setProjectTrackerId()`/`setProjectTrackerKey()`/ + `setProjectTrackerIssueId()`, and asserts non-null on `$dataProvider->getId()` + once after `flush()` rather than per call. Also handles + `\DateTime::createFromFormat()` returning `\DateTime|false`. + - `DanishHolidayHelperTest`: tightened `$expected` from `?\DateTimeInterface` + to `\DateTimeInterface` on three test methods (and the matching data + provider shape) since the data is never null. + - `WorkloadReportServiceTest`: asserts `mktime()` did not return `false` + before passing to `date()`. + - `InvoiceController` and `ProjectBillingController`: null-guard + `$invoice->getId()` (`?int`) before building invoice-id arrays passed to + `BillingService::generateSpreadsheet{Csv,Html}()`; `array_filter` to drop + nulls from `array_map` over invoice collections; `array_map(intval(...))` + on string id list from `$request->query->get('ids')`. + - `SubscriptionController::check()` returns a 400 when `$user->getEmail()` + is `null` rather than passing `?string` downstream; `subscriptionHandler()` + throws when `key($content)` is `null` rather than handing `null` to + `SubscriptionSubjectEnum::tryFrom()`. + - `ManagementReportController::createGroupedInvoices()` skips invoices with + a null `recordedDate`, and casts the `format('Y')`/`format('n')` string + parts to `int` so the grouped array keys match the + `ManagementReportService::generateSpreadsheetCsvResponse()` signature. + +* `LeantimeApiService::update()` and `::delete()` now skip data providers + with a null ID rather than passing `int|null` to the `LeantimeUpdateMessage` + / `LeantimeDeleteMessage` constructors (PHPStan `argument.type`, 2 entries — + baseline 165 → 163). Persisted entities always have an ID; the guard is + defensive against transient instances. + * Report services now skip worklogs with no associated project or issue rather than dereferencing nullable getters (PHPStan `method.nonObject`, 13 entries — baseline 184 → 165). Affected services: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 35a5b27ba..3c4ca3092 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,65 +1,11 @@ parameters: ignoreErrors: - - - message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetCsvResponse\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/InvoiceController.php - - - - message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetCsvResponse\(\) expects array\, list\ given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/InvoiceController.php - - - - message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetHtml\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/InvoiceController.php - - - - message: '#^Cannot call method format\(\) on DateTimeInterface\|null\.$#' - identifier: method.nonObject - count: 2 - path: src/Controller/ManagementReportController.php - - message: '#^Parameter \#1 \$groupedInvoices of method App\\Service\\ManagementReportService\:\:generateSpreadsheetCsvResponse\(\) expects array\\>, array\\> given\.$#' identifier: argument.type count: 1 path: src/Controller/ManagementReportController.php - - - message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetCsvResponse\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/ProjectBillingController.php - - - - message: '#^Parameter \#1 \$ids of method App\\Service\\BillingService\:\:generateSpreadsheetHtml\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/ProjectBillingController.php - - - - message: '#^Parameter \#1 \$email of method App\\Repository\\SubscriptionRepository\:\:findByCustom\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/SubscriptionController.php - - - - message: '#^Parameter \#1 \$userEmail of method App\\Controller\\SubscriptionController\:\:subscriptionHandler\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/SubscriptionController.php - - - - message: '#^Parameter \#1 \$value of static method App\\Enum\\SubscriptionSubjectEnum\:\:tryFrom\(\) expects int\|string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/SubscriptionController.php - - message: '#^Parameter \#1 \$started of method App\\Entity\\Worklog\:\:setStarted\(\) expects DateTimeInterface, DateTime\|false given\.$#' identifier: argument.type @@ -246,18 +192,6 @@ parameters: count: 1 path: src/Service/LeantimeApiService.php - - - message: '#^Parameter \#1 \$dataProviderId of class App\\Message\\LeantimeDeleteMessage constructor expects int, int\|null given\.$#' - identifier: argument.type - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Parameter \#4 \$dataProviderId of class App\\Message\\LeantimeUpdateMessage constructor expects int, int\|null given\.$#' - identifier: argument.type - count: 1 - path: src/Service/LeantimeApiService.php - - message: '#^Parameter \#2 \$week of method App\\Service\\PlanningService\:\:processIssuesForWeek\(\) expects int, int\|string given\.$#' identifier: argument.type @@ -270,42 +204,6 @@ parameters: count: 1 path: src/Service/WorkloadReportService.php - - - message: '#^Parameter \#1 \$expected of method App\\Tests\\Service\\DanishHolidayHelperTest\:\:assertSameDate\(\) expects DateTimeInterface, DateTimeInterface\|null given\.$#' - identifier: argument.type - count: 3 - path: tests/Service/DanishHolidayHelperTest.php - - - - message: '#^Call to an undefined method object\:\:clear\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method object\:\:flush\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method object\:\:persist\(\)\.$#' - identifier: method.notFound - count: 6 - path: tests/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method object\:\:upsertIssue\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Service/DataProviderServiceTest.php - - message: '#^Cannot call method getProjectTrackerId\(\) on App\\Entity\\Project\|null\.$#' identifier: method.nonObject @@ -313,151 +211,97 @@ parameters: path: tests/Service/DataProviderServiceTest.php - - message: '#^Parameter \$dataProviderId of class App\\Model\\DataProvider\\DataProviderIssueData constructor expects int, int\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method object\:\:clear\(\)\.$#' - identifier: method.notFound + message: '#^Cannot call method getSourceDeletedDate\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject count: 1 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Call to an undefined method object\:\:find\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Call to an undefined method object\:\:findAll\(\)\.$#' - identifier: method.notFound - count: 24 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 8 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Call to an undefined method object\:\:flush\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Call to an undefined method object\:\:persist\(\)\.$#' - identifier: method.notFound - count: 12 + message: '#^Cannot call method getSourceDeletedDate\(\) on App\\Entity\\Project\|null\.$#' + identifier: method.nonObject + count: 1 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#1 \$dataProviderId of method App\\Service\\LeantimeApiService\:\:deleteAsJob\(\) expects int, int\|null given\.$#' - identifier: argument.type + message: '#^Cannot call method getSourceDeletedDate\(\) on App\\Entity\\Worklog\|null\.$#' + identifier: method.nonObject count: 1 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#1 \$projectTrackerId of method App\\Entity\\Issue\:\:setProjectTrackerId\(\) expects string, int given\.$#' - identifier: argument.type + message: '#^Cannot call method getSourceModifiedDate\(\) on App\\Entity\\Issue\|null\.$#' + identifier: method.nonObject count: 2 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#1 \$projectTrackerId of method App\\Entity\\Project\:\:setProjectTrackerId\(\) expects string\|null, int given\.$#' - identifier: argument.type + message: '#^Cannot call method getSourceModifiedDate\(\) on App\\Entity\\Project\|null\.$#' + identifier: method.nonObject count: 2 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#1 \$projectTrackerId of method App\\Entity\\Version\:\:setProjectTrackerId\(\) expects string, int given\.$#' - identifier: argument.type + message: '#^Cannot call method getSourceModifiedDate\(\) on App\\Entity\\Version\|null\.$#' + identifier: method.nonObject count: 2 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#1 \$projectTrackerIssueId of method App\\Entity\\Worklog\:\:setProjectTrackerIssueId\(\) expects string, int given\.$#' - identifier: argument.type + message: '#^Cannot call method getSourceModifiedDate\(\) on App\\Entity\\Worklog\|null\.$#' + identifier: method.nonObject count: 2 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#1 \$projectTrackerKey of method App\\Entity\\Issue\:\:setProjectTrackerKey\(\) expects string, int given\.$#' - identifier: argument.type - count: 2 + message: '#^Cannot call method getTimestamp\(\) on DateTimeInterface\|null\.$#' + identifier: method.nonObject + count: 8 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#1 \$projectTrackerKey of method App\\Entity\\Project\:\:setProjectTrackerKey\(\) expects string\|null, int given\.$#' + message: '#^Parameter \#1 \$projectTrackerIssueId of method App\\Entity\\Worklog\:\:setProjectTrackerIssueId\(\) expects string, int given\.$#' identifier: argument.type - count: 2 + count: 1 path: tests/Service/LeantimeApiServiceTest.php - message: '#^Parameter \#1 \$started of method App\\Entity\\Worklog\:\:setStarted\(\) expects DateTimeInterface, DateTime\|false given\.$#' identifier: argument.type - count: 2 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Parameter \#2 \$messageBus of class App\\Service\\LeantimeApiService constructor expects Symfony\\Component\\Messenger\\MessageBusInterface, object given\.$#' - identifier: argument.type - count: 2 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Parameter \#3 \$dataProviderRepository of class App\\Service\\LeantimeApiService constructor expects App\\Repository\\DataProviderRepository, object given\.$#' - identifier: argument.type - count: 2 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Parameter \#4 \$dataProviderId of method App\\Service\\LeantimeApiService\:\:updateAsJob\(\) expects int, int\|null given\.$#' - identifier: argument.type - count: 8 + count: 1 path: tests/Service/LeantimeApiServiceTest.php - - message: '#^Parameter \#4 \$entityManager of class App\\Service\\LeantimeApiService constructor expects Doctrine\\ORM\\EntityManagerInterface, object given\.$#' - identifier: argument.type - count: 2 - path: tests/Service/LeantimeApiServiceTest.php + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNotNull\(\) with PhpOffice\\PhpSpreadsheet\\Spreadsheet will always evaluate to true\.$#' + identifier: method.alreadyNarrowedType + count: 1 + path: tests/Service/ProjectBillingServiceTest.php - - message: '#^Parameter \#5 \$projectRepository of class App\\Service\\LeantimeApiService constructor expects App\\Repository\\ProjectRepository, object given\.$#' + message: '#^Parameter \#1 \$invoiceIds of method App\\Service\\BillingService\:\:exportInvoicesToSpreadsheet\(\) expects array\, array\ given\.$#' identifier: argument.type - count: 2 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\BillingService\:\:createProjectBilling\(\)\.$#' - identifier: method.notFound count: 1 path: tests/Service/ProjectBillingServiceTest.php - - message: '#^Call to an undefined method App\\Service\\BillingService\:\:getIssuesNotIncludedInProjectBilling\(\)\.$#' - identifier: method.notFound + message: '#^Parameter \#1 \$project of method App\\Repository\\IssueRepository\:\:getClosedIssuesFromInterval\(\) expects App\\Entity\\Project, App\\Entity\\Project\|null given\.$#' + identifier: argument.type count: 1 path: tests/Service/ProjectBillingServiceTest.php - - message: '#^Call to an undefined method object\:\:exportInvoicesToSpreadsheet\(\)\.$#' - identifier: method.notFound + message: '#^Parameter \#1 \$projectBillingId of method App\\Service\\ProjectBillingService\:\:createProjectBilling\(\) expects int, int\|null given\.$#' + identifier: argument.type count: 1 path: tests/Service/ProjectBillingServiceTest.php - - message: '#^Call to an undefined method object\:\:getClosedIssuesFromInterval\(\)\.$#' - identifier: method.notFound + message: '#^Parameter \#2 \$periodStart of method App\\Repository\\IssueRepository\:\:getClosedIssuesFromInterval\(\) expects DateTimeInterface, DateTimeInterface\|null given\.$#' + identifier: argument.type count: 1 path: tests/Service/ProjectBillingServiceTest.php - - message: '#^Parameter \#2 \$timestamp of function date expects int\|null, int\|false given\.$#' + message: '#^Parameter \#3 \$periodEnd of method App\\Repository\\IssueRepository\:\:getClosedIssuesFromInterval\(\) expects DateTimeInterface, DateTimeInterface\|null given\.$#' identifier: argument.type - count: 3 - path: tests/Service/WorkloadReportServiceTest.php + count: 1 + path: tests/Service/ProjectBillingServiceTest.php diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index dddf5c287..8e7024a07 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -289,7 +289,9 @@ public function record(Request $request, Invoice $invoice): Response #[Route('/{id}/show-export', name: 'app_invoices_show_export', methods: ['GET'])] public function showExport(Request $request, Invoice $invoice): Response { - $html = $this->billingService->generateSpreadsheetHtml([$invoice->getId()]); + $invoiceId = $invoice->getId(); + \assert(null !== $invoiceId); + $html = $this->billingService->generateSpreadsheetHtml([$invoiceId]); return $this->render('invoices/export_show.html.twig', [ 'invoice' => $invoice, @@ -317,7 +319,10 @@ public function export(Invoice $invoice, InvoiceRepository $invoiceRepository): $invoice->setExportedDate(new \DateTime()); $invoiceRepository->save($invoice, true); - return $this->billingService->generateSpreadsheetCsvResponse([$invoice->getId()]); + $invoiceId = $invoice->getId(); + \assert(null !== $invoiceId); + + return $this->billingService->generateSpreadsheetCsvResponse([$invoiceId]); } /** @@ -358,6 +363,6 @@ public function exportSelection(Request $request, InvoiceRepository $invoiceRepo $entityManager->flush(); - return $this->billingService->generateSpreadsheetCsvResponse($ids); + return $this->billingService->generateSpreadsheetCsvResponse(array_map(intval(...), $ids)); } } diff --git a/src/Controller/ManagementReportController.php b/src/Controller/ManagementReportController.php index 30595abfb..7ff10a814 100644 --- a/src/Controller/ManagementReportController.php +++ b/src/Controller/ManagementReportController.php @@ -98,10 +98,13 @@ private function createGroupedInvoices(array $invoices): array $groupedInvoices = []; foreach ($invoices as $invoice) { $recordedDate = $invoice->getRecordedDate(); - $year = $recordedDate->format('Y'); - $month = $recordedDate->format('n'); - $yearQuarter = ceil($month / 3); - $groupedInvoices[$year][(int) $yearQuarter][] = $invoice; + if (null === $recordedDate) { + continue; + } + $year = (int) $recordedDate->format('Y'); + $month = (int) $recordedDate->format('n'); + $yearQuarter = (int) ceil($month / 3); + $groupedInvoices[$year][$yearQuarter][] = $invoice; } foreach ($groupedInvoices as $year => $quarters) { diff --git a/src/Controller/ProjectBillingController.php b/src/Controller/ProjectBillingController.php index 88e3e06c6..7fa30ee31 100644 --- a/src/Controller/ProjectBillingController.php +++ b/src/Controller/ProjectBillingController.php @@ -195,9 +195,10 @@ public function record(Request $request, ProjectBilling $projectBilling, Project #[Route('/{id}/show-export', name: 'app_project_billing_show_export', methods: ['GET'])] public function showExport(Request $request, ProjectBilling $projectBilling, BillingService $billingService): Response { - $ids = array_map(function ($invoice) { - return $invoice->getId(); - }, $projectBilling->getInvoices()->toArray()); + $ids = array_values(array_filter(array_map( + fn (Invoice $invoice) => $invoice->getId(), + $projectBilling->getInvoices()->toArray() + ), fn (?int $id) => null !== $id)); $html = $billingService->generateSpreadsheetHtml($ids); @@ -232,9 +233,10 @@ public function export(Request $request, ProjectBilling $projectBilling, Invoice $projectBilling->setExportedDate(new \DateTime()); $projectBillingRepository->save($projectBilling, true); - $ids = array_map(function ($invoice) { - return $invoice->getId(); - }, $invoices->toArray()); + $ids = array_values(array_filter(array_map( + fn (Invoice $invoice) => $invoice->getId(), + $invoices->toArray() + ), fn (?int $id) => null !== $id)); return $billingService->generateSpreadsheetCsvResponse($ids); } diff --git a/src/Controller/SubscriptionController.php b/src/Controller/SubscriptionController.php index 6f945db9d..d64929b51 100644 --- a/src/Controller/SubscriptionController.php +++ b/src/Controller/SubscriptionController.php @@ -78,6 +78,9 @@ public function check(User $user, Request $request): Response { $content = $request->toArray(); $userEmail = $user->getEmail(); + if (null === $userEmail) { + return new JsonResponse(['error' => 'User email is required.'], Response::HTTP_BAD_REQUEST); + } $reportType = key($content); $report = &$content[$reportType]; switch ($reportType) { @@ -131,6 +134,9 @@ public function check(User $user, Request $request): Response private function subscriptionHandler(string $userEmail, string $subscriptionType, array $content): JsonResponse { $reportType = key($content); + if (null === $reportType) { + throw new \InvalidArgumentException('Content is empty.'); + } $subscription = $this->subscriptionRepository->findOneByCustom($userEmail, $subscriptionType, $content); if ($subscription) { diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index f84a6ea0b..46e2dc261 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -75,13 +75,18 @@ public function update(string $className, bool $asyncJobQueue = false, ?\DateTim $dataProviders = $this->getEnabledLeantimeDataProviders(); foreach ($dataProviders as $dataProvider) { + $dataProviderId = $dataProvider->getId(); + if (null === $dataProviderId) { + continue; + } + $projectTrackerProjectIds = match ($className) { Project::class, Worker::class => null, default => $this->projectRepository->getProjectTrackerIdsByDataProviders([$dataProvider]), }; $this->messageBus->dispatch( - new LeantimeUpdateMessage($className, 0, $this::LIMIT, $dataProvider->getId(), $asyncJobQueue, $modifiedAfter, $projectTrackerProjectIds, $disableModifiedAtCheck), + new LeantimeUpdateMessage($className, 0, $this::LIMIT, $dataProviderId, $asyncJobQueue, $modifiedAfter, $projectTrackerProjectIds, $disableModifiedAtCheck), [new TransportNamesStamp($asyncJobQueue ? $this::QUEUE_ASYNC : $this::QUEUE_SYNC)], ); } @@ -92,8 +97,13 @@ public function delete(bool $asyncJobQueue = false, ?\DateTimeInterface $deleted $dataProviders = $this->getEnabledLeantimeDataProviders(); foreach ($dataProviders as $dataProvider) { + $dataProviderId = $dataProvider->getId(); + if (null === $dataProviderId) { + continue; + } + $this->messageBus->dispatch( - new LeantimeDeleteMessage($dataProvider->getId(), $asyncJobQueue, $deletedAfter), + new LeantimeDeleteMessage($dataProviderId, $asyncJobQueue, $deletedAfter), [new TransportNamesStamp($asyncJobQueue ? $this::QUEUE_ASYNC : $this::QUEUE_SYNC)], ); } diff --git a/tests/Service/DanishHolidayHelperTest.php b/tests/Service/DanishHolidayHelperTest.php index b9bdcd21d..b81aa3e40 100644 --- a/tests/Service/DanishHolidayHelperTest.php +++ b/tests/Service/DanishHolidayHelperTest.php @@ -79,14 +79,14 @@ public function testHolidayNames(): void /** * @dataProvider dataNextNonHoliday */ - public function testNextNonHoliday(\DateTimeInterface $date, ?\DateTimeInterface $expected): void + public function testNextNonHoliday(\DateTimeInterface $date, \DateTimeInterface $expected): void { $actual = $this->helper->getNextNonHoliday($date); $this->assertSameDate($expected, $actual); } /** - * @return iterable + * @return iterable */ public static function dataNextNonHoliday(): iterable { @@ -99,14 +99,14 @@ public static function dataNextNonHoliday(): iterable /** * @dataProvider dataNextBankDay */ - public function testNextBankDay(\DateTimeInterface $date, ?\DateTimeInterface $expected): void + public function testNextBankDay(\DateTimeInterface $date, \DateTimeInterface $expected): void { $actual = $this->helper->getNextBankDay($date); $this->assertSameDate($expected, $actual); } /** - * @return iterable + * @return iterable */ public static function dataNextBankDay(): iterable { @@ -134,14 +134,14 @@ public static function dataNextBankDay(): iterable /** * @dataProvider dataNextBankDay30 */ - public function testNextBankDay30(\DateTimeInterface $date, ?\DateTimeInterface $expected): void + public function testNextBankDay30(\DateTimeInterface $date, \DateTimeInterface $expected): void { $actual = $this->helper->getNextBankDay($date, 30); $this->assertSameDate($expected, $actual); } /** - * @return iterable + * @return iterable */ public static function dataNextBankDay30(): iterable { diff --git a/tests/Service/DataProviderServiceTest.php b/tests/Service/DataProviderServiceTest.php index 6bd5c2292..f49089339 100644 --- a/tests/Service/DataProviderServiceTest.php +++ b/tests/Service/DataProviderServiceTest.php @@ -27,6 +27,10 @@ public function testUpsertIssueProjectChangeMovesWorklogs(): void $worklogRepository = $container->get(WorklogRepository::class); $issueRepository = $container->get(IssueRepository::class); $service = $container->get(DataProviderService::class); + \assert($entityManager instanceof EntityManagerInterface); + \assert($worklogRepository instanceof WorklogRepository); + \assert($issueRepository instanceof IssueRepository); + \assert($service instanceof DataProviderService); // Create data provider. $dataProvider = new DataProvider(); @@ -99,9 +103,11 @@ public function testUpsertIssueProjectChangeMovesWorklogs(): void $this->assertEquals('proj-a', $worklogE->getProject()->getProjectTrackerId()); // Move issue C from project A to project B via upsertIssue. + $dataProviderId = $dataProvider->getId(); + \assert(null !== $dataProviderId); $service->upsertIssue(new DataProviderIssueData( projectTrackerId: 'issue-c', - dataProviderId: $dataProvider->getId(), + dataProviderId: $dataProviderId, projectTrackerProjectId: 'proj-b', name: 'Issue C', epics: [], @@ -122,12 +128,15 @@ public function testUpsertIssueProjectChangeMovesWorklogs(): void $entityManager->clear(); // Verify issue is now on project B. - $issue = $issueRepository->findOneBy(['projectTrackerId' => 'issue-c', 'dataProvider' => $dataProvider->getId()]); + $issue = $issueRepository->findOneBy(['projectTrackerId' => 'issue-c', 'dataProvider' => $dataProviderId]); + \assert(null !== $issue && null !== $issue->getProject()); $this->assertEquals('proj-b', $issue->getProject()->getProjectTrackerId()); // Verify both worklogs are now on project B. - $worklogD = $worklogRepository->findOneBy(['worklogId' => 100, 'dataProvider' => $dataProvider->getId()]); - $worklogE = $worklogRepository->findOneBy(['worklogId' => 101, 'dataProvider' => $dataProvider->getId()]); + $worklogD = $worklogRepository->findOneBy(['worklogId' => 100, 'dataProvider' => $dataProviderId]); + $worklogE = $worklogRepository->findOneBy(['worklogId' => 101, 'dataProvider' => $dataProviderId]); + \assert(null !== $worklogD && null !== $worklogD->getProject()); + \assert(null !== $worklogE && null !== $worklogE->getProject()); $this->assertEquals('proj-b', $worklogD->getProject()->getProjectTrackerId()); $this->assertEquals('proj-b', $worklogE->getProject()->getProjectTrackerId()); } diff --git a/tests/Service/LeantimeApiServiceTest.php b/tests/Service/LeantimeApiServiceTest.php index 9e9848b26..438bcbd1c 100644 --- a/tests/Service/LeantimeApiServiceTest.php +++ b/tests/Service/LeantimeApiServiceTest.php @@ -39,6 +39,13 @@ public function testUpdate(): void $issueRepository = $container->get(IssueRepository::class); $worklogRepository = $container->get(WorklogRepository::class); $entityManager = $container->get(EntityManagerInterface::class); + \assert($messageBus instanceof MessageBusInterface); + \assert($dataProviderRepository instanceof DataProviderRepository); + \assert($projectRepository instanceof ProjectRepository); + \assert($versionRepository instanceof VersionRepository); + \assert($issueRepository instanceof IssueRepository); + \assert($worklogRepository instanceof WorklogRepository); + \assert($entityManager instanceof EntityManagerInterface); $loggerMock = $this->createMock(LoggerInterface::class); @@ -97,17 +104,19 @@ public function testUpdate(): void $dataProvider->setSecret('Not so secret'); $entityManager->persist($dataProvider); $entityManager->flush(); + $dataProviderId = $dataProvider->getId(); + \assert(null !== $dataProviderId); // Projects $before = count($projectRepository->findAll()); - $service->updateAsJob(Project::class, 0, 100, $dataProvider->getId()); + $service->updateAsJob(Project::class, 0, 100, $dataProviderId); $after = count($projectRepository->findAll()); $this->assertEquals($before + 2, $after); $project = $projectRepository->findOneBy(['projectTrackerId' => 50]); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $project->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter - $service->updateAsJob(Project::class, 0, 100, $dataProvider->getId(), [], false, new \DateTime('2025-01-01')); + $service->updateAsJob(Project::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); $after = count($projectRepository->findAll()); $this->assertEquals($before + 2, $after); $project = $projectRepository->findOneBy(['projectTrackerId' => 50]); @@ -116,13 +125,13 @@ public function testUpdate(): void // Milestones $before = count($versionRepository->findAll()); - $service->updateAsJob(Version::class, 0, 100, $dataProvider->getId()); + $service->updateAsJob(Version::class, 0, 100, $dataProviderId); $after = count($versionRepository->findAll()); $this->assertEquals($before + 2, $after); $version = $versionRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $version->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter - $service->updateAsJob(Version::class, 0, 100, $dataProvider->getId(), [], false, new \DateTime('2025-01-01')); + $service->updateAsJob(Version::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); $after = count($versionRepository->findAll()); $this->assertEquals($before + 2, $after); $version = $versionRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); @@ -131,13 +140,13 @@ public function testUpdate(): void // Tickets $before = count($issueRepository->findAll()); - $service->updateAsJob(Issue::class, 0, 100, $dataProvider->getId()); + $service->updateAsJob(Issue::class, 0, 100, $dataProviderId); $after = count($issueRepository->findAll()); $this->assertEquals($before + 2, $after); $issue = $issueRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $issue->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter - $service->updateAsJob(Issue::class, 0, 100, $dataProvider->getId(), [], false, new \DateTime('2025-01-01')); + $service->updateAsJob(Issue::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); $after = count($issueRepository->findAll()); $this->assertEquals($before + 2, $after); $issue = $issueRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); @@ -146,13 +155,13 @@ public function testUpdate(): void // Timesheets $before = count($worklogRepository->findAll()); - $service->updateAsJob(Worklog::class, 0, 100, $dataProvider->getId()); + $service->updateAsJob(Worklog::class, 0, 100, $dataProviderId); $after = count($worklogRepository->findAll()); $this->assertEquals($before + 2, $after); $worklog = $worklogRepository->findOneBy(['worklogId' => 1, 'dataProvider' => $dataProvider]); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $worklog->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter - $service->updateAsJob(Worklog::class, 0, 100, $dataProvider->getId(), [], false, new \DateTime('2025-01-01')); + $service->updateAsJob(Worklog::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); $after = count($worklogRepository->findAll()); $this->assertEquals($before + 2, $after); $worklog = $worklogRepository->findOneBy(['worklogId' => 1, 'dataProvider' => $dataProvider]); @@ -171,6 +180,13 @@ public function testDeleted(): void $issueRepository = $container->get(IssueRepository::class); $worklogRepository = $container->get(WorklogRepository::class); $entityManager = $container->get(EntityManagerInterface::class); + \assert($messageBus instanceof MessageBusInterface); + \assert($dataProviderRepository instanceof DataProviderRepository); + \assert($projectRepository instanceof ProjectRepository); + \assert($versionRepository instanceof VersionRepository); + \assert($issueRepository instanceof IssueRepository); + \assert($worklogRepository instanceof WorklogRepository); + \assert($entityManager instanceof EntityManagerInterface); $loggerMock = $this->createMock(LoggerInterface::class); @@ -206,9 +222,9 @@ public function testDeleted(): void $project1 = new Project(); $project1->setDataProvider($dataProvider); - $project1->setProjectTrackerId(64); + $project1->setProjectTrackerId('64'); $project1->setName('Project to delete - protected'); - $project1->setProjectTrackerKey(64); + $project1->setProjectTrackerKey('64'); $project1->setProjectTrackerProjectUrl('http://localhost/'); $project1->setInclude(true); $project1->setProjectLeadMail('test@economics.local.itkdev.dk'); @@ -218,9 +234,9 @@ public function testDeleted(): void $project2 = new Project(); $project2->setDataProvider($dataProvider); - $project2->setProjectTrackerId(65); + $project2->setProjectTrackerId('65'); $project2->setName('Project to delete'); - $project2->setProjectTrackerKey(65); + $project2->setProjectTrackerKey('65'); $project2->setProjectTrackerProjectUrl('http://localhost/'); $project2->setInclude(true); $project2->setProjectLeadMail('test@economics.local.itkdev.dk'); @@ -232,21 +248,21 @@ public function testDeleted(): void $version1->setDataProvider($dataProvider); $version1->setName('Version 1'); $version1->setProject($project1); - $version1->setProjectTrackerId(6724); + $version1->setProjectTrackerId('6724'); $entityManager->persist($version1); $version2 = new Version(); $version2->setDataProvider($dataProvider); $version2->setName('Version 2'); $version2->setProject($project2); - $version2->setProjectTrackerId(6725); + $version2->setProjectTrackerId('6725'); $entityManager->persist($version2); $issue1 = new Issue(); $issue1->setDataProvider($dataProvider); $issue1->setProject($project1); - $issue1->setProjectTrackerId(6723); - $issue1->setProjectTrackerKey(6723); + $issue1->setProjectTrackerId('6723'); + $issue1->setProjectTrackerKey('6723'); $issue1->setName('issue 1 - protected'); $issue1->setAccountId('Account 1'); $issue1->setAccountKey('Account 1'); @@ -264,8 +280,8 @@ public function testDeleted(): void $issue2 = new Issue(); $issue2->setDataProvider($dataProvider); $issue2->setProject($project1); - $issue2->setProjectTrackerId(6726); - $issue2->setProjectTrackerKey(6726); + $issue2->setProjectTrackerId('6726'); + $issue2->setProjectTrackerKey('6726'); $issue2->setName('issue 2'); $issue2->setAccountId('Account 1'); $issue2->setAccountKey('Account 1'); @@ -283,13 +299,15 @@ public function testDeleted(): void $worklog1 = new Worklog(); $worklog1->setProject($project1); $worklog1->setDataProvider($dataProvider); - $worklog1->setProjectTrackerIssueId(6723); + $worklog1->setProjectTrackerIssueId('6723'); $worklog1->setWorklogId(66937); $worklog1->setDescription('Beskrivelse af worklog - protected'); $worklog1->setIsBilled(false); $worklog1->setWorker('admin@example.com'); $worklog1->setTimeSpentSeconds(60 * 15); - $worklog1->setStarted(\DateTime::createFromFormat('U', (string) strtotime('2024-01-01'), new \DateTimeZone('Europe/Copenhagen'))); + $worklog1Started = \DateTime::createFromFormat('U', (string) strtotime('2024-01-01'), new \DateTimeZone('Europe/Copenhagen')); + \assert($worklog1Started instanceof \DateTime); + $worklog1->setStarted($worklog1Started); $worklog1->setIssue($issue1); $worklog1->setDataProvider($dataProvider); $worklog1->setKind(BillableKindsEnum::GENERAL_BILLABLE); @@ -344,6 +362,7 @@ public function testDeleted(): void $entityManager->flush(); $id = $dataProvider->getId(); + \assert(null !== $id); $entityManager->clear(); diff --git a/tests/Service/ProjectBillingServiceTest.php b/tests/Service/ProjectBillingServiceTest.php index 7a8a5d355..e548ea9db 100644 --- a/tests/Service/ProjectBillingServiceTest.php +++ b/tests/Service/ProjectBillingServiceTest.php @@ -27,9 +27,12 @@ public function testGetIssuesNotIncludedInProjectBilling(): void /** @var ProjectBillingService $projectBillingService */ $projectBillingService = $container->get(ProjectBillingService::class); - /** @var BillingService $projectBillingService */ + /** @var BillingService $billingService */ $billingService = $container->get(BillingService::class); + /** @var IssueRepository $issueRepository */ + $issueRepository = $container->get(IssueRepository::class); + $project = $projectRepository->findOneBy([], ['id' => 'asc']); $projectBilling = new ProjectBilling(); @@ -40,7 +43,7 @@ public function testGetIssuesNotIncludedInProjectBilling(): void $projectBilling->setRecorded(false); $projectBilling->setDescription('Project billing'); - $issues = $container->get(IssueRepository::class)->getClosedIssuesFromInterval($projectBilling->getProject(), $projectBilling->getPeriodStart(), $projectBilling->getPeriodEnd()); + $issues = $issueRepository->getClosedIssuesFromInterval($projectBilling->getProject(), $projectBilling->getPeriodStart(), $projectBilling->getPeriodEnd()); $this->assertCount(10, $issues); $entityManager->persist($projectBilling); diff --git a/tests/Service/WorkloadReportServiceTest.php b/tests/Service/WorkloadReportServiceTest.php index e29b2a18e..ec2009705 100644 --- a/tests/Service/WorkloadReportServiceTest.php +++ b/tests/Service/WorkloadReportServiceTest.php @@ -50,7 +50,10 @@ public function testGetWorkloadReport(): void $dateTimeHelperMock = $this->createMock(DateTimeHelper::class); $dateTimeHelperMock->method('getWeeksOfYear')->willReturn(range(1, 52)); $dateTimeHelperMock->method('getMonthName')->willReturnCallback(function ($month) { - return date('F', mktime(0, 0, 0, $month, 10)); + $timestamp = mktime(0, 0, 0, $month, 10); + \assert(false !== $timestamp); + + return date('F', $timestamp); }); $dateTimeHelperMock->method('getFirstAndLastDateOfWeek')->willReturn([ 'dateFrom' => new \DateTime('2024-01-01 00:00:00'), @@ -115,7 +118,10 @@ public function testExceptionIsThrownWhenWorkerIdentifierIsEmpty(): void $dateTimeHelperMock = $this->createMock(DateTimeHelper::class); $dateTimeHelperMock->method('getWeeksOfYear')->willReturn(range(1, 52)); $dateTimeHelperMock->method('getMonthName')->willReturnCallback(function ($month) { - return date('F', mktime(0, 0, 0, $month, 10)); + $timestamp = mktime(0, 0, 0, $month, 10); + \assert(false !== $timestamp); + + return date('F', $timestamp); }); $dateTimeHelperMock->method('getFirstAndLastDateOfWeek')->willReturn([ 'dateFrom' => new \DateTime('2024-01-01 00:00:00'), @@ -174,7 +180,10 @@ public function testExceptionIsThrownWhenWorkerWorkloadIsUnset(): void $dateTimeHelperMock = $this->createMock(DateTimeHelper::class); $dateTimeHelperMock->method('getWeeksOfYear')->willReturn(range(1, 52)); $dateTimeHelperMock->method('getMonthName')->willReturnCallback(function ($month) { - return date('F', mktime(0, 0, 0, $month, 10)); + $timestamp = mktime(0, 0, 0, $month, 10); + \assert(false !== $timestamp); + + return date('F', $timestamp); }); $dateTimeHelperMock->method('getFirstAndLastDateOfWeek')->willReturn([ 'dateFrom' => new \DateTime('2024-01-01 00:00:00'), From d2387edbfcce1982a225b7d3e39fd29bb6e6a1e4 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 21:33:53 +0200 Subject: [PATCH 12/23] 7273: Cleared the long tail of PHPStan call-site bugs --- CHANGELOG.md | 39 +++++ phpstan-baseline.neon | 162 -------------------- src/DataFixtures/AppFixtures.php | 4 +- src/Entity/User.php | 2 +- src/Service/DashboardService.php | 4 +- src/Service/DataProviderService.php | 2 +- src/Service/DateTimeHelper.php | 5 +- src/Service/ForecastReportService.php | 4 +- src/Service/InvoicingRateReportService.php | 6 +- src/Service/PlanningService.php | 2 +- src/Service/WorkloadReportService.php | 2 +- tests/Service/LeantimeApiServiceTest.php | 17 +- tests/Service/ProjectBillingServiceTest.php | 21 ++- 13 files changed, 86 insertions(+), 184 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b281a391f..f1d3ecc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Cleared the long tail of PHPStan call-site bugs across services, tests, + and one report data flow (baseline 75 → 37). Notable fixes: + - `Epic::getName()` → `Epic::getTitle()` in `ForecastReportService` — the + method `getName()` doesn't exist on `Epic`; the report was calling the + wrong getter and getting an empty epic name in output. + - `Worklog::setTimeSpentSeconds()` in `DataProviderService` now casts the + `hours * SECONDS_IN_HOUR` product to `int` — `hours` is `float`, so the + untyped pass-through was silently losing precision before reaching the + `int $timeSpentSeconds` setter. + - `DateTimeHelper::getMonthName()`, `DashboardService::getYearData()`, + `AppFixtures`, and two test methods in `LeantimeApiServiceTest` now + assert that `\DateTime::createFromFormat()` / `mktime()` didn't return + `false` before using the result. + - `PlanningService::processIssuesForWeek()` `$week` widened from `int` to + `int|string` — the caller iterates over `sortIssuesByWeek()` output + which has the literal `'unscheduled'` bucket alongside numeric weeks. + - `ForecastReportService` casts `$project->getId()` (`int`) to `string` + when constructing `ForecastReportProjectData` (which expects `string`). + - `WorkloadReportService` and `InvoicingRateReportService` cast `$period` + (`int`) to `string` when calling `periodAverages->set()` on the + `ArrayCollection`; `InvoicingRateReportService` also + unwraps an erroneous `[$workerProjects]` value into `$workerProjects` + to match `projectData`'s declared value type. + - `User::getUserIdentifier()` simplified the empty-email check from + `null === $this->email || '' === $this->email` to just `'' === $this->email` + now that `$email` is non-null (Pattern B). + - `ManagementReportController::createGroupedInvoices()` return type + tightened from `array` to `array` to match + the service signature. + - Remaining test fixes: null-asserts after `findOneBy()`/`find()` calls + that index by `projectTrackerId` etc. before chaining + `getSourceModifiedDate()`/`getSourceDeletedDate()`; lingering + `setProjectTrackerIssueId(int)` and `setStarted(DateTime|false)` in + `LeantimeApiServiceTest::testDeleted`; `ProjectBillingServiceTest` + extracts a non-null `$project` / `$projectBillingId` and filters nulls + from the invoice-id array before passing to + `BillingService::exportInvoicesToSpreadsheet()`, and drops the + redundant `assertNotNull($spreadsheet)` (the return type is non-null). + * Test fixtures and controller endpoints tightened to satisfy PHPStan call-site checks (`argument.type`, `method.notFound`, `method.nonObject` — baseline 163 → 75). Highlights: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3c4ca3092..c61721699 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6,60 +6,6 @@ parameters: count: 1 path: src/Controller/ManagementReportController.php - - - message: '#^Parameter \#1 \$started of method App\\Entity\\Worklog\:\:setStarted\(\) expects DateTimeInterface, DateTime\|false given\.$#' - identifier: argument.type - count: 1 - path: src/DataFixtures/AppFixtures.php - - - - message: '#^Strict comparison using \=\=\= between null and string will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: src/Entity/User.php - - - - message: '#^Parameter \#2 \$timestamp of function date expects int\|null, int\|false given\.$#' - identifier: argument.type - count: 1 - path: src/Service/DashboardService.php - - - - message: '#^Parameter \#1 \$timeSpentSeconds of method App\\Entity\\Worklog\:\:setTimeSpentSeconds\(\) expects int, float given\.$#' - identifier: argument.type - count: 1 - path: src/Service/DataProviderService.php - - - - message: '#^Cannot call method format\(\) on DateTime\|false\.$#' - identifier: method.nonObject - count: 1 - path: src/Service/DateTimeHelper.php - - - - message: '#^Call to an undefined method App\\Entity\\Epic\:\:getName\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Parameter \#1 \$projectId of class App\\Model\\Reports\\ForecastReportProjectData constructor expects string, int\\|int\<1, max\> given\.$#' - identifier: argument.type - count: 1 - path: src/Service/ForecastReportService.php - - - - message: '#^Parameter \#1 \$key of method Doctrine\\Common\\Collections\\ArrayCollection\\:\:set\(\) expects string, int given\.$#' - identifier: argument.type - count: 1 - path: src/Service/InvoicingRateReportService.php - - - - message: '#^Parameter \#2 \$value of method Doctrine\\Common\\Collections\\ArrayCollection\\>\:\:set\(\) expects array\, array\\> given\.$#' - identifier: argument.type - count: 1 - path: src/Service/InvoicingRateReportService.php - - message: '#^Access to an undefined property object\:\:\$description\.$#' identifier: property.notFound @@ -192,116 +138,8 @@ parameters: count: 1 path: src/Service/LeantimeApiService.php - - - message: '#^Parameter \#2 \$week of method App\\Service\\PlanningService\:\:processIssuesForWeek\(\) expects int, int\|string given\.$#' - identifier: argument.type - count: 1 - path: src/Service/PlanningService.php - - - - message: '#^Parameter \#1 \$key of method Doctrine\\Common\\Collections\\ArrayCollection\\:\:set\(\) expects string, int given\.$#' - identifier: argument.type - count: 1 - path: src/Service/WorkloadReportService.php - - message: '#^Cannot call method getProjectTrackerId\(\) on App\\Entity\\Project\|null\.$#' identifier: method.nonObject count: 2 path: tests/Service/DataProviderServiceTest.php - - - - message: '#^Cannot call method getSourceDeletedDate\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Cannot call method getSourceDeletedDate\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Cannot call method getSourceDeletedDate\(\) on App\\Entity\\Worklog\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Cannot call method getSourceModifiedDate\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 2 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Cannot call method getSourceModifiedDate\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 2 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Cannot call method getSourceModifiedDate\(\) on App\\Entity\\Version\|null\.$#' - identifier: method.nonObject - count: 2 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Cannot call method getSourceModifiedDate\(\) on App\\Entity\\Worklog\|null\.$#' - identifier: method.nonObject - count: 2 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Cannot call method getTimestamp\(\) on DateTimeInterface\|null\.$#' - identifier: method.nonObject - count: 8 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Parameter \#1 \$projectTrackerIssueId of method App\\Entity\\Worklog\:\:setProjectTrackerIssueId\(\) expects string, int given\.$#' - identifier: argument.type - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Parameter \#1 \$started of method App\\Entity\\Worklog\:\:setStarted\(\) expects DateTimeInterface, DateTime\|false given\.$#' - identifier: argument.type - count: 1 - path: tests/Service/LeantimeApiServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNotNull\(\) with PhpOffice\\PhpSpreadsheet\\Spreadsheet will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Service/ProjectBillingServiceTest.php - - - - message: '#^Parameter \#1 \$invoiceIds of method App\\Service\\BillingService\:\:exportInvoicesToSpreadsheet\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: tests/Service/ProjectBillingServiceTest.php - - - - message: '#^Parameter \#1 \$project of method App\\Repository\\IssueRepository\:\:getClosedIssuesFromInterval\(\) expects App\\Entity\\Project, App\\Entity\\Project\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Service/ProjectBillingServiceTest.php - - - - message: '#^Parameter \#1 \$projectBillingId of method App\\Service\\ProjectBillingService\:\:createProjectBilling\(\) expects int, int\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Service/ProjectBillingServiceTest.php - - - - message: '#^Parameter \#2 \$periodStart of method App\\Repository\\IssueRepository\:\:getClosedIssuesFromInterval\(\) expects DateTimeInterface, DateTimeInterface\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Service/ProjectBillingServiceTest.php - - - - message: '#^Parameter \#3 \$periodEnd of method App\\Repository\\IssueRepository\:\:getClosedIssuesFromInterval\(\) expects DateTimeInterface, DateTimeInterface\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Service/ProjectBillingServiceTest.php diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 2ef9fc034..7008286ec 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -150,7 +150,9 @@ public function load(ObjectManager $manager): void $worklog->setProject($project); $worklog->setWorker($workerArray[$i % 10]); $worklog->setTimeSpentSeconds(60 * 15 * ($k + 1)); - $worklog->setStarted(\DateTime::createFromFormat('U', (string) strtotime("$year-$modMonth-$modDay"), new \DateTimeZone('Europe/Copenhagen'))); + $started = \DateTime::createFromFormat('U', (string) strtotime("$year-$modMonth-$modDay"), new \DateTimeZone('Europe/Copenhagen')); + \assert($started instanceof \DateTime); + $worklog->setStarted($started); $worklog->setIssue($issue); $worklog->setDataProvider($dataProvider); $worklog->setKind($modKind); diff --git a/src/Entity/User.php b/src/Entity/User.php index 1e8dbc38e..afea81d57 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -52,7 +52,7 @@ public function setEmail(string $email): self */ public function getUserIdentifier(): string { - if (null === $this->email || '' === $this->email) { + if ('' === $this->email) { throw new \LogicException('User identifier requires a non-empty email.'); } diff --git a/src/Service/DashboardService.php b/src/Service/DashboardService.php index eba7373e7..524c2d661 100644 --- a/src/Service/DashboardService.php +++ b/src/Service/DashboardService.php @@ -47,7 +47,9 @@ public function getUserDashboard(User $user, ?int $year = null): ?DashboardData $today->setTime(23, 59, 59); for ($month = 1; $month <= 12; ++$month) { - $daysInMonth = date('t', mktime(0, 0, 0, $month, 1, $year)); + $monthStart = mktime(0, 0, 0, $month, 1, $year); + \assert(false !== $monthStart); + $daysInMonth = date('t', $monthStart); for ($day = 1; $day <= $daysInMonth; ++$day) { $dayDate = new \DateTime(); diff --git a/src/Service/DataProviderService.php b/src/Service/DataProviderService.php index 899f55c5b..cc9d747bb 100644 --- a/src/Service/DataProviderService.php +++ b/src/Service/DataProviderService.php @@ -250,7 +250,7 @@ public function upsertWorklog(DataProviderWorklogData $upsertWorklogData): void $worklog->setWorker($upsertWorklogData->username); $worklog->setStarted($upsertWorklogData->startedDate); $worklog->setProjectTrackerIssueId($upsertWorklogData->projectTrackerIssueId); - $worklog->setTimeSpentSeconds($upsertWorklogData->hours * $this::SECONDS_IN_HOUR); + $worklog->setTimeSpentSeconds((int) ($upsertWorklogData->hours * $this::SECONDS_IN_HOUR)); $worklog->setKind(BillableKindsEnum::tryFrom($upsertWorklogData->kind)); $worklog->setProject($issue->getProject()); $worklog->setIssue($issue); diff --git a/src/Service/DateTimeHelper.php b/src/Service/DateTimeHelper.php index 87ea2433c..e089ed20a 100644 --- a/src/Service/DateTimeHelper.php +++ b/src/Service/DateTimeHelper.php @@ -104,7 +104,10 @@ public function getWeeksOfYear(int $year): array */ public function getMonthName(int $monthNumber): string { - return \DateTime::createFromFormat('!m', (string) $monthNumber)->format('F'); + $date = \DateTime::createFromFormat('!m', (string) $monthNumber); + \assert($date instanceof \DateTime); + + return $date->format('F'); } /** diff --git a/src/Service/ForecastReportService.php b/src/Service/ForecastReportService.php index 082e6afe0..1b92966a8 100644 --- a/src/Service/ForecastReportService.php +++ b/src/Service/ForecastReportService.php @@ -62,7 +62,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa } // If the project isn't already in the forecast, add it if (!isset($forecastReportData->projects[$projectId])) { - $newForecastReportProjectData = new ForecastReportProjectData($projectId); + $newForecastReportProjectData = new ForecastReportProjectData((string) $projectId); $newForecastReportProjectData->projectName = $project->getName() ?? '[no project name]'; $forecastReportData->projects[$projectId] = $newForecastReportProjectData; } @@ -86,7 +86,7 @@ public function getForecastReport(\DateTimeInterface $fromDate, \DateTimeInterfa $issueLink = $issue->getLinkToIssue() ?? '[no issue link]'; if ($issue->getEpics()->count() > 0) { - $issueTag = implode(',', array_map(fn ($epic) => $epic->getName(), $issue->getEpics()->toArray())); + $issueTag = implode(',', array_map(fn ($epic) => $epic->getTitle(), $issue->getEpics()->toArray())); } else { $issueTag = '[no tag]'; } diff --git a/src/Service/InvoicingRateReportService.php b/src/Service/InvoicingRateReportService.php index 4875ce2cf..9190118de 100644 --- a/src/Service/InvoicingRateReportService.php +++ b/src/Service/InvoicingRateReportService.php @@ -156,16 +156,14 @@ public function getInvoicingRateReport( // Calculate and set the average for this period $average = round($periodSums[$period] / $periodCounts[$period], 4); - $invoicingRateReportData->periodAverages->set($period, $average); + $invoicingRateReportData->periodAverages->set((string) $period, $average); } $invoicingRateReportWorker->average = $loggedHoursSum > 0 ? round($loggedBilledHoursSum / $loggedHoursSum * 100, 4) : 0; $invoicingRateReportData->workers->add($invoicingRateReportWorker); - $invoicingRateReportWorker->projectData->set('projects', [ - $workerProjects, - ]); + $invoicingRateReportWorker->projectData->set('projects', $workerProjects); } // Calculate and set the total average diff --git a/src/Service/PlanningService.php b/src/Service/PlanningService.php index 2afd4e7cb..8040f0ae1 100644 --- a/src/Service/PlanningService.php +++ b/src/Service/PlanningService.php @@ -158,7 +158,7 @@ private function sortIssuesByWeek(array $allIssues): array /** * @param array $issues */ - private function processIssuesForWeek(PlanningData $planning, int $week, array $issues, ?bool $holidayPlanning = false): void + private function processIssuesForWeek(PlanningData $planning, int|string $week, array $issues, ?bool $holidayPlanning = false): void { foreach ($issues as $issueData) { if (!$holidayPlanning && IssueStatusEnum::DONE === $issueData->getStatus()) { diff --git a/src/Service/WorkloadReportService.php b/src/Service/WorkloadReportService.php index 639627982..f7cfe0e04 100644 --- a/src/Service/WorkloadReportService.php +++ b/src/Service/WorkloadReportService.php @@ -110,7 +110,7 @@ public function getWorkloadReport(int $year, PeriodTypeEnum $viewPeriodType = Pe // Calculate and set the average for this period $average = round($periodSums[$period] / $periodCounts[$period], 1); - $workloadReportData->periodAverages->set($period, $average); + $workloadReportData->periodAverages->set((string) $period, $average); } $workloadReportWorker->average = $expectedWorkloadSum > 0 ? round($loggedHoursSum / $expectedWorkloadSum * 100, 1) : 0; diff --git a/tests/Service/LeantimeApiServiceTest.php b/tests/Service/LeantimeApiServiceTest.php index 438bcbd1c..1a86a782a 100644 --- a/tests/Service/LeantimeApiServiceTest.php +++ b/tests/Service/LeantimeApiServiceTest.php @@ -114,12 +114,14 @@ public function testUpdate(): void $after = count($projectRepository->findAll()); $this->assertEquals($before + 2, $after); $project = $projectRepository->findOneBy(['projectTrackerId' => 50]); + \assert(null !== $project && null !== $project->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $project->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter $service->updateAsJob(Project::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); $after = count($projectRepository->findAll()); $this->assertEquals($before + 2, $after); $project = $projectRepository->findOneBy(['projectTrackerId' => 50]); + \assert(null !== $project && null !== $project->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $project->getSourceModifiedDate()->getTimestamp()); // Milestones @@ -129,12 +131,14 @@ public function testUpdate(): void $after = count($versionRepository->findAll()); $this->assertEquals($before + 2, $after); $version = $versionRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); + \assert(null !== $version && null !== $version->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $version->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter $service->updateAsJob(Version::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); $after = count($versionRepository->findAll()); $this->assertEquals($before + 2, $after); $version = $versionRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); + \assert(null !== $version && null !== $version->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $version->getSourceModifiedDate()->getTimestamp()); // Tickets @@ -144,12 +148,14 @@ public function testUpdate(): void $after = count($issueRepository->findAll()); $this->assertEquals($before + 2, $after); $issue = $issueRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); + \assert(null !== $issue && null !== $issue->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $issue->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter $service->updateAsJob(Issue::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); $after = count($issueRepository->findAll()); $this->assertEquals($before + 2, $after); $issue = $issueRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); + \assert(null !== $issue && null !== $issue->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $issue->getSourceModifiedDate()->getTimestamp()); // Timesheets @@ -159,12 +165,14 @@ public function testUpdate(): void $after = count($worklogRepository->findAll()); $this->assertEquals($before + 2, $after); $worklog = $worklogRepository->findOneBy(['worklogId' => 1, 'dataProvider' => $dataProvider]); + \assert(null !== $worklog && null !== $worklog->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $worklog->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter $service->updateAsJob(Worklog::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); $after = count($worklogRepository->findAll()); $this->assertEquals($before + 2, $after); $worklog = $worklogRepository->findOneBy(['worklogId' => 1, 'dataProvider' => $dataProvider]); + \assert(null !== $worklog && null !== $worklog->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $worklog->getSourceModifiedDate()->getTimestamp()); } @@ -316,13 +324,15 @@ public function testDeleted(): void $worklog2 = new Worklog(); $worklog2->setProject($project1); $worklog2->setDataProvider($dataProvider); - $worklog2->setProjectTrackerIssueId(6726); + $worklog2->setProjectTrackerIssueId('6726'); $worklog2->setWorklogId(66938); $worklog2->setDescription('Beskrivelse af worklog'); $worklog2->setIsBilled(false); $worklog2->setWorker('admin@example.com'); $worklog2->setTimeSpentSeconds(60 * 15); - $worklog2->setStarted(\DateTime::createFromFormat('U', (string) strtotime('2024-01-01'), new \DateTimeZone('Europe/Copenhagen'))); + $worklog2Started = \DateTime::createFromFormat('U', (string) strtotime('2024-01-01'), new \DateTimeZone('Europe/Copenhagen')); + \assert($worklog2Started instanceof \DateTime); + $worklog2->setStarted($worklog2Started); $worklog2->setIssue($issue2); $worklog2->setDataProvider($dataProvider); $worklog2->setKind(BillableKindsEnum::GENERAL_BILLABLE); @@ -380,12 +390,15 @@ public function testDeleted(): void $this->assertEquals($countProjectsBeforeCreate + 1, $countProjectsAfterDelete); $project1 = $projectRepository->find($projectId1); + \assert(null !== $project1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $project1->getSourceDeletedDate()); $issue1 = $issueRepository->find($issueId1); + \assert(null !== $issue1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $issue1->getSourceDeletedDate()); $worklog1 = $worklogRepository->find($worklogId1); + \assert(null !== $worklog1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $worklog1->getSourceDeletedDate()); } diff --git a/tests/Service/ProjectBillingServiceTest.php b/tests/Service/ProjectBillingServiceTest.php index e548ea9db..d5a85a1c3 100644 --- a/tests/Service/ProjectBillingServiceTest.php +++ b/tests/Service/ProjectBillingServiceTest.php @@ -34,22 +34,28 @@ public function testGetIssuesNotIncludedInProjectBilling(): void $issueRepository = $container->get(IssueRepository::class); $project = $projectRepository->findOneBy([], ['id' => 'asc']); + \assert(null !== $project); + + $periodStart = (new \DateTime())->sub(new \DateInterval('P1D')); + $periodEnd = (new \DateTime())->add(new \DateInterval('P1D')); $projectBilling = new ProjectBilling(); - $projectBilling->setPeriodStart((new \DateTime())->sub(new \DateInterval('P1D'))); - $projectBilling->setPeriodEnd((new \DateTime())->add(new \DateInterval('P1D'))); + $projectBilling->setPeriodStart($periodStart); + $projectBilling->setPeriodEnd($periodEnd); $projectBilling->setName('Project Billing 1'); $projectBilling->setProject($project); $projectBilling->setRecorded(false); $projectBilling->setDescription('Project billing'); - $issues = $issueRepository->getClosedIssuesFromInterval($projectBilling->getProject(), $projectBilling->getPeriodStart(), $projectBilling->getPeriodEnd()); + $issues = $issueRepository->getClosedIssuesFromInterval($project, $periodStart, $periodEnd); $this->assertCount(10, $issues); $entityManager->persist($projectBilling); $entityManager->flush(); - $projectBillingService->createProjectBilling($projectBilling->getId()); + $projectBillingId = $projectBilling->getId(); + \assert(null !== $projectBillingId); + $projectBillingService->createProjectBilling($projectBillingId); $this->assertCount(2, $projectBilling->getInvoices()); @@ -57,12 +63,13 @@ public function testGetIssuesNotIncludedInProjectBilling(): void $this->assertCount(4, $issues); - $ids = $projectBilling->getInvoices()->map(fn ($invoice) => $invoice->getId())->toArray(); + $ids = array_values(array_filter( + $projectBilling->getInvoices()->map(fn ($invoice) => $invoice->getId())->toArray(), + fn (?int $id) => null !== $id + )); $spreadsheet = $billingService->exportInvoicesToSpreadsheet($ids); - $this->assertNotNull($spreadsheet); - $spreadsheetArray = $spreadsheet->getActiveSheet()->toArray(null, false, false); $this->assertCount(8, $spreadsheetArray); From 1a57db267c9b07cbf21dd31438433b1187ed27b5 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 21:39:40 +0200 Subject: [PATCH 13/23] 7273: Final pass on PHPStan call-site bugs --- CHANGELOG.md | 20 +++++++++++++++++++ phpstan-baseline.neon | 12 ----------- src/Controller/ManagementReportController.php | 2 +- tests/Service/DataProviderServiceTest.php | 2 ++ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d3ecc52..8542530f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Final pass on PHPStan call-site bugs outside the LeantimeApiService + dynamic-object cluster (baseline 37 → 34): + - `DataProviderServiceTest` adds `\assert(null !== $worklog{D,E}->getProject())` + before chaining `getProjectTrackerId()` on the worklogs created earlier + in the test (line 102/103). + - `ManagementReportController::createGroupedInvoices()` return type + tightened from `array` to `array` to match + the year-key int-cast at the build site and the + `ManagementReportService::generateSpreadsheetCsvResponse()` signature. + + The remaining 34 baseline entries are all in `LeantimeApiService.php`: + 22 distinct `property.notFound` reports on `$data->...` accesses where + `$data` is the `object` returned by `json_decode($json, null)`, plus one + `match.alwaysTrue` that's the price of an explicit `default => throw` + arm we added for safety on the `type` match in `delete()`. Clearing the + cluster needs a real refactor — either decode as associative array and + switch to `['property']` access throughout, or define per-endpoint + `@phpstan-type` shapes and thread them through each helper. Out of + scope for this incremental cleanup pass. + * Cleared the long tail of PHPStan call-site bugs across services, tests, and one report data flow (baseline 75 → 37). Notable fixes: - `Epic::getName()` → `Epic::getTitle()` in `ForecastReportService` — the diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c61721699..507e8633e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,11 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Parameter \#1 \$groupedInvoices of method App\\Service\\ManagementReportService\:\:generateSpreadsheetCsvResponse\(\) expects array\\>, array\\> given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/ManagementReportController.php - - message: '#^Access to an undefined property object\:\:\$description\.$#' identifier: property.notFound @@ -137,9 +131,3 @@ parameters: identifier: match.alwaysTrue count: 1 path: src/Service/LeantimeApiService.php - - - - message: '#^Cannot call method getProjectTrackerId\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 2 - path: tests/Service/DataProviderServiceTest.php diff --git a/src/Controller/ManagementReportController.php b/src/Controller/ManagementReportController.php index 7ff10a814..ad8203840 100644 --- a/src/Controller/ManagementReportController.php +++ b/src/Controller/ManagementReportController.php @@ -91,7 +91,7 @@ public function export(Request $request, ManagementReportService $managementRepo /** * @param array $invoices * - * @return array> + * @return array> */ private function createGroupedInvoices(array $invoices): array { diff --git a/tests/Service/DataProviderServiceTest.php b/tests/Service/DataProviderServiceTest.php index f49089339..a1ec10494 100644 --- a/tests/Service/DataProviderServiceTest.php +++ b/tests/Service/DataProviderServiceTest.php @@ -99,6 +99,8 @@ public function testUpsertIssueProjectChangeMovesWorklogs(): void $entityManager->flush(); // Verify worklogs are on project A. + \assert(null !== $worklogD->getProject()); + \assert(null !== $worklogE->getProject()); $this->assertEquals('proj-a', $worklogD->getProject()->getProjectTrackerId()); $this->assertEquals('proj-a', $worklogE->getProject()->getProjectTrackerId()); From e88fed38abf27804f231d8c3706dc07d83bc9209 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 21:46:45 +0200 Subject: [PATCH 14/23] 7273: Changed to array json_decode output --- CHANGELOG.md | 20 +++++ phpstan-baseline.neon | 133 +---------------------------- src/Service/LeantimeApiService.php | 114 +++++++++++++++---------- 3 files changed, 88 insertions(+), 179 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8542530f9..70cebe484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* `LeantimeApiService` now decodes JSON responses as associative arrays + (`json_decode($content, true)`) instead of objects, and all + `$data->property` / `$result->property` accesses across the file — + including `fetchFromLeantime()`'s return, the `delete()` loop, the + `updateAsJob()` paginator, `dispatchUpsertMessage()`, and the five + upsert helpers — switched to array access. `fetchFromLeantime()` and + the five helpers are now typed `array` instead of + `object`. Cleared the last 33 PHPStan baseline entries (22 unique + `property.notFound` reports plus one `match.alwaysTrue`). To clear + the `match.alwaysTrue` on the `delete()` `match($type)`, the local + `$types` array got a `@var list` + annotation so PHPStan narrows correctly and the `default => throw` arm + is no longer needed. Behavior unchanged — JSON shape is identical from + the wire, only the in-memory representation changed. + + **`phpstan-baseline.neon` is now empty (`ignoreErrors: []`).** Every + line in `src/` and `tests/` is clean at PHPStan level 8 with zero + suppressions. The file is kept in the repo so future regressions can + re-populate it if introduced. + * Final pass on PHPStan call-site bugs outside the LeantimeApiService dynamic-object cluster (baseline 37 → 34): - `DataProviderServiceTest` adds `\assert(null !== $worklog{D,E}->getProject())` diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 507e8633e..aab499115 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,133 +1,2 @@ parameters: - ignoreErrors: - - - message: '#^Access to an undefined property object\:\:\$description\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$dueDate\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$email\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$hours\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$id\.$#' - identifier: property.notFound - count: 5 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$kind\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$milestoneId\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$modified\.$#' - identifier: property.notFound - count: 4 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$name\.$#' - identifier: property.notFound - count: 4 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$plannedHours\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$projectId\.$#' - identifier: property.notFound - count: 2 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$remainingHours\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$resolutionDate\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$results\.$#' - identifier: property.notFound - count: 2 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$resultsCount\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$status\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$tags\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$ticketId\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$username\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$workDate\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Access to an undefined property object\:\:\$worker\.$#' - identifier: property.notFound - count: 1 - path: src/Service/LeantimeApiService.php - - - - message: '#^Match arm comparison between ''timesheets'' and ''timesheets'' is always true\.$#' - identifier: match.alwaysTrue - count: 1 - path: src/Service/LeantimeApiService.php + ignoreErrors: [] diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index 46e2dc261..14a98e69d 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -117,6 +117,7 @@ public function deleteAsJob(int $dataProviderId, bool $asyncJobQueue = false, ?\ throw new NotFoundException("DataProvider with id: $dataProviderId not found"); } + /** @var list $types */ $types = [ self::TIMESHEETS, self::TICKETS, @@ -131,11 +132,11 @@ public function deleteAsJob(int $dataProviderId, bool $asyncJobQueue = false, ?\ // Get data from Leantime. $data = $this->fetchFromLeantime($dataProvider, 'deleted', $params); - $results = $data->results; + $results = $data['results']; // Queue delete. foreach ($types as $type) { - if (!isset($results->{$type})) { + if (!isset($results[$type])) { continue; } @@ -144,12 +145,11 @@ public function deleteAsJob(int $dataProviderId, bool $asyncJobQueue = false, ?\ self::MILESTONES => Version::class, self::TICKETS => Issue::class, self::TIMESHEETS => Worklog::class, - default => throw new \InvalidArgumentException(sprintf('Unsupported type: %s', $type)), }; - foreach ($results->{$type} as $result) { - $projectTrackerId = $result->id; - $deletedDate = $this->getLeanDateTime($result->deletedDate); + foreach ($results[$type] as $result) { + $projectTrackerId = $result['id']; + $deletedDate = $this->getLeanDateTime($result['deletedDate']); $this->messageBus->dispatch( new EntityRemovedFromDataProviderMessage($classname, $dataProviderId, $projectTrackerId, $deletedDate), @@ -200,9 +200,9 @@ public function updateAsJob(string $className, int $startId, int $limit, int $da $fetchDate = new \DateTime(); // Queue upsert. - foreach ($data->results as $result) { + foreach ($data['results'] as $result) { $this->dispatchUpsertMessage($className, $result, $dataProviderId, $fetchDate, $asyncJobQueue, $dataProviderUrl, $disableModifiedAtCheck); - $startId = $result->id; + $startId = $result['id']; } $startId = $startId + 1; @@ -213,7 +213,7 @@ public function updateAsJob(string $className, int $startId, int $limit, int $da } // Queue next page. - if ($data->resultsCount === $limit) { + if ($data['resultsCount'] === $limit) { $this->messageBus->dispatch( new LeantimeUpdateMessage($className, $startId, $limit, $dataProviderId, $asyncJobQueue, $modifiedAfter, $projectTrackerProjectIds, $disableModifiedAtCheck), [new TransportNamesStamp($asyncJobQueue ? $this::QUEUE_ASYNC : $this::QUEUE_SYNC)], @@ -221,7 +221,10 @@ public function updateAsJob(string $className, int $startId, int $limit, int $da } } - private function dispatchUpsertMessage(string $className, object $data, int $dataProviderId, \DateTimeInterface $fetchDate, bool $asyncJobQueue = false, ?string $dataProviderUrl = null, bool $disableModifiedAtCheck = false): void + /** + * @param array $data + */ + private function dispatchUpsertMessage(string $className, array $data, int $dataProviderId, \DateTimeInterface $fetchDate, bool $asyncJobQueue = false, ?string $dataProviderUrl = null, bool $disableModifiedAtCheck = false): void { try { $message = match ($className) { @@ -246,98 +249,115 @@ private function dispatchUpsertMessage(string $className, object $data, int $dat } } - private function getProjectUpsertFromResult(object $result, int $dataProviderId, \DateTimeInterface $fetchDate, ?string $dataProviderUrl = null, bool $disableModifiedAtCheck = false): DataProviderProjectData + /** + * @param array $result + */ + private function getProjectUpsertFromResult(array $result, int $dataProviderId, \DateTimeInterface $fetchDate, ?string $dataProviderUrl = null, bool $disableModifiedAtCheck = false): DataProviderProjectData { - $projectTrackerId = (string) $result->id; + $projectTrackerId = (string) $result['id']; return new DataProviderProjectData( $dataProviderId, - $result->name, + $result['name'], $projectTrackerId, $this->linkToProject($projectTrackerId, $dataProviderUrl), $fetchDate, - $this->getLeanDateTime($result->modified), + $this->getLeanDateTime($result['modified']), $disableModifiedAtCheck, ); } - private function getVersionUpsertFromResult(object $result, int $dataProviderId, \DateTimeInterface $fetchDate, bool $disableModifiedAtCheck = false): DataProviderVersionData + /** + * @param array $result + */ + private function getVersionUpsertFromResult(array $result, int $dataProviderId, \DateTimeInterface $fetchDate, bool $disableModifiedAtCheck = false): DataProviderVersionData { return new DataProviderVersionData( $dataProviderId, - $result->name, - (string) $result->id, - (string) $result->projectId, + $result['name'], + (string) $result['id'], + (string) $result['projectId'], $fetchDate, - $this->getLeanDateTime($result->modified), + $this->getLeanDateTime($result['modified']), $disableModifiedAtCheck, ); } - private function getIssueUpsertFromResult(object $result, int $dataProviderId, \DateTimeInterface $fetchDate, ?string $dataProviderUrl = null, bool $disableModifiedAtCheck = false): DataProviderIssueData + /** + * @param array $result + */ + private function getIssueUpsertFromResult(array $result, int $dataProviderId, \DateTimeInterface $fetchDate, ?string $dataProviderUrl = null, bool $disableModifiedAtCheck = false): DataProviderIssueData { - $projectTrackerId = (string) $result->id; + $projectTrackerId = (string) $result['id']; return new DataProviderIssueData( $projectTrackerId, $dataProviderId, - (string) $result->projectId, - $result->name, - $result->tags, - $result->plannedHours, - $result->remainingHours, - $result->worker, - $this->convertStatusToEnum($result->status), - $this->getLeanDateTime($result->dueDate), - $this->getLeanDateTime($result->resolutionDate), + (string) $result['projectId'], + $result['name'], + $result['tags'], + $result['plannedHours'], + $result['remainingHours'], + $result['worker'], + $this->convertStatusToEnum($result['status']), + $this->getLeanDateTime($result['dueDate']), + $this->getLeanDateTime($result['resolutionDate']), $fetchDate, $this->linkToTicket($projectTrackerId, $dataProviderUrl), - $this->getLeanDateTime($result->modified), - $result->milestoneId, + $this->getLeanDateTime($result['modified']), + $result['milestoneId'], $disableModifiedAtCheck, ); } - private function getWorklogUpsertFromResult(object $result, int $dataProviderId, \DateTimeInterface $fetchDate, bool $disableModifiedAtCheck = false): DataProviderWorklogData + /** + * @param array $result + */ + private function getWorklogUpsertFromResult(array $result, int $dataProviderId, \DateTimeInterface $fetchDate, bool $disableModifiedAtCheck = false): DataProviderWorklogData { - $startedDate = $this->getLeanDateTime($result->workDate); + $startedDate = $this->getLeanDateTime($result['workDate']); if (null === $startedDate) { throw new NotAcceptableException('Worklog upsert not acceptable: startedDate is null'); } return new DataProviderWorklogData( - $result->id, + $result['id'], $dataProviderId, - (string) $result->ticketId, - $result->description, + (string) $result['ticketId'], + $result['description'], $startedDate, - $result->username, - $result->hours, - $result->kind, + $result['username'], + $result['hours'], + $result['kind'], $fetchDate, - $this->getLeanDateTime($result->modified), + $this->getLeanDateTime($result['modified']), $disableModifiedAtCheck, ); } - private function getWorkerUpsertFromResult(object $result, int $dataProviderId, \DateTimeInterface $fetchDate): DataProviderWorkerData + /** + * @param array $result + */ + private function getWorkerUpsertFromResult(array $result, int $dataProviderId, \DateTimeInterface $fetchDate): DataProviderWorkerData { return new DataProviderWorkerData( - $result->id, - $result->name, - $result->email, + $result['id'], + $result['name'], + $result['email'], ); } /** * @param array $params + * + * @return array */ - private function fetchFromLeantime(DataProvider $dataProvider, string $type, array $params): object + private function fetchFromLeantime(DataProvider $dataProvider, string $type, array $params): array { $response = $this->post($dataProvider, $type, $params); - return json_decode($response->getContent(), null, 512, JSON_THROW_ON_ERROR); + return json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); } /** From 57b4c62db2a770dab5d6a6d6e8b90ef70df15262 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Sun, 17 May 2026 21:56:40 +0200 Subject: [PATCH 15/23] 7273: Shortened changelog entries --- CHANGELOG.md | 295 ++++++++------------------------------------------- 1 file changed, 45 insertions(+), 250 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70cebe484..d1516a8b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,256 +8,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -* `LeantimeApiService` now decodes JSON responses as associative arrays - (`json_decode($content, true)`) instead of objects, and all - `$data->property` / `$result->property` accesses across the file — - including `fetchFromLeantime()`'s return, the `delete()` loop, the - `updateAsJob()` paginator, `dispatchUpsertMessage()`, and the five - upsert helpers — switched to array access. `fetchFromLeantime()` and - the five helpers are now typed `array` instead of - `object`. Cleared the last 33 PHPStan baseline entries (22 unique - `property.notFound` reports plus one `match.alwaysTrue`). To clear - the `match.alwaysTrue` on the `delete()` `match($type)`, the local - `$types` array got a `@var list` - annotation so PHPStan narrows correctly and the `default => throw` arm - is no longer needed. Behavior unchanged — JSON shape is identical from - the wire, only the in-memory representation changed. - - **`phpstan-baseline.neon` is now empty (`ignoreErrors: []`).** Every - line in `src/` and `tests/` is clean at PHPStan level 8 with zero - suppressions. The file is kept in the repo so future regressions can - re-populate it if introduced. - -* Final pass on PHPStan call-site bugs outside the LeantimeApiService - dynamic-object cluster (baseline 37 → 34): - - `DataProviderServiceTest` adds `\assert(null !== $worklog{D,E}->getProject())` - before chaining `getProjectTrackerId()` on the worklogs created earlier - in the test (line 102/103). - - `ManagementReportController::createGroupedInvoices()` return type - tightened from `array` to `array` to match - the year-key int-cast at the build site and the - `ManagementReportService::generateSpreadsheetCsvResponse()` signature. - - The remaining 34 baseline entries are all in `LeantimeApiService.php`: - 22 distinct `property.notFound` reports on `$data->...` accesses where - `$data` is the `object` returned by `json_decode($json, null)`, plus one - `match.alwaysTrue` that's the price of an explicit `default => throw` - arm we added for safety on the `type` match in `delete()`. Clearing the - cluster needs a real refactor — either decode as associative array and - switch to `['property']` access throughout, or define per-endpoint - `@phpstan-type` shapes and thread them through each helper. Out of - scope for this incremental cleanup pass. - -* Cleared the long tail of PHPStan call-site bugs across services, tests, - and one report data flow (baseline 75 → 37). Notable fixes: - - `Epic::getName()` → `Epic::getTitle()` in `ForecastReportService` — the - method `getName()` doesn't exist on `Epic`; the report was calling the - wrong getter and getting an empty epic name in output. - - `Worklog::setTimeSpentSeconds()` in `DataProviderService` now casts the - `hours * SECONDS_IN_HOUR` product to `int` — `hours` is `float`, so the - untyped pass-through was silently losing precision before reaching the - `int $timeSpentSeconds` setter. - - `DateTimeHelper::getMonthName()`, `DashboardService::getYearData()`, - `AppFixtures`, and two test methods in `LeantimeApiServiceTest` now - assert that `\DateTime::createFromFormat()` / `mktime()` didn't return - `false` before using the result. - - `PlanningService::processIssuesForWeek()` `$week` widened from `int` to - `int|string` — the caller iterates over `sortIssuesByWeek()` output - which has the literal `'unscheduled'` bucket alongside numeric weeks. - - `ForecastReportService` casts `$project->getId()` (`int`) to `string` - when constructing `ForecastReportProjectData` (which expects `string`). - - `WorkloadReportService` and `InvoicingRateReportService` cast `$period` - (`int`) to `string` when calling `periodAverages->set()` on the - `ArrayCollection`; `InvoicingRateReportService` also - unwraps an erroneous `[$workerProjects]` value into `$workerProjects` - to match `projectData`'s declared value type. - - `User::getUserIdentifier()` simplified the empty-email check from - `null === $this->email || '' === $this->email` to just `'' === $this->email` - now that `$email` is non-null (Pattern B). - - `ManagementReportController::createGroupedInvoices()` return type - tightened from `array` to `array` to match - the service signature. - - Remaining test fixes: null-asserts after `findOneBy()`/`find()` calls - that index by `projectTrackerId` etc. before chaining - `getSourceModifiedDate()`/`getSourceDeletedDate()`; lingering - `setProjectTrackerIssueId(int)` and `setStarted(DateTime|false)` in - `LeantimeApiServiceTest::testDeleted`; `ProjectBillingServiceTest` - extracts a non-null `$project` / `$projectBillingId` and filters nulls - from the invoice-id array before passing to - `BillingService::exportInvoicesToSpreadsheet()`, and drops the - redundant `assertNotNull($spreadsheet)` (the return type is non-null). - -* Test fixtures and controller endpoints tightened to satisfy PHPStan - call-site checks (`argument.type`, `method.notFound`, `method.nonObject` — - baseline 163 → 75). Highlights: - - `LeantimeApiServiceTest`, `DataProviderServiceTest`, and - `ProjectBillingServiceTest` now `\assert(... instanceof ...)` after each - `$container->get()` so PHPStan knows the concrete service type; this - cascaded through ~20 previously-untyped `->persist()`/`->flush()`/ - `->findOneBy()` calls on `object`. Also fixed a `@var BillingService - $projectBillingService` typo in `ProjectBillingServiceTest` that mis-typed - `$billingService`, hiding 4 method-not-found errors. - - `LeantimeApiServiceTest` now passes `'64'`/`'65'`/etc. (string) instead of - `64`/`65` to `setProjectTrackerId()`/`setProjectTrackerKey()`/ - `setProjectTrackerIssueId()`, and asserts non-null on `$dataProvider->getId()` - once after `flush()` rather than per call. Also handles - `\DateTime::createFromFormat()` returning `\DateTime|false`. - - `DanishHolidayHelperTest`: tightened `$expected` from `?\DateTimeInterface` - to `\DateTimeInterface` on three test methods (and the matching data - provider shape) since the data is never null. - - `WorkloadReportServiceTest`: asserts `mktime()` did not return `false` - before passing to `date()`. - - `InvoiceController` and `ProjectBillingController`: null-guard - `$invoice->getId()` (`?int`) before building invoice-id arrays passed to - `BillingService::generateSpreadsheet{Csv,Html}()`; `array_filter` to drop - nulls from `array_map` over invoice collections; `array_map(intval(...))` - on string id list from `$request->query->get('ids')`. - - `SubscriptionController::check()` returns a 400 when `$user->getEmail()` - is `null` rather than passing `?string` downstream; `subscriptionHandler()` - throws when `key($content)` is `null` rather than handing `null` to - `SubscriptionSubjectEnum::tryFrom()`. - - `ManagementReportController::createGroupedInvoices()` skips invoices with - a null `recordedDate`, and casts the `format('Y')`/`format('n')` string - parts to `int` so the grouped array keys match the - `ManagementReportService::generateSpreadsheetCsvResponse()` signature. - -* `LeantimeApiService::update()` and `::delete()` now skip data providers - with a null ID rather than passing `int|null` to the `LeantimeUpdateMessage` - / `LeantimeDeleteMessage` constructors (PHPStan `argument.type`, 2 entries — - baseline 165 → 163). Persisted entities always have an ID; the guard is - defensive against transient instances. - -* Report services now skip worklogs with no associated project or issue - rather than dereferencing nullable getters (PHPStan `method.nonObject`, - 13 entries — baseline 184 → 165). Affected services: - `ForecastReportService::getForecastReport()`, - `BillableUnbilledHoursReportService::getReport()`, - `InvoicingRateReportService::getInvoicingRateReport()`. Each foreach - body caches `$project = $worklog->getProject()` and - `$issue = $worklog->getIssue()` once at the top, `continue`s when - either is null, and uses the narrowed references throughout — also - cleared 6 cascading `argument.type`/`property.notFound` follow-ons - that had been hiding behind the chained calls. - -* Typed `HourReportProjectTicket::__construct()` parameters (PHPStan - `missingType.parameter`, 6 entries — baseline 190 → 184). The - constructor now requires `string $id, string $projectTrackerId, - string $headline, float $totalEstimated, float $totalSpent, - string $linkToIssue` to match the property types. Caller in - `HourReportService::getHourReport()` coerces `Issue` getter return - values (`(string) $issue->getId()`, `?? ''` fallbacks for nullable - trackers/name/link) — same pattern as `ForecastReportService`. - -* Migration `Version20260517151632`: relaxed 11 ManyToOne foreign keys - from `NOT NULL` to `NULL` to match the property types they bind to - (PHPStan `doctrine.associationType`, Pattern C — 11 entries cleared, - baseline 201 → 190). Affected: `cybersecurity_agreement.service_agreement_id`, - `invoice_entry.invoice_id`, `issue_product.{issue_id,product_id}`, - `product.project_id`, `project_billing.project_id`, - `service_agreement.{project_id,client_id,project_lead_id}`, - `version.project_id`, `worklog.issue_id`. Application-layer validation - (`#[Assert\NotNull]` on `Product::$project`, form validators on the - others) continues to enforce required-ness; the DB now reflects the - truthful "may be null in-memory mid-construction" state instead of a - constraint that PHP couldn't satisfy. - -* Aligned Doctrine entity property types with database column nullability - (PHPStan `doctrine.columnType`/`doctrine.associationType`, Pattern B — - 43 entries cleared, baseline 243 → 201). Two-part change: - - **Migration `Version20260517131038`**: made 18 columns nullable to - match property types on entities that legitimately allow null — - the four synced entities (`issue`, `project`, `version`, `worklog`) - where fields arrive incrementally from external trackers, plus the - DateTime fields `project_billing.period_{start,end}` and - `service_agreement.valid_from`. - - **Property defaults**: converted 25 `?T $prop = null` declarations to - `T $prop = default` on app-managed entities (`Account`, `Client`, - `DataProvider`, `Epic`, `Invoice`, `InvoiceEntry`, `IssueProduct`, - `Product`, `ProjectBilling`, `ProjectVersionBudget`, - `ServiceAgreement`, `Subscription`, `User`, `Worker`, `WorkerGroup`). - Defaults: `''` for strings, `false` for booleans, `0`/`0.0` for - numerics, `'0'` for the decimal-as-string `Product::$price`, and - `HostingProviderEnum::ADM` for `ServiceAgreement::$hostingProvider` - (no natural zero exists for enums; pick a default and rely on - callers/validators to set the intended value before persist). - - Switched two `__toString()` methods (`Worker`, `WorkerGroup`) from - `??` to `?:` to preserve the empty-string-falls-through behavior - now that the properties can't be `null`. - -* Fixed 16 specific PHPStan-flagged issues, mostly real bugs the type - surface had been hiding: - - `ForecastReportService` substitutes `'[no issue id]'`/`'[no issue link]'`/`''` - fallbacks for nullable issue/worklog fields when assigning to non-null - model properties; removed an always-false `!$currentProject` guard - that PHPStan proved unreachable. - - `ForecastReportIssueVersionData::$worklogs` typed `array` - to reflect that keys come from `Worklog::getId()` (`?int`). - - Removed dead `readonly string $id` declarations on `HourReportData`, - `WorkloadReportData`, `InvoicingRateReportData` (never assigned, never read). - - Added `default => throw` arms to incomplete `match` expressions in - `DateTimeHelper::getFirstAndLastDateOfQuarter` and `LeantimeApiService`. - - Fixed `SubscriptionHandlerService::getVersion()` looking up - `VersionRepository` by non-existent `versionId` field; use - `find($versionId)` against the primary key instead. - - `User::getUserIdentifier()` now throws on empty email (Symfony's - `UserInterface` requires `non-empty-string`). - - `SubscriptionController::getFrequencies()` skips subscriptions with - a null `frequency` instead of dereferencing. - - `PlanningService` dropped extra argument to `Issue::getHoursRemaining()` - (method takes 0 parameters). - - `ProductsImportCommand` removed an unreachable `null === $row` check. - - `DanishHolidayHelper::$instance` `@var` corrected to `?DanishHolidayHelper` - to reflect that the singleton is unset before first call. - - `WorkloadReportServiceTest::testGetWorkloadReport` replaced redundant - `assertInstanceOf` calls (always-true on already-narrowed type) with - assertions on `$result->viewmode` that match the period type passed in. - Baseline 261 → 243. -* Cleaned up small PHPStan issues: corrected `Epic::getIssues()` PHPDoc - return type (`Collection`, was `Epic`), switched - `DanishHolidayHelper::getInstance()` from `empty()` to `isset()` on the - typed singleton property, removed unused `PaginatorInterface $paginator` - from `SubscriptionRepository::__construct()`, dropped the unused - `$worklogId`/`$description` parameters from `ForecastReportWorklogData` - (caller updated; properties were already set via direct assignment), - and removed the unreachable `break;` after a `throw` in - `SubscriptionHandlerService::handleSubscription()`. Baseline 268 → 261. -* Cleared all 167 PHPStan `missingType.iterableValue` baseline entries. - Removed redundant `@method find*` scaffold docblocks from 17 repositories - (the parent `@extends ServiceEntityRepository` already provides typed - signatures), and typed iterable parameters/returns/properties across - repositories, services, controllers, models, entities, forms, commands, - messages, and tests. Baseline 394 → 268. 28 latent call-site issues - (`argument.type`/`method.nonObject`) were surfaced by the tighter typing - and are now recorded in the baseline for follow-up — e.g., calls on - nullable repository results (`Issue|null`, `Project|null`, - `DateTime|false`) without null checks. -* Added generic-class typehints to clear all 50 PHPStan - `missingType.generics` entries: `@extends AbstractType` on 34 - form types, `@return PaginationInterface` on 9 repository - `getFilteredPagination()` methods, `ArrayCollection` on 5 report - model properties, and `ArrayIterator` on 3 service `@var` - docblocks. Baseline 454 → 394. -* Added missing parameter and return types across controllers, services, - repositories, and tests (`InvoiceController::generateDescription`, - `ManagementReportController::getInvoicesDataFromDates`, - `OpenIdConnectController::logout`, `SubscriptionController::subscriptionHandler`, - `Invoice::setInvoiceEntryIndexes`, `InvoiceEntry::setInvoiceIndex`, - `ClientHelper::getStandardPrice`, `LeantimeApiService::post`, - `ManagementReportService::generateSpreadsheetCsvResponse`/`calculateYear`, - `IssueRepository::getClosedIssuesFromInterval`, - `ProjectRepository::getProjectTrackerIdsByDataProviders`, - `SubscriptionRepository::findByCustom`/`findOneByCustom`, - `WorklogRepository::find{,Billed}WorklogsByWorkerAndDateRange`, and - several PHPUnit test methods). Clears 37 PHPStan `missingType.return` - and `missingType.parameter` entries; baseline 491 → 454. Tightening - `SubscriptionController::subscriptionHandler` to require `string` - surfaced three pre-existing `string|null` call-site issues, now - recorded in the baseline for follow-up. -* Added `Collection` generic typehints to OneToMany/ManyToMany - properties on `Client`, `Epic`, `Invoice`, `InvoiceEntry`, `Issue`, - `Product`, `Project`, `ProjectBilling`, and `Version` entities. Resolves - 20 PHPStan `doctrine.associationType` mismatches (and 19 cascading - inference errors); baseline shrinks from 530 to 491 entries. +* PHPStan baseline cleared (530 → 0). `src/` and `tests/` are now clean at + level 8 with zero suppressions (`phpstan-baseline.neon` is + `ignoreErrors: []`). The bulk of the work was PHPDoc/typehint annotations, + generic-class shapes, dropped scaffold `@method find*` docblocks on 17 + repositories, and null-guards. Items below are behavior- or + schema-affecting and worth calling out. +* Migration `Version20260517131038`: made 18 columns nullable on synced + entities (`issue`, `project`, `version`, `worklog`) and three DateTime + fields (`project_billing.period_{start,end}`, `service_agreement.valid_from`) + to match property types. +* Migration `Version20260517151632`: relaxed 11 `NOT NULL` ManyToOne FKs to + `NULL` (`cybersecurity_agreement`, `invoice_entry`, `issue_product`, + `product`, `project_billing`, `service_agreement`, `version`, `worklog`). + Application-layer validation (`#[Assert\NotNull]`, form validators) + continues to enforce required-ness. +* Replaced `?T $prop = null` with non-null defaults on 25 app-managed entity + properties to satisfy the existing `NOT NULL` columns: `''` for strings, + `false` for bools, `0`/`0.0` for numerics, `'0'` for `Product::$price` + (decimal), `HostingProviderEnum::ADM` for `ServiceAgreement::$hostingProvider`. + `Worker::__toString()` and `WorkerGroup::__toString()` switched from `??` + to `?:` to preserve empty-string-falls-through behavior. +* Real bugs surfaced and fixed during the cleanup: + - `ForecastReportService` was calling non-existent `Epic::getName()` (epic + tags rendered empty in the forecast); now uses `Epic::getTitle()`. + - `SubscriptionHandlerService::getVersion()` was looking up `Version` by a + non-existent `versionId` field; now uses `find($versionId)`. + - `DataProviderService::setTimeSpentSeconds()` was passing `float` through + to an `int` setter (silent precision loss); explicit `(int)` cast added. + - Report services (`ForecastReportService`, + `BillableUnbilledHoursReportService`, `InvoicingRateReportService`) + skip worklogs with a null project or issue instead of crashing on + chained getters. + - `SubscriptionController::check()` returns HTTP 400 on null + `User::getEmail()` rather than passing `?string` downstream; + `User::getUserIdentifier()` throws on empty email + (Symfony's `UserInterface` requires `non-empty-string`). + - `LeantimeApiService` switched from `json_decode($json, null)` to + `json_decode($json, true)`; all `$data->property` accesses converted to + array access. Wire shape unchanged. +* Misc dead code removed: `readonly string $id` on three report-data models + (never assigned, never read), an unreachable `break;` after `throw`, a + `null === $row` check on a `Row` iterator, an unused `PaginatorInterface + $paginator` constructor arg in `SubscriptionRepository`. Fixed an + `Epic::getIssues()` PHPDoc typo (`Collection` → + `Collection`). * Migrated static analysis from Psalm to PHPStan (level 8) with `phpstan/extension-installer`, `phpstan-symfony`, `phpstan-doctrine`, and `phpstan-phpunit`. The `composer code-analysis` entry point is unchanged. From 8e7187d96a64b054f519d14cde064d56200b5cee Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 18 May 2026 07:44:27 +0200 Subject: [PATCH 16/23] 7273: Changed to asterix --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1516a8b6..da531eb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,21 +30,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `Worker::__toString()` and `WorkerGroup::__toString()` switched from `??` to `?:` to preserve empty-string-falls-through behavior. * Real bugs surfaced and fixed during the cleanup: - - `ForecastReportService` was calling non-existent `Epic::getName()` (epic + * `ForecastReportService` was calling non-existent `Epic::getName()` (epic tags rendered empty in the forecast); now uses `Epic::getTitle()`. - - `SubscriptionHandlerService::getVersion()` was looking up `Version` by a + * `SubscriptionHandlerService::getVersion()` was looking up `Version` by a non-existent `versionId` field; now uses `find($versionId)`. - - `DataProviderService::setTimeSpentSeconds()` was passing `float` through + * `DataProviderService::setTimeSpentSeconds()` was passing `float` through to an `int` setter (silent precision loss); explicit `(int)` cast added. - - Report services (`ForecastReportService`, + * Report services (`ForecastReportService`, `BillableUnbilledHoursReportService`, `InvoicingRateReportService`) skip worklogs with a null project or issue instead of crashing on chained getters. - - `SubscriptionController::check()` returns HTTP 400 on null + * `SubscriptionController::check()` returns HTTP 400 on null `User::getEmail()` rather than passing `?string` downstream; `User::getUserIdentifier()` throws on empty email (Symfony's `UserInterface` requires `non-empty-string`). - - `LeantimeApiService` switched from `json_decode($json, null)` to + * `LeantimeApiService` switched from `json_decode($json, null)` to `json_decode($json, true)`; all `$data->property` accesses converted to array access. Wire shape unchanged. * Misc dead code removed: `readonly string $id` on three report-data models From afe5e532f88cbeb26705de1be24c1703436edef6 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Wed, 20 May 2026 10:47:42 +0200 Subject: [PATCH 17/23] 7273: Added MockObject type to mocks --- CHANGELOG.md | 14 +- phpstan-baseline.neon | 348 ------------------ .../SubscriptionHandlerCommandTest.php | 7 +- tests/Unit/Command/SyncCommandTest.php | 7 +- tests/Unit/Command/SyncDeletedCommandTest.php | 3 +- .../Unit/Command/SyncModifiedCommandTest.php | 3 +- ...tityRemovedFromDataProviderHandlerTest.php | 3 +- ...BillableUnbilledHoursReportServiceTest.php | 9 +- tests/Unit/Service/BillingServiceTest.php | 7 +- tests/Unit/Service/DashboardServiceTest.php | 7 +- .../Unit/Service/DataProviderServiceTest.php | 21 +- .../Service/ForecastReportServiceTest.php | 7 +- tests/Unit/Service/HourReportServiceTest.php | 5 +- .../InvoicingRateReportServiceTest.php | 7 +- .../Service/ManagementReportServiceTest.php | 3 +- tests/Unit/Service/PlanningServiceTest.php | 9 +- .../Service/ProjectBillingServiceTest.php | 17 +- .../SubscriptionHandlerServiceTest.php | 17 +- 18 files changed, 82 insertions(+), 412 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c17d00415..6817fb7e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] * [PR-308](https://github.com/itk-dev/economics/pull/308) - * PHPStan baseline cleared (530 → 0). `src/` and `tests/` are now clean at - level 8 with zero suppressions (`phpstan-baseline.neon` is - `ignoreErrors: []`). The bulk of the work was PHPDoc/typehint annotations, - generic-class shapes, dropped scaffold `@method find*` docblocks on 17 - repositories, and null-guards. Items below are behavior- or - schema-affecting and worth calling out. + * PHPStan baseline reduced from 530 → 0 in `src/` and the pre-merge `tests/`, + then bounced to 444 when develop's new integration/unit test suites + arrived, and is now at 187. The first pass cleaned `src/` to zero + suppressions via PHPDoc/typehint annotations, generic-class shapes, + dropped scaffold `@method find*` docblocks on 17 repositories, and + null-guards. The post-merge pass intersected PHPUnit mock properties + with `MockObject` across 16 test files, removing 257 baseline entries. + Items below are behavior- or schema-affecting and worth calling out. * Migration `Version20260517131038`: made 18 columns nullable on synced entities (`issue`, `project`, `version`, `worklog`) and three DateTime fields (`project_billing.period_{start,end}`, `service_agreement.valid_from`) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f773663ab..5a5a08e0c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -738,114 +738,12 @@ parameters: count: 1 path: tests/Integration/Service/WorkloadReportServiceTest.php - - - message: '#^Call to an undefined method App\\Repository\\SubscriptionRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 7 - path: tests/Unit/Command/SubscriptionHandlerCommandTest.php - - - - message: '#^Call to an undefined method App\\Service\\SubscriptionHandlerService\:\:expects\(\)\.$#' - identifier: method.notFound - count: 5 - path: tests/Unit/Command/SubscriptionHandlerCommandTest.php - - - - message: '#^Call to an undefined method App\\Service\\SubscriptionHandlerService\:\:method\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Command/SubscriptionHandlerCommandTest.php - - - - message: '#^Call to an undefined method Psr\\Log\\LoggerInterface\:\:expects\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Unit/Command/SubscriptionHandlerCommandTest.php - - - - message: '#^Call to an undefined method App\\Service\\LeantimeApiService\:\:expects\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Command/SyncCommandTest.php - - - - message: '#^Call to an undefined method Symfony\\Contracts\\HttpClient\\HttpClientInterface\:\:expects\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Command/SyncCommandTest.php - - - - message: '#^Call to an undefined method App\\Service\\LeantimeApiService\:\:expects\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Unit/Command/SyncDeletedCommandTest.php - - - - message: '#^Call to an undefined method App\\Service\\LeantimeApiService\:\:expects\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Unit/Command/SyncModifiedCommandTest.php - - - - message: '#^Call to an undefined method App\\Service\\DataProviderService\:\:expects\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/MessageHandler/EntityRemovedFromDataProviderHandlerTest.php - - - - message: '#^Call to an undefined method App\\Service\\DataProviderService\:\:method\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/MessageHandler/EntityRemovedFromDataProviderHandlerTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorkerRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorklogRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\DateTimeHelper\:\:expects\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\DateTimeHelper\:\:method\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\BillableUnbilledHoursReportData'' and App\\Model\\Reports\\BillableUnbilledHoursReportData will always evaluate to true\.$#' identifier: method.alreadyNarrowedType count: 2 path: tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php - - - message: '#^Call to an undefined method App\\Repository\\InvoiceEntryRepository\:\:expects\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/BillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\InvoiceRepository\:\:expects\(\)\.$#' - identifier: method.notFound - count: 5 - path: tests/Unit/Service/BillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\InvoiceRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 10 - path: tests/Unit/Service/BillingServiceTest.php - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|false given\.$#' identifier: argument.type @@ -858,96 +756,12 @@ parameters: count: 2 path: tests/Unit/Service/BillingServiceTest.php - - - message: '#^Call to an undefined method App\\Repository\\WorkerRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/DashboardServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorklogRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/DashboardServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\DateTimeHelper\:\:method\(\)\.$#' - identifier: method.notFound - count: 6 - path: tests/Unit/Service/DashboardServiceTest.php - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsFloat\(\) with float will always evaluate to true\.$#' identifier: method.alreadyNarrowedType count: 1 path: tests/Unit/Service/DashboardServiceTest.php - - - message: '#^Call to an undefined method App\\Repository\\EpicRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\IssueRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 7 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\ProjectRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 12 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\VersionRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorkerRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorklogRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 5 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method Doctrine\\ORM\\EntityManagerInterface\:\:expects\(\)\.$#' - identifier: method.notFound - count: 34 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method Doctrine\\ORM\\EntityManagerInterface\:\:method\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method Psr\\Log\\LoggerInterface\:\:expects\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/DataProviderServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorkerRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/ForecastReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorklogRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/ForecastReportServiceTest.php - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\ForecastReportData'' and App\\Model\\Reports\\ForecastReportData will always evaluate to true\.$#' identifier: method.alreadyNarrowedType @@ -966,24 +780,6 @@ parameters: count: 1 path: tests/Unit/Service/ForecastReportServiceTest.php - - - message: '#^Call to an undefined method App\\Repository\\IssueRepository\:\:expects\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/HourReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\IssueRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 6 - path: tests/Unit/Service/HourReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorklogRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 6 - path: tests/Unit/Service/HourReportServiceTest.php - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\HourReportData'' and App\\Model\\Reports\\HourReportData will always evaluate to true\.$#' identifier: method.alreadyNarrowedType @@ -1014,24 +810,6 @@ parameters: count: 1 path: tests/Unit/Service/HourReportServiceTest.php - - - message: '#^Call to an undefined method App\\Repository\\WorkerRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 5 - path: tests/Unit/Service/InvoicingRateReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorklogRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/InvoicingRateReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\DateTimeHelper\:\:method\(\)\.$#' - identifier: method.notFound - count: 7 - path: tests/Unit/Service/InvoicingRateReportServiceTest.php - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\InvoicingRateReportData'' and App\\Model\\Reports\\InvoicingRateReportData will always evaluate to true\.$#' identifier: method.alreadyNarrowedType @@ -1056,30 +834,6 @@ parameters: count: 2 path: tests/Unit/Service/ManagementReportServiceTest.php - - - message: '#^Call to an undefined method App\\Repository\\IssueRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 7 - path: tests/Unit/Service/PlanningServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\ProjectRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/PlanningServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorkerRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 6 - path: tests/Unit/Service/PlanningServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\DateTimeHelper\:\:method\(\)\.$#' - identifier: method.notFound - count: 7 - path: tests/Unit/Service/PlanningServiceTest.php - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Planning\\\\PlanningData'' and App\\Model\\Planning\\PlanningData will always evaluate to true\.$#' identifier: method.alreadyNarrowedType @@ -1092,110 +846,8 @@ parameters: count: 1 path: tests/Unit/Service/PlanningServiceTest.php - - - message: '#^Call to an undefined method App\\Repository\\ClientRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\IssueRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 10 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\ProjectBillingRepository\:\:expects\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\ProjectBillingRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 9 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\BillingService\:\:expects\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\BillingService\:\:method\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\ClientHelper\:\:method\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method Doctrine\\ORM\\EntityManagerInterface\:\:expects\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method Doctrine\\ORM\\EntityManagerInterface\:\:method\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Unit/Service/ProjectBillingServiceTest.php - - message: '#^Strict comparison using \=\=\= between ''Widget'' and bool will always evaluate to false\.$#' identifier: identical.alwaysFalse count: 1 path: tests/Unit/Service/ProjectBillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\ProjectRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 6 - path: tests/Unit/Service/SubscriptionHandlerServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\VersionRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/SubscriptionHandlerServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\HourReportService\:\:expects\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/SubscriptionHandlerServiceTest.php - - - - message: '#^Call to an undefined method App\\Service\\HourReportService\:\:method\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/SubscriptionHandlerServiceTest.php - - - - message: '#^Call to an undefined method Doctrine\\ORM\\EntityManagerInterface\:\:expects\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Unit/Service/SubscriptionHandlerServiceTest.php - - - - message: '#^Call to an undefined method Symfony\\Component\\Mailer\\MailerInterface\:\:expects\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Unit/Service/SubscriptionHandlerServiceTest.php - - - - message: '#^Call to an undefined method Symfony\\Contracts\\Translation\\TranslatorInterface\:\:method\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/SubscriptionHandlerServiceTest.php - - - - message: '#^Call to an undefined method Twig\\Environment\:\:method\(\)\.$#' - identifier: method.notFound - count: 5 - path: tests/Unit/Service/SubscriptionHandlerServiceTest.php diff --git a/tests/Unit/Command/SubscriptionHandlerCommandTest.php b/tests/Unit/Command/SubscriptionHandlerCommandTest.php index e0e70d26b..c92f53c23 100644 --- a/tests/Unit/Command/SubscriptionHandlerCommandTest.php +++ b/tests/Unit/Command/SubscriptionHandlerCommandTest.php @@ -8,6 +8,7 @@ use App\Enum\SubscriptionSubjectEnum; use App\Repository\SubscriptionRepository; use App\Service\SubscriptionHandlerService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; @@ -15,9 +16,9 @@ class SubscriptionHandlerCommandTest extends TestCase { - private SubscriptionRepository $subscriptionRepository; - private SubscriptionHandlerService $subscriptionHandlerService; - private LoggerInterface $logger; + private SubscriptionRepository&MockObject $subscriptionRepository; + private SubscriptionHandlerService&MockObject $subscriptionHandlerService; + private LoggerInterface&MockObject $logger; protected function setUp(): void { diff --git a/tests/Unit/Command/SyncCommandTest.php b/tests/Unit/Command/SyncCommandTest.php index 637e52bae..e087c83ea 100644 --- a/tests/Unit/Command/SyncCommandTest.php +++ b/tests/Unit/Command/SyncCommandTest.php @@ -5,6 +5,7 @@ use App\Command\SyncCommand; use App\Entity\Project; use App\Service\LeantimeApiService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; @@ -13,9 +14,9 @@ class SyncCommandTest extends TestCase { - private LeantimeApiService $leantimeApiService; - private HttpClientInterface $httpClient; - private LoggerInterface $logger; + private LeantimeApiService&MockObject $leantimeApiService; + private HttpClientInterface&MockObject $httpClient; + private LoggerInterface&MockObject $logger; private CommandTester $commandTester; protected function setUp(): void diff --git a/tests/Unit/Command/SyncDeletedCommandTest.php b/tests/Unit/Command/SyncDeletedCommandTest.php index bf1b92f6a..834950f21 100644 --- a/tests/Unit/Command/SyncDeletedCommandTest.php +++ b/tests/Unit/Command/SyncDeletedCommandTest.php @@ -4,13 +4,14 @@ use App\Command\SyncDeletedCommand; use App\Service\LeantimeApiService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; class SyncDeletedCommandTest extends TestCase { - private LeantimeApiService $leantimeApiService; + private LeantimeApiService&MockObject $leantimeApiService; private CommandTester $commandTester; protected function setUp(): void diff --git a/tests/Unit/Command/SyncModifiedCommandTest.php b/tests/Unit/Command/SyncModifiedCommandTest.php index 05a0f42cf..139beb387 100644 --- a/tests/Unit/Command/SyncModifiedCommandTest.php +++ b/tests/Unit/Command/SyncModifiedCommandTest.php @@ -4,13 +4,14 @@ use App\Command\SyncModifiedCommand; use App\Service\LeantimeApiService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; class SyncModifiedCommandTest extends TestCase { - private LeantimeApiService $leantimeApiService; + private LeantimeApiService&MockObject $leantimeApiService; private CommandTester $commandTester; protected function setUp(): void diff --git a/tests/Unit/MessageHandler/EntityRemovedFromDataProviderHandlerTest.php b/tests/Unit/MessageHandler/EntityRemovedFromDataProviderHandlerTest.php index 165f8af75..1a9d79ea0 100644 --- a/tests/Unit/MessageHandler/EntityRemovedFromDataProviderHandlerTest.php +++ b/tests/Unit/MessageHandler/EntityRemovedFromDataProviderHandlerTest.php @@ -9,13 +9,14 @@ use App\Message\EntityRemovedFromDataProviderMessage; use App\MessageHandler\EntityRemovedFromDataProviderHandler; use App\Service\DataProviderService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; class EntityRemovedFromDataProviderHandlerTest extends TestCase { - private DataProviderService $service; + private DataProviderService&MockObject $service; private EntityRemovedFromDataProviderHandler $handler; protected function setUp(): void diff --git a/tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php b/tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php index 2a404573c..4adb47ca3 100644 --- a/tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php +++ b/tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php @@ -10,15 +10,16 @@ use App\Repository\WorklogRepository; use App\Service\BillableUnbilledHoursReportService; use App\Service\DateTimeHelper; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Contracts\Translation\TranslatorInterface; class BillableUnbilledHoursReportServiceTest extends TestCase { - private WorklogRepository $worklogRepository; - private DateTimeHelper $dateTimeHelper; - private WorkerRepository $workerRepository; - private TranslatorInterface $translator; + private WorklogRepository&MockObject $worklogRepository; + private DateTimeHelper&MockObject $dateTimeHelper; + private WorkerRepository&MockObject $workerRepository; + private TranslatorInterface&MockObject $translator; private BillableUnbilledHoursReportService $service; protected function setUp(): void diff --git a/tests/Unit/Service/BillingServiceTest.php b/tests/Unit/Service/BillingServiceTest.php index b0f0c662d..8ed3c822f 100644 --- a/tests/Unit/Service/BillingServiceTest.php +++ b/tests/Unit/Service/BillingServiceTest.php @@ -16,14 +16,15 @@ use App\Repository\InvoiceEntryRepository; use App\Repository\InvoiceRepository; use App\Service\BillingService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Contracts\Translation\TranslatorInterface; class BillingServiceTest extends TestCase { - private InvoiceRepository $invoiceRepository; - private InvoiceEntryRepository $invoiceEntryRepository; - private TranslatorInterface $translator; + private InvoiceRepository&MockObject $invoiceRepository; + private InvoiceEntryRepository&MockObject $invoiceEntryRepository; + private TranslatorInterface&MockObject $translator; private BillingService $billingService; protected function setUp(): void diff --git a/tests/Unit/Service/DashboardServiceTest.php b/tests/Unit/Service/DashboardServiceTest.php index a6ef180d8..69d886f5e 100644 --- a/tests/Unit/Service/DashboardServiceTest.php +++ b/tests/Unit/Service/DashboardServiceTest.php @@ -9,13 +9,14 @@ use App\Repository\WorklogRepository; use App\Service\DashboardService; use App\Service\DateTimeHelper; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class DashboardServiceTest extends TestCase { - private WorkerRepository $workerRepository; - private WorklogRepository $worklogRepository; - private DateTimeHelper $dateTimeHelper; + private WorkerRepository&MockObject $workerRepository; + private WorklogRepository&MockObject $worklogRepository; + private DateTimeHelper&MockObject $dateTimeHelper; private DashboardService $dashboardService; protected function setUp(): void diff --git a/tests/Unit/Service/DataProviderServiceTest.php b/tests/Unit/Service/DataProviderServiceTest.php index 044384144..4f369ce06 100644 --- a/tests/Unit/Service/DataProviderServiceTest.php +++ b/tests/Unit/Service/DataProviderServiceTest.php @@ -25,22 +25,23 @@ use App\Service\DataProviderService; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; class DataProviderServiceTest extends TestCase { - private EntityManagerInterface $entityManager; - private ProjectRepository $projectRepository; - private IssueRepository $issueRepository; - private WorklogRepository $worklogRepository; - private DataProviderRepository $dataProviderRepository; - private VersionRepository $versionRepository; - private WorkerRepository $workerRepository; - private EpicRepository $epicRepository; - private ContainerInterface $transportLocator; - private LoggerInterface $logger; + private EntityManagerInterface&MockObject $entityManager; + private ProjectRepository&MockObject $projectRepository; + private IssueRepository&MockObject $issueRepository; + private WorklogRepository&MockObject $worklogRepository; + private DataProviderRepository&MockObject $dataProviderRepository; + private VersionRepository&MockObject $versionRepository; + private WorkerRepository&MockObject $workerRepository; + private EpicRepository&MockObject $epicRepository; + private ContainerInterface&MockObject $transportLocator; + private LoggerInterface&MockObject $logger; private DataProviderService $service; private DataProvider $dataProvider; diff --git a/tests/Unit/Service/ForecastReportServiceTest.php b/tests/Unit/Service/ForecastReportServiceTest.php index 66b572515..becf7cbbc 100644 --- a/tests/Unit/Service/ForecastReportServiceTest.php +++ b/tests/Unit/Service/ForecastReportServiceTest.php @@ -12,13 +12,14 @@ use App\Service\ForecastReportService; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class ForecastReportServiceTest extends TestCase { - private WorklogRepository $worklogRepository; - private WorkerRepository $workerRepository; - private EntityManagerInterface $entityManager; + private WorklogRepository&MockObject $worklogRepository; + private WorkerRepository&MockObject $workerRepository; + private EntityManagerInterface&MockObject $entityManager; private ForecastReportService $service; protected function setUp(): void diff --git a/tests/Unit/Service/HourReportServiceTest.php b/tests/Unit/Service/HourReportServiceTest.php index a3d1ad4d9..0f51642bf 100644 --- a/tests/Unit/Service/HourReportServiceTest.php +++ b/tests/Unit/Service/HourReportServiceTest.php @@ -11,12 +11,13 @@ use App\Repository\IssueRepository; use App\Repository\WorklogRepository; use App\Service\HourReportService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class HourReportServiceTest extends TestCase { - private IssueRepository $issueRepository; - private WorklogRepository $worklogRepository; + private IssueRepository&MockObject $issueRepository; + private WorklogRepository&MockObject $worklogRepository; private HourReportService $hourReportService; public function setUp(): void diff --git a/tests/Unit/Service/InvoicingRateReportServiceTest.php b/tests/Unit/Service/InvoicingRateReportServiceTest.php index a2132e9ea..6e7139f56 100644 --- a/tests/Unit/Service/InvoicingRateReportServiceTest.php +++ b/tests/Unit/Service/InvoicingRateReportServiceTest.php @@ -9,13 +9,14 @@ use App\Repository\WorklogRepository; use App\Service\DateTimeHelper; use App\Service\InvoicingRateReportService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class InvoicingRateReportServiceTest extends TestCase { - private WorkerRepository $workerRepository; - private WorklogRepository $worklogRepository; - private DateTimeHelper $dateTimeHelper; + private WorkerRepository&MockObject $workerRepository; + private WorklogRepository&MockObject $worklogRepository; + private DateTimeHelper&MockObject $dateTimeHelper; private InvoicingRateReportService $service; protected function setUp(): void diff --git a/tests/Unit/Service/ManagementReportServiceTest.php b/tests/Unit/Service/ManagementReportServiceTest.php index d809a0e15..77a95b90b 100644 --- a/tests/Unit/Service/ManagementReportServiceTest.php +++ b/tests/Unit/Service/ManagementReportServiceTest.php @@ -3,13 +3,14 @@ namespace App\Tests\Unit\Service; use App\Service\ManagementReportService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Contracts\Translation\TranslatorInterface; class ManagementReportServiceTest extends TestCase { - private TranslatorInterface $translator; + private TranslatorInterface&MockObject $translator; private ManagementReportService $service; protected function setUp(): void diff --git a/tests/Unit/Service/PlanningServiceTest.php b/tests/Unit/Service/PlanningServiceTest.php index 3d1159798..042ee0c54 100644 --- a/tests/Unit/Service/PlanningServiceTest.php +++ b/tests/Unit/Service/PlanningServiceTest.php @@ -12,14 +12,15 @@ use App\Repository\WorkerRepository; use App\Service\DateTimeHelper; use App\Service\PlanningService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class PlanningServiceTest extends TestCase { - private DateTimeHelper $dateTimeHelper; - private IssueRepository $issueRepository; - private WorkerRepository $workerRepository; - private ProjectRepository $projectRepository; + private DateTimeHelper&MockObject $dateTimeHelper; + private IssueRepository&MockObject $issueRepository; + private WorkerRepository&MockObject $workerRepository; + private ProjectRepository&MockObject $projectRepository; private PlanningService $service; protected function setUp(): void diff --git a/tests/Unit/Service/ProjectBillingServiceTest.php b/tests/Unit/Service/ProjectBillingServiceTest.php index c6e1928ca..59aae2bda 100644 --- a/tests/Unit/Service/ProjectBillingServiceTest.php +++ b/tests/Unit/Service/ProjectBillingServiceTest.php @@ -23,19 +23,20 @@ use App\Service\InvoiceEntryHelper; use App\Service\ProjectBillingService; use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Contracts\Translation\TranslatorInterface; class ProjectBillingServiceTest extends TestCase { - private ProjectBillingRepository $projectBillingRepository; - private BillingService $billingService; - private IssueRepository $issueRepository; - private ClientRepository $clientRepository; - private ClientHelper $clientHelper; - private EntityManagerInterface $entityManager; - private TranslatorInterface $translator; - private InvoiceEntryHelper $invoiceEntryHelper; + private ProjectBillingRepository&MockObject $projectBillingRepository; + private BillingService&MockObject $billingService; + private IssueRepository&MockObject $issueRepository; + private ClientRepository&MockObject $clientRepository; + private ClientHelper&MockObject $clientHelper; + private EntityManagerInterface&MockObject $entityManager; + private TranslatorInterface&MockObject $translator; + private InvoiceEntryHelper&MockObject $invoiceEntryHelper; private ProjectBillingService $service; protected function setUp(): void diff --git a/tests/Unit/Service/SubscriptionHandlerServiceTest.php b/tests/Unit/Service/SubscriptionHandlerServiceTest.php index 2ec28409c..d3242c35d 100644 --- a/tests/Unit/Service/SubscriptionHandlerServiceTest.php +++ b/tests/Unit/Service/SubscriptionHandlerServiceTest.php @@ -13,6 +13,7 @@ use App\Service\HourReportService; use App\Service\SubscriptionHandlerService; use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\MailerInterface; @@ -21,14 +22,14 @@ class SubscriptionHandlerServiceTest extends TestCase { - private ProjectRepository $projectRepository; - private VersionRepository $versionRepository; - private HourReportService $hourReportService; - private Environment $twig; - private LoggerInterface $logger; - private MailerInterface $mailer; - private TranslatorInterface $translator; - private EntityManagerInterface $entityManager; + private ProjectRepository&MockObject $projectRepository; + private VersionRepository&MockObject $versionRepository; + private HourReportService&MockObject $hourReportService; + private Environment&MockObject $twig; + private LoggerInterface&MockObject $logger; + private MailerInterface&MockObject $mailer; + private TranslatorInterface&MockObject $translator; + private EntityManagerInterface&MockObject $entityManager; protected function setUp(): void { From 549158f80df1558c0b3eccabfb7a5b15f7fc116a Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Wed, 20 May 2026 11:06:54 +0200 Subject: [PATCH 18/23] 7273: Removed redundant asserts --- CHANGELOG.md | 9 +- phpstan-baseline.neon | 246 ------------------ .../Repository/AccountRepositoryTest.php | 3 - .../Repository/ClientRepositoryTest.php | 2 - .../CybersecurityAgreementRepositoryTest.php | 3 - .../Repository/InvoiceRepositoryTest.php | 4 - .../Repository/IssueRepositoryTest.php | 4 - .../Repository/ProductRepositoryTest.php | 2 - .../ProjectBillingRepositoryTest.php | 2 - .../Repository/ProjectRepositoryTest.php | 6 - .../ServiceAgreementRepositoryTest.php | 3 - .../Repository/SubscriptionRepositoryTest.php | 1 - .../Repository/WorkerGroupRepositoryTest.php | 2 - .../Repository/WorkerRepositoryTest.php | 4 - .../Repository/WorklogRepositoryTest.php | 13 - ...BillableUnbilledHoursReportServiceTest.php | 2 - .../Service/ForecastReportServiceTest.php | 2 - .../Service/HourReportServiceTest.php | 6 - .../InvoicingRateReportServiceTest.php | 4 - .../Service/WorkloadReportServiceTest.php | 4 - ...BillableUnbilledHoursReportServiceTest.php | 5 - tests/Unit/Service/DashboardServiceTest.php | 1 - .../Service/ForecastReportServiceTest.php | 2 - tests/Unit/Service/HourReportServiceTest.php | 5 - .../InvoicingRateReportServiceTest.php | 3 - tests/Unit/Service/PlanningServiceTest.php | 2 - 26 files changed, 6 insertions(+), 334 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6817fb7e7..cd39df7b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [PR-308](https://github.com/itk-dev/economics/pull/308) * PHPStan baseline reduced from 530 → 0 in `src/` and the pre-merge `tests/`, then bounced to 444 when develop's new integration/unit test suites - arrived, and is now at 187. The first pass cleaned `src/` to zero + arrived, and is now at 141. The first pass cleaned `src/` to zero suppressions via PHPDoc/typehint annotations, generic-class shapes, dropped scaffold `@method find*` docblocks on 17 repositories, and - null-guards. The post-merge pass intersected PHPUnit mock properties - with `MockObject` across 16 test files, removing 257 baseline entries. + null-guards. Post-merge passes intersected PHPUnit mock properties + with `MockObject` across 16 test files (−257 entries), and dropped + redundant `assertInstanceOf` / `assertIsArray` / `assertIsFloat` / + `assertArrayHasKey` calls whose target type was already statically + proved across 24 test files (−46 entries). Items below are behavior- or schema-affecting and worth calling out. * Migration `Version20260517131038`: made 18 columns nullable on synced entities (`issue`, `project`, `version`, `worklog`) and three DateTime diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5a5a08e0c..53573f7d1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -288,30 +288,12 @@ parameters: count: 1 path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/AccountRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with array\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/AccountRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\AccountRepositoryTest\:\:\$repository \(App\\Repository\\AccountRepository\) does not accept object\.$#' identifier: assign.propertyType count: 1 path: tests/Integration/Repository/AccountRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ClientRepositoryTest.php - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' identifier: argument.type @@ -324,36 +306,12 @@ parameters: count: 1 path: tests/Integration/Repository/ClientRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Entity\\\\CybersecurityAgreement'' and App\\Entity\\CybersecurityAgreement will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with non\-empty\-array\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\CybersecurityAgreementRepositoryTest\:\:\$repository \(App\\Repository\\CybersecurityAgreementRepository\) does not accept object\.$#' identifier: assign.propertyType count: 1 path: tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Entity\\\\Invoice'' and App\\Entity\\Invoice will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/InvoiceRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/InvoiceRepositoryTest.php - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' identifier: argument.type @@ -372,24 +330,6 @@ parameters: count: 2 path: tests/Integration/Repository/IssueRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Entity\\\\Issue'' and App\\Entity\\Issue will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/IssueRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/IssueRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with array\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/IssueRepositoryTest.php - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' identifier: method.nonObject @@ -450,12 +390,6 @@ parameters: count: 1 path: tests/Integration/Repository/ProductRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ProductRepositoryTest.php - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' identifier: method.nonObject @@ -474,12 +408,6 @@ parameters: count: 1 path: tests/Integration/Repository/ProductRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ProjectBillingRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\ProjectBillingRepositoryTest\:\:\$repository \(App\\Repository\\ProjectBillingRepository\) does not accept object\.$#' identifier: assign.propertyType @@ -492,24 +420,6 @@ parameters: count: 1 path: tests/Integration/Repository/ProjectRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Doctrine\\\\ORM\\\\QueryBuilder'' and Doctrine\\ORM\\QueryBuilder will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with non\-empty\-array\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' identifier: argument.type @@ -534,54 +444,24 @@ parameters: count: 1 path: tests/Integration/Repository/ProjectRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ServiceAgreementRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with non\-empty\-array\\> will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ServiceAgreementRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\ServiceAgreementRepositoryTest\:\:\$repository \(App\\Repository\\ServiceAgreementRepository\) does not accept object\.$#' identifier: assign.propertyType count: 1 path: tests/Integration/Repository/ServiceAgreementRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Entity\\\\Subscription'' and App\\Entity\\Subscription will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/SubscriptionRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\SubscriptionRepositoryTest\:\:\$repository \(App\\Repository\\SubscriptionRepository\) does not accept object\.$#' identifier: assign.propertyType count: 1 path: tests/Integration/Repository/SubscriptionRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Knp\\\\Component\\\\Pager\\\\Pagination\\\\PaginationInterface'' and Knp\\Component\\Pager\\Pagination\\PaginationInterface\ will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/WorkerGroupRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\WorkerGroupRepositoryTest\:\:\$repository \(App\\Repository\\WorkerGroupRepository\) does not accept object\.$#' identifier: assign.propertyType count: 1 path: tests/Integration/Repository/WorkerGroupRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Entity\\\\Worker'' and App\\Entity\\Worker will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/WorkerRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\WorkerRepositoryTest\:\:\$entityManager \(Doctrine\\ORM\\EntityManagerInterface\) does not accept object\.$#' identifier: assign.propertyType @@ -600,48 +480,6 @@ parameters: count: 5 path: tests/Integration/Repository/WorklogRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertArrayHasKey\(\) with ''current_page'' and array\{total_count\: int, pages_count\: float, current_page\: int, page_size\: int, paginator\: Doctrine\\ORM\\Tools\\Pagination\\Paginator\\} will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertArrayHasKey\(\) with ''page_size'' and array\{total_count\: int, pages_count\: float, current_page\: int, page_size\: int, paginator\: Doctrine\\ORM\\Tools\\Pagination\\Paginator\\} will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertArrayHasKey\(\) with ''pages_count'' and array\{total_count\: int, pages_count\: float, current_page\: int, page_size\: int, paginator\: Doctrine\\ORM\\Tools\\Pagination\\Paginator\\} will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertArrayHasKey\(\) with ''paginator'' and array\{total_count\: int, pages_count\: float, current_page\: int, page_size\: int, paginator\: Doctrine\\ORM\\Tools\\Pagination\\Paginator\\} will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertArrayHasKey\(\) with ''total_count'' and array\{total_count\: int, pages_count\: float, current_page\: int, page_size\: int, paginator\: Doctrine\\ORM\\Tools\\Pagination\\Paginator\\} will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Entity\\\\Worklog'' and App\\Entity\\Worklog will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 2 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with array\{total_count\: int, pages_count\: float, current_page\: int, page_size\: int, paginator\: Doctrine\\ORM\\Tools\\Pagination\\Paginator\\} will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - message: '#^Parameter \#1 \$project of method App\\Repository\\WorklogRepository\:\:findByFilterData\(\) expects App\\Entity\\Project, App\\Entity\\Project\|null given\.$#' identifier: argument.type @@ -678,72 +516,18 @@ parameters: count: 1 path: tests/Integration/Repository/WorklogRepositoryTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\BillableUnbilledHoursReportData'' and App\\Model\\Reports\\BillableUnbilledHoursReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\ForecastReportData'' and App\\Model\\Reports\\ForecastReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/ForecastReportServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\HourReportData'' and App\\Model\\Reports\\HourReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/HourReportServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\HourReportProjectTag'' and App\\Model\\Reports\\HourReportProjectTag will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 2 - path: tests/Integration/Service/HourReportServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\InvoicingRateReportData'' and App\\Model\\Reports\\InvoicingRateReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/InvoicingRateReportServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\InvoicingRateReportWorker'' and App\\Model\\Reports\\InvoicingRateReportWorker will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/InvoicingRateReportServiceTest.php - - message: '#^You should use assertCount\(\$expectedCount, \$variable\) instead of assertSame\(\$expectedCount, \$variable\-\>count\(\)\)\.$#' identifier: phpunit.assertCount count: 1 path: tests/Integration/Service/InvoicingRateReportServiceTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\WorkloadReportData'' and App\\Model\\Reports\\WorkloadReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/WorkloadReportServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\WorkloadReportWorker'' and App\\Model\\Reports\\WorkloadReportWorker will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/WorkloadReportServiceTest.php - - message: '#^You should use assertCount\(\$expectedCount, \$variable\) instead of assertSame\(\$expectedCount, \$variable\-\>count\(\)\)\.$#' identifier: phpunit.assertCount count: 1 path: tests/Integration/Service/WorkloadReportServiceTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\BillableUnbilledHoursReportData'' and App\\Model\\Reports\\BillableUnbilledHoursReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 2 - path: tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|false given\.$#' identifier: argument.type @@ -756,18 +540,6 @@ parameters: count: 2 path: tests/Unit/Service/BillingServiceTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsFloat\(\) with float will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Unit/Service/DashboardServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\ForecastReportData'' and App\\Model\\Reports\\ForecastReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Unit/Service/ForecastReportServiceTest.php - - message: '#^Cannot access property \$invoiced on App\\Model\\Reports\\ForecastReportProjectData\|null\.$#' identifier: property.nonObject @@ -780,12 +552,6 @@ parameters: count: 1 path: tests/Unit/Service/ForecastReportServiceTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\HourReportData'' and App\\Model\\Reports\\HourReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 2 - path: tests/Unit/Service/HourReportServiceTest.php - - message: '#^Cannot access property \$projectTickets on App\\Model\\Reports\\HourReportProjectTag\|null\.$#' identifier: property.nonObject @@ -810,12 +576,6 @@ parameters: count: 1 path: tests/Unit/Service/HourReportServiceTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\InvoicingRateReportData'' and App\\Model\\Reports\\InvoicingRateReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 2 - path: tests/Unit/Service/InvoicingRateReportServiceTest.php - - message: '#^Cannot access property \$average on App\\Model\\Reports\\InvoicingRateReportWorker\|false\.$#' identifier: property.nonObject @@ -834,12 +594,6 @@ parameters: count: 2 path: tests/Unit/Service/ManagementReportServiceTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Planning\\\\PlanningData'' and App\\Model\\Planning\\PlanningData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Unit/Service/PlanningServiceTest.php - - message: '#^Cannot access property \$displayName on App\\Model\\Planning\\Assignee\|null\.$#' identifier: property.nonObject diff --git a/tests/Integration/Repository/AccountRepositoryTest.php b/tests/Integration/Repository/AccountRepositoryTest.php index 25e149967..63129e24f 100644 --- a/tests/Integration/Repository/AccountRepositoryTest.php +++ b/tests/Integration/Repository/AccountRepositoryTest.php @@ -4,7 +4,6 @@ use App\Model\Invoices\NameFilterData; use App\Repository\AccountRepository; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class AccountRepositoryTest extends KernelTestCase @@ -21,7 +20,6 @@ public function testGetAllChoices(): void { $result = $this->repository->getAllChoices(); - $this->assertIsArray($result); $this->assertGreaterThanOrEqual(2, \count($result)); $this->assertArrayHasKey('ACC001: Test Account 1', $result); @@ -36,7 +34,6 @@ public function testGetFilteredPaginationNoFilter(): void $filterData = new NameFilterData(); $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThanOrEqual(2, $result->getTotalItemCount()); } diff --git a/tests/Integration/Repository/ClientRepositoryTest.php b/tests/Integration/Repository/ClientRepositoryTest.php index 9032dfff8..cdb8e7a47 100644 --- a/tests/Integration/Repository/ClientRepositoryTest.php +++ b/tests/Integration/Repository/ClientRepositoryTest.php @@ -4,7 +4,6 @@ use App\Model\Invoices\ClientFilterData; use App\Repository\ClientRepository; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class ClientRepositoryTest extends KernelTestCase @@ -22,7 +21,6 @@ public function testGetFilteredPaginationNoFilter(): void $filterData = new ClientFilterData(); $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThanOrEqual(4, $result->getTotalItemCount()); } diff --git a/tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php b/tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php index 152467bb3..d467f2d1e 100644 --- a/tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php +++ b/tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Integration\Repository; -use App\Entity\CybersecurityAgreement; use App\Repository\CybersecurityAgreementRepository; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -21,10 +20,8 @@ public function testFindAllIndexed(): void $result = $this->repository->findAllIndexed(); $this->assertNotEmpty($result); - $this->assertIsArray($result); foreach ($result as $key => $entity) { - $this->assertInstanceOf(CybersecurityAgreement::class, $entity); $this->assertEquals($entity->getId(), $key); } } diff --git a/tests/Integration/Repository/InvoiceRepositoryTest.php b/tests/Integration/Repository/InvoiceRepositoryTest.php index f8e6ca534..ad7702f30 100644 --- a/tests/Integration/Repository/InvoiceRepositoryTest.php +++ b/tests/Integration/Repository/InvoiceRepositoryTest.php @@ -2,10 +2,8 @@ namespace App\Tests\Integration\Repository; -use App\Entity\Invoice; use App\Model\Invoices\InvoiceFilterData; use App\Repository\InvoiceRepository; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class InvoiceRepositoryTest extends KernelTestCase @@ -27,7 +25,6 @@ public function testGetByRecordedDateBetween(): void $this->assertNotEmpty($result); foreach ($result as $invoice) { - $this->assertInstanceOf(Invoice::class, $invoice); $this->assertTrue($invoice->isRecorded()); $this->assertNotNull($invoice->getRecordedDate()); } @@ -49,7 +46,6 @@ public function testGetFilteredPaginationRecorded(): void $filterData->recorded = true; $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThanOrEqual(2, $result->getTotalItemCount()); foreach ($result as $invoice) { $this->assertTrue($invoice->isRecorded()); diff --git a/tests/Integration/Repository/IssueRepositoryTest.php b/tests/Integration/Repository/IssueRepositoryTest.php index 3de42e486..81b30b22a 100644 --- a/tests/Integration/Repository/IssueRepositoryTest.php +++ b/tests/Integration/Repository/IssueRepositoryTest.php @@ -12,7 +12,6 @@ use App\Repository\VersionRepository; use App\Repository\WorkerGroupRepository; use Doctrine\ORM\EntityManagerInterface; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class IssueRepositoryTest extends KernelTestCase @@ -35,7 +34,6 @@ public function testGetFilteredPaginationNoFilter(): void $filterData = new IssueFilterData(); $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThan(0, $result->getTotalItemCount()); } @@ -70,7 +68,6 @@ public function testFindEpicOptionsByProject(): void $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $result = $this->repository->findEpicOptionsByProject($project); - $this->assertIsArray($result); $this->assertArrayHasKey('Epic 1', $result); } @@ -110,7 +107,6 @@ public function testIssuesContainingVersion(): void $this->assertNotEmpty($result); foreach ($result as $issue) { - $this->assertInstanceOf(Issue::class, $issue); $versionIds = $issue->getVersions()->map(fn (Version $v) => $v->getId())->toArray(); $this->assertContains($version->getId(), $versionIds); } diff --git a/tests/Integration/Repository/ProductRepositoryTest.php b/tests/Integration/Repository/ProductRepositoryTest.php index 18e6faff4..e7408594d 100644 --- a/tests/Integration/Repository/ProductRepositoryTest.php +++ b/tests/Integration/Repository/ProductRepositoryTest.php @@ -5,7 +5,6 @@ use App\Model\Invoices\ProductFilterData; use App\Repository\ProductRepository; use App\Repository\ProjectRepository; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class ProductRepositoryTest extends KernelTestCase @@ -23,7 +22,6 @@ public function testGetFilteredPaginationNoFilter(): void $filterData = new ProductFilterData(); $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThanOrEqual(3, $result->getTotalItemCount()); } diff --git a/tests/Integration/Repository/ProjectBillingRepositoryTest.php b/tests/Integration/Repository/ProjectBillingRepositoryTest.php index d2cd72b27..f9ab1ff73 100644 --- a/tests/Integration/Repository/ProjectBillingRepositoryTest.php +++ b/tests/Integration/Repository/ProjectBillingRepositoryTest.php @@ -4,7 +4,6 @@ use App\Model\Invoices\ProjectBillingFilterData; use App\Repository\ProjectBillingRepository; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class ProjectBillingRepositoryTest extends KernelTestCase @@ -23,7 +22,6 @@ public function testGetFilteredPaginationRecorded(): void $filterData->recorded = true; $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThanOrEqual(1, $result->getTotalItemCount()); foreach ($result as $pb) { $this->assertTrue($pb->isRecorded()); diff --git a/tests/Integration/Repository/ProjectRepositoryTest.php b/tests/Integration/Repository/ProjectRepositoryTest.php index b46dcbe75..8b8b1fb74 100644 --- a/tests/Integration/Repository/ProjectRepositoryTest.php +++ b/tests/Integration/Repository/ProjectRepositoryTest.php @@ -7,8 +7,6 @@ use App\Repository\DataProviderRepository; use App\Repository\ProjectRepository; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\QueryBuilder; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class ProjectRepositoryTest extends KernelTestCase @@ -28,8 +26,6 @@ public function testGetIncluded(): void { $qb = $this->repository->getIncluded(); - $this->assertInstanceOf(QueryBuilder::class, $qb); - $results = $qb->getQuery()->getResult(); $this->assertNotEmpty($results); @@ -50,7 +46,6 @@ public function testGetFilteredPaginationIncluded(): void $filterData->include = true; $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThanOrEqual(20, $result->getTotalItemCount()); } @@ -102,7 +97,6 @@ public function testGetProjectTrackerIdsByDataProviders(): void $result = $this->repository->getProjectTrackerIdsByDataProviders($dataProviders); $this->assertNotEmpty($result); - $this->assertIsArray($result); // Verify results are sorted $sorted = $result; diff --git a/tests/Integration/Repository/ServiceAgreementRepositoryTest.php b/tests/Integration/Repository/ServiceAgreementRepositoryTest.php index e1bc28146..fd397ff7d 100644 --- a/tests/Integration/Repository/ServiceAgreementRepositoryTest.php +++ b/tests/Integration/Repository/ServiceAgreementRepositoryTest.php @@ -5,7 +5,6 @@ use App\Enum\HostingProviderEnum; use App\Model\Invoices\ServiceAgreementFilterData; use App\Repository\ServiceAgreementRepository; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class ServiceAgreementRepositoryTest extends KernelTestCase @@ -23,7 +22,6 @@ public function testGetFilteredPaginationNoFilter(): void $filterData = new ServiceAgreementFilterData(); $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThanOrEqual(3, $result->getTotalItemCount()); } @@ -86,7 +84,6 @@ public function testGetApiServiceAgreements(): void $result = $this->repository->getApiServiceAgreements(); $this->assertNotEmpty($result); - $this->assertIsArray($result); foreach ($result as $item) { $this->assertArrayHasKey('projectTrackerKey', $item); diff --git a/tests/Integration/Repository/SubscriptionRepositoryTest.php b/tests/Integration/Repository/SubscriptionRepositoryTest.php index 8e1162ca8..b95564fdb 100644 --- a/tests/Integration/Repository/SubscriptionRepositoryTest.php +++ b/tests/Integration/Repository/SubscriptionRepositoryTest.php @@ -23,7 +23,6 @@ public function testFindByCustom(): void $this->assertCount(2, $result); foreach ($result as $subscription) { - $this->assertInstanceOf(Subscription::class, $subscription); $this->assertEquals('subscriber@test.com', $subscription->getEmail()); } } diff --git a/tests/Integration/Repository/WorkerGroupRepositoryTest.php b/tests/Integration/Repository/WorkerGroupRepositoryTest.php index 6623743a1..4cc115744 100644 --- a/tests/Integration/Repository/WorkerGroupRepositoryTest.php +++ b/tests/Integration/Repository/WorkerGroupRepositoryTest.php @@ -4,7 +4,6 @@ use App\Model\Invoices\NameFilterData; use App\Repository\WorkerGroupRepository; -use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class WorkerGroupRepositoryTest extends KernelTestCase @@ -22,7 +21,6 @@ public function testGetFilteredPaginationNoFilter(): void $filterData = new NameFilterData(); $result = $this->repository->getFilteredPagination($filterData); - $this->assertInstanceOf(PaginationInterface::class, $result); $this->assertGreaterThanOrEqual(2, $result->getTotalItemCount()); } diff --git a/tests/Integration/Repository/WorkerRepositoryTest.php b/tests/Integration/Repository/WorkerRepositoryTest.php index b2a9a77bb..516d5dc24 100644 --- a/tests/Integration/Repository/WorkerRepositoryTest.php +++ b/tests/Integration/Repository/WorkerRepositoryTest.php @@ -26,10 +26,6 @@ public function testFindAllIncludedInReports(): void $this->assertNotEmpty($results); $this->assertGreaterThanOrEqual(10, \count($results)); - - foreach ($results as $worker) { - $this->assertInstanceOf(Worker::class, $worker); - } } public function testFindAllIncludedInReportsExcludesDisabled(): void diff --git a/tests/Integration/Repository/WorklogRepositoryTest.php b/tests/Integration/Repository/WorklogRepositoryTest.php index 55fd7606d..53c902772 100644 --- a/tests/Integration/Repository/WorklogRepositoryTest.php +++ b/tests/Integration/Repository/WorklogRepositoryTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Integration\Repository; -use App\Entity\Worklog; use App\Model\Invoices\InvoiceEntryWorklogsFilterData; use App\Repository\InvoiceEntryRepository; use App\Repository\ProjectRepository; @@ -37,9 +36,6 @@ public function testFindByFilterDataBasic(): void $result = $this->repository->findByFilterData($project, $invoiceEntry, $filterData); $this->assertNotEmpty($result); - foreach ($result as $worklog) { - $this->assertInstanceOf(Worklog::class, $worklog); - } } public function testFindByFilterDataByWorker(): void @@ -177,9 +173,6 @@ public function testFindBillableWorklogsByWorkerAndDateRange(): void ); $this->assertNotEmpty($result); - foreach ($result as $worklog) { - $this->assertInstanceOf(Worklog::class, $worklog); - } } public function testFindBillableWorklogsByWorkerAndDateRangeFilteredByWorker(): void @@ -223,12 +216,6 @@ public function testGetWorklogsAttachedToInvoiceInDateRange(): void new \DateTime("$year-12-31") ); - $this->assertIsArray($result); - $this->assertArrayHasKey('total_count', $result); - $this->assertArrayHasKey('pages_count', $result); - $this->assertArrayHasKey('current_page', $result); - $this->assertArrayHasKey('page_size', $result); - $this->assertArrayHasKey('paginator', $result); $this->assertEquals(1, $result['current_page']); $this->assertEquals(50, $result['page_size']); $this->assertGreaterThan(0, $result['total_count']); diff --git a/tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php b/tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php index 36b4f5d7a..7cd0cb253 100644 --- a/tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php +++ b/tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Integration\Service; -use App\Model\Reports\BillableUnbilledHoursReportData; use App\Service\BillableUnbilledHoursReportService; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -20,7 +19,6 @@ public function testGetBillableUnbilledHoursReportAggregatesBillableUnbilledWork $report = $service->getBillableUnbilledHoursReport($year); - $this->assertInstanceOf(BillableUnbilledHoursReportData::class, $report); $this->assertGreaterThan(0, $report->totalHoursForAllProjects, 'Fixtures contain billable unbilled worklogs for the current year.'); $this->assertCount(1, $report->projectData, 'Report wraps projectData as a single-element collection of project arrays.'); $this->assertNotEmpty($report->projectTotals); diff --git a/tests/Integration/Service/ForecastReportServiceTest.php b/tests/Integration/Service/ForecastReportServiceTest.php index 3a5af1a98..7e15ccf9e 100644 --- a/tests/Integration/Service/ForecastReportServiceTest.php +++ b/tests/Integration/Service/ForecastReportServiceTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Integration\Service; -use App\Model\Reports\ForecastReportData; use App\Service\ForecastReportService; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -23,7 +22,6 @@ public function testGetForecastReportRunsAgainstFixturesAndReturnsConsistentTota $report = $service->getForecastReport($fromDate, $toDate); - $this->assertInstanceOf(ForecastReportData::class, $report); $this->assertGreaterThanOrEqual(0.0, $report->totalInvoiced); $this->assertGreaterThanOrEqual(0.0, $report->totalInvoicedAndRecorded); diff --git a/tests/Integration/Service/HourReportServiceTest.php b/tests/Integration/Service/HourReportServiceTest.php index 0d0111be8..8bdc64790 100644 --- a/tests/Integration/Service/HourReportServiceTest.php +++ b/tests/Integration/Service/HourReportServiceTest.php @@ -3,8 +3,6 @@ namespace App\Tests\Integration\Service; use App\Entity\Project; -use App\Model\Reports\HourReportData; -use App\Model\Reports\HourReportProjectTag; use App\Repository\ProjectRepository; use App\Service\HourReportService; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -27,8 +25,6 @@ public function testGetHourReportAggregatesWorklogsAcrossIssuesAndEpicTag(): voi $report = $service->getHourReport($project, null, null); - $this->assertInstanceOf(HourReportData::class, $report); - // Each project has 10 issues × 100 worklogs; worklog k contributes (k+1)*15 minutes. // Total = 10 issues × 900s × Σ(1..100) = 10 × 900 × 5050 = 45,450,000s = 12_625h. $this->assertEqualsWithDelta(12625.0, $report->projectTotalSpent, 0.001); @@ -48,12 +44,10 @@ public function testGetHourReportAggregatesWorklogsAcrossIssuesAndEpicTag(): voi $this->assertArrayHasKey('noTag', $tagsByLabel); $epicTag = $tagsByLabel['Epic 1']; - $this->assertInstanceOf(HourReportProjectTag::class, $epicTag); $this->assertCount(1, $epicTag->projectTickets); $this->assertEqualsWithDelta(1262.5, $epicTag->totalSpent, 0.001); $noTag = $tagsByLabel['noTag']; - $this->assertInstanceOf(HourReportProjectTag::class, $noTag); $this->assertCount(9, $noTag->projectTickets); } diff --git a/tests/Integration/Service/InvoicingRateReportServiceTest.php b/tests/Integration/Service/InvoicingRateReportServiceTest.php index 5352cd388..4d1f38303 100644 --- a/tests/Integration/Service/InvoicingRateReportServiceTest.php +++ b/tests/Integration/Service/InvoicingRateReportServiceTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Integration\Service; -use App\Model\Reports\InvoicingRateReportData; use App\Model\Reports\InvoicingRateReportViewModeEnum; use App\Model\Reports\InvoicingRateReportWorker; use App\Model\Reports\WorkloadReportPeriodTypeEnum as PeriodTypeEnum; @@ -27,8 +26,6 @@ public function testGetInvoicingRateReportProducesPeriodsForEveryIncludedWorker( InvoicingRateReportViewModeEnum::SUMMARY, ); - $this->assertInstanceOf(InvoicingRateReportData::class, $report); - // Fixtures create 10 workers all included in reports. $this->assertCount(10, $report->workers); @@ -45,7 +42,6 @@ public function testGetInvoicingRateReportProducesPeriodsForEveryIncludedWorker( /** @var InvoicingRateReportWorker $worker */ $worker = $report->workers->first(); - $this->assertInstanceOf(InvoicingRateReportWorker::class, $worker); $this->assertGreaterThanOrEqual(0.0, $worker->average); $this->assertLessThanOrEqual(100.0, $worker->average); $this->assertSame($report->period->count(), $worker->dataByPeriod->count()); diff --git a/tests/Integration/Service/WorkloadReportServiceTest.php b/tests/Integration/Service/WorkloadReportServiceTest.php index 153a5011b..21e2caca2 100644 --- a/tests/Integration/Service/WorkloadReportServiceTest.php +++ b/tests/Integration/Service/WorkloadReportServiceTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Integration\Service; -use App\Model\Reports\WorkloadReportData; use App\Model\Reports\WorkloadReportPeriodTypeEnum as PeriodTypeEnum; use App\Model\Reports\WorkloadReportViewModeEnum as ViewModeEnum; use App\Model\Reports\WorkloadReportWorker; @@ -27,8 +26,6 @@ public function testGetWorkloadReportProducesPeriodsForEveryIncludedWorker(): vo ViewModeEnum::WORKLOAD, ); - $this->assertInstanceOf(WorkloadReportData::class, $report); - // 10 fixture workers, all included in reports. $this->assertCount(10, $report->workers); @@ -40,7 +37,6 @@ public function testGetWorkloadReportProducesPeriodsForEveryIncludedWorker(): vo /** @var WorkloadReportWorker $worker */ $worker = $report->workers->first(); - $this->assertInstanceOf(WorkloadReportWorker::class, $worker); $this->assertSame($report->period->count(), $worker->loggedPercentage->count()); $this->assertGreaterThanOrEqual(0.0, $worker->average); } diff --git a/tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php b/tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php index 4adb47ca3..3b224d7c0 100644 --- a/tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php +++ b/tests/Unit/Service/BillableUnbilledHoursReportServiceTest.php @@ -5,7 +5,6 @@ use App\Entity\Issue; use App\Entity\Project; use App\Entity\Worklog; -use App\Model\Reports\BillableUnbilledHoursReportData; use App\Repository\WorkerRepository; use App\Repository\WorklogRepository; use App\Service\BillableUnbilledHoursReportService; @@ -55,8 +54,6 @@ public function testFullYearUsesYearDateRange(): void ->willReturn([]); $result = $this->service->getBillableUnbilledHoursReport(2024); - - $this->assertInstanceOf(BillableUnbilledHoursReportData::class, $result); } public function testQuarterUsesQuarterDateRange(): void @@ -76,8 +73,6 @@ public function testQuarterUsesQuarterDateRange(): void ->willReturn([]); $result = $this->service->getBillableUnbilledHoursReport(2024, 2); - - $this->assertInstanceOf(BillableUnbilledHoursReportData::class, $result); } public function testAggregatesPerProject(): void diff --git a/tests/Unit/Service/DashboardServiceTest.php b/tests/Unit/Service/DashboardServiceTest.php index 69d886f5e..96d74e003 100644 --- a/tests/Unit/Service/DashboardServiceTest.php +++ b/tests/Unit/Service/DashboardServiceTest.php @@ -149,6 +149,5 @@ public function testGetUserDashboardYearStatusCalculation(): void $this->assertInstanceOf(DashboardData::class, $result); // yearStatus = (totalTimeSpent - yearNormToDate) / 3600 // The exact value depends on how many weekdays in 2023, but it should be a number - $this->assertIsFloat($result->workHours); } } diff --git a/tests/Unit/Service/ForecastReportServiceTest.php b/tests/Unit/Service/ForecastReportServiceTest.php index becf7cbbc..ebf1449c2 100644 --- a/tests/Unit/Service/ForecastReportServiceTest.php +++ b/tests/Unit/Service/ForecastReportServiceTest.php @@ -6,7 +6,6 @@ use App\Entity\Project; use App\Entity\Worker; use App\Entity\Worklog; -use App\Model\Reports\ForecastReportData; use App\Repository\WorkerRepository; use App\Repository\WorklogRepository; use App\Service\ForecastReportService; @@ -65,7 +64,6 @@ public function testEmptyWorklogsReturnsEmptyReport(): void new \DateTime('2024-01-31'), ); - $this->assertInstanceOf(ForecastReportData::class, $result); $this->assertEqualsWithDelta(0.0, $result->totalInvoiced, 0.001); $this->assertEqualsWithDelta(0.0, $result->totalInvoicedAndRecorded, 0.001); } diff --git a/tests/Unit/Service/HourReportServiceTest.php b/tests/Unit/Service/HourReportServiceTest.php index 0f51642bf..4cf57e4c7 100644 --- a/tests/Unit/Service/HourReportServiceTest.php +++ b/tests/Unit/Service/HourReportServiceTest.php @@ -7,7 +7,6 @@ use App\Entity\Project; use App\Entity\Version; use App\Entity\Worklog; -use App\Model\Reports\HourReportData; use App\Repository\IssueRepository; use App\Repository\WorklogRepository; use App\Service\HourReportService; @@ -79,8 +78,6 @@ public function testGetHourReportWithVersionFilter(): void new \DateTime('2024-01-31'), $version, ); - - $this->assertInstanceOf(HourReportData::class, $result); } public function testGetHourReportWithoutVersionUsesProjectFilter(): void @@ -100,8 +97,6 @@ public function testGetHourReportWithoutVersionUsesProjectFilter(): void new \DateTime('2024-01-01'), new \DateTime('2024-01-31'), ); - - $this->assertInstanceOf(HourReportData::class, $result); } public function testGetHourReportSkipsIssuesWithNoWorklogsInRange(): void diff --git a/tests/Unit/Service/InvoicingRateReportServiceTest.php b/tests/Unit/Service/InvoicingRateReportServiceTest.php index 6e7139f56..53744da50 100644 --- a/tests/Unit/Service/InvoicingRateReportServiceTest.php +++ b/tests/Unit/Service/InvoicingRateReportServiceTest.php @@ -3,7 +3,6 @@ namespace App\Tests\Unit\Service; use App\Entity\Worker; -use App\Model\Reports\InvoicingRateReportData; use App\Model\Reports\WorkloadReportPeriodTypeEnum as PeriodTypeEnum; use App\Repository\WorkerRepository; use App\Repository\WorklogRepository; @@ -39,7 +38,6 @@ public function testMonthPeriodReturns12Periods(): void $result = $this->service->getInvoicingRateReport(2024, PeriodTypeEnum::MONTH); - $this->assertInstanceOf(InvoicingRateReportData::class, $result); $this->assertCount(12, $result->period); } @@ -97,7 +95,6 @@ public function testPercentageCalculation(): void $result = $this->service->getInvoicingRateReport(2024, PeriodTypeEnum::MONTH); - $this->assertInstanceOf(InvoicingRateReportData::class, $result); $this->assertCount(1, $result->workers); // With 0 logged hours, average should be 0 diff --git a/tests/Unit/Service/PlanningServiceTest.php b/tests/Unit/Service/PlanningServiceTest.php index 042ee0c54..5da4d78fb 100644 --- a/tests/Unit/Service/PlanningServiceTest.php +++ b/tests/Unit/Service/PlanningServiceTest.php @@ -6,7 +6,6 @@ use App\Entity\Project as ProjectEntity; use App\Entity\Worker; use App\Enum\IssueStatusEnum; -use App\Model\Planning\PlanningData; use App\Repository\IssueRepository; use App\Repository\ProjectRepository; use App\Repository\WorkerRepository; @@ -54,7 +53,6 @@ public function testGetPlanningDataBuildsWeeks(): void $result = $this->service->getPlanningData(2024, null); - $this->assertInstanceOf(PlanningData::class, $result); // With holidayPlanning=false (default), weeks are grouped into support+sprint periods // 52 weeks / (1 support + 3 sprint) = 13 groups * 2 entries per group = 26 $this->assertCount(26, $result->weeks); From 7310f167b107b9eaac89574b410ff69494240432 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Wed, 20 May 2026 11:30:56 +0200 Subject: [PATCH 19/23] 7273: Added inline asserts to tests --- CHANGELOG.md | 10 +- phpstan-baseline.neon | 264 ++---------------- .../Controller/AbstractControllerTestCase.php | 4 +- .../Controller/HourReportFilterTest.php | 4 +- .../Controller/InvoiceEntryFlowTest.php | 24 +- .../Controller/InvoiceFlowTest.php | 12 +- .../Controller/InvoiceFullFlowTest.php | 25 +- .../Controller/ProjectBillingFlowTest.php | 8 +- .../Controller/ProjectBillingFullFlowTest.php | 9 +- .../Repository/AccountRepositoryTest.php | 4 +- .../Repository/ClientRepositoryTest.php | 4 +- .../CybersecurityAgreementRepositoryTest.php | 4 +- .../Repository/InvoiceRepositoryTest.php | 4 +- .../Repository/IssueRepositoryTest.php | 14 +- .../Repository/ProductRepositoryTest.php | 5 +- .../ProjectBillingRepositoryTest.php | 4 +- .../Repository/ProjectRepositoryTest.php | 9 +- .../ServiceAgreementRepositoryTest.php | 4 +- .../Repository/SubscriptionRepositoryTest.php | 4 +- .../Repository/WorkerGroupRepositoryTest.php | 4 +- .../Repository/WorkerRepositoryTest.php | 8 +- .../Repository/WorklogRepositoryTest.php | 17 +- ...BillableUnbilledHoursReportServiceTest.php | 4 +- .../Service/ForecastReportServiceTest.php | 2 +- .../Service/HourReportServiceTest.php | 8 +- .../InvoicingRateReportServiceTest.php | 2 +- .../Service/LeantimeApiServiceTest.php | 24 +- .../Service/ProjectBillingServiceTest.php | 10 +- .../Service/WorkloadReportServiceTest.php | 2 +- 29 files changed, 187 insertions(+), 310 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd39df7b1..fe886a813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,14 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [PR-308](https://github.com/itk-dev/economics/pull/308) * PHPStan baseline reduced from 530 → 0 in `src/` and the pre-merge `tests/`, then bounced to 444 when develop's new integration/unit test suites - arrived, and is now at 141. The first pass cleaned `src/` to zero + arrived, and is now at 91. The first pass cleaned `src/` to zero suppressions via PHPDoc/typehint annotations, generic-class shapes, dropped scaffold `@method find*` docblocks on 17 repositories, and null-guards. Post-merge passes intersected PHPUnit mock properties - with `MockObject` across 16 test files (−257 entries), and dropped + with `MockObject` across 16 test files (−257 entries); dropped redundant `assertInstanceOf` / `assertIsArray` / `assertIsFloat` / `assertArrayHasKey` calls whose target type was already statically - proved across 24 test files (−46 entries). + proved across 24 test files (−46 entries); and narrowed + `Container::get()` results in integration tests with inline + `\assert($x instanceof T)` (and removed redundant `/** @var */` + PHPDoc lines that drift silently when the underlying types change) + across 28 test files (−50 entries). Items below are behavior- or schema-affecting and worth calling out. * Migration `Version20260517131038`: made 18 columns nullable on synced entities (`issue`, `project`, `version`, `worklog`) and three DateTime diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 53573f7d1..219e389cc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,11 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/AbstractControllerTestCase.php - - message: '#^Method App\\Tests\\Integration\\Controller\\AbstractControllerTestCase\:\:assertDeniedFor\(\) has parameter \$roles with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -36,42 +30,6 @@ parameters: count: 1 path: tests/Integration/Controller/AbstractControllerTestCase.php - - - message: '#^Call to an undefined method object\:\:getIncluded\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/HourReportFilterTest.php - - - - message: '#^Call to an undefined method object\:\:clear\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - - message: '#^Call to an undefined method object\:\:find\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - - message: '#^Call to an undefined method object\:\:flush\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - - message: '#^Call to an undefined method object\:\:getIncluded\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - - message: '#^Call to an undefined method object\:\:persist\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - message: '#^Parameter \#1 \$id of method App\\Tests\\Integration\\Controller\\InvoiceEntryFlowTest\:\:reloadEntry\(\) expects int, int\|null given\.$#' identifier: argument.type @@ -84,18 +42,6 @@ parameters: count: 2 path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Integration/Controller/InvoiceFlowTest.php - - - - message: '#^Call to an undefined method object\:\:getIncluded\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/InvoiceFlowTest.php - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' identifier: method.nonObject @@ -108,36 +54,6 @@ parameters: count: 1 path: tests/Integration/Controller/InvoiceFlowTest.php - - - message: '#^Call to an undefined method object\:\:clear\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Call to an undefined method object\:\:find\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Call to an undefined method object\:\:findBy\(\)\.$#' - identifier: method.notFound - count: 2 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Call to an undefined method object\:\:getIncluded\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - message: '#^Cannot call method getAmount\(\) on App\\Entity\\InvoiceEntry\|null\.$#' identifier: method.nonObject @@ -198,24 +114,18 @@ parameters: count: 1 path: tests/Integration/Controller/InvoiceFullFlowTest.php + - + message: '#^Method App\\Tests\\Integration\\Controller\\InvoiceFullFlowTest\:\:submitNewEntry\(\) should return int but returns int\|null\.$#' + identifier: return.type + count: 1 + path: tests/Integration/Controller/InvoiceFullFlowTest.php + - message: '#^Offset 1 might not exist on array\{\}\|array\{non\-falsy\-string, numeric\-string\}\.$#' identifier: offsetAccess.notFound count: 2 path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/ProjectBillingFlowTest.php - - - - message: '#^Call to an undefined method object\:\:getIncluded\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/ProjectBillingFlowTest.php - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' identifier: method.nonObject @@ -228,24 +138,6 @@ parameters: count: 1 path: tests/Integration/Controller/ProjectBillingFlowTest.php - - - message: '#^Call to an undefined method object\:\:clear\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Call to an undefined method object\:\:find\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - message: '#^Cannot call method getDescription\(\) on App\\Entity\\ProjectBilling\|null\.$#' identifier: method.nonObject @@ -288,30 +180,12 @@ parameters: count: 1 path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - message: '#^Property App\\Tests\\Integration\\Repository\\AccountRepositoryTest\:\:\$repository \(App\\Repository\\AccountRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/AccountRepositoryTest.php - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' identifier: argument.type count: 2 path: tests/Integration/Repository/ClientRepositoryTest.php - - - message: '#^Property App\\Tests\\Integration\\Repository\\ClientRepositoryTest\:\:\$repository \(App\\Repository\\ClientRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/ClientRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\CybersecurityAgreementRepositoryTest\:\:\$repository \(App\\Repository\\CybersecurityAgreementRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' identifier: argument.type @@ -319,21 +193,15 @@ parameters: path: tests/Integration/Repository/InvoiceRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\InvoiceRepositoryTest\:\:\$repository \(App\\Repository\\InvoiceRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/InvoiceRepositoryTest.php - - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 2 + message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' + identifier: method.nonObject + count: 4 path: tests/Integration/Repository/IssueRepositoryTest.php - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' + message: '#^Cannot call method getId\(\) on App\\Entity\\Version\|null\.$#' identifier: method.nonObject - count: 4 + count: 1 path: tests/Integration/Repository/IssueRepositoryTest.php - @@ -349,20 +217,20 @@ parameters: path: tests/Integration/Repository/IssueRepositoryTest.php - - message: '#^Parameter \#2 \$haystack of method App\\Tests\\Integration\\Repository\\IssueRepositoryTest\:\:assertStringContains\(\) expects string, string\|null given\.$#' + message: '#^Parameter \#1 \$version of method App\\Repository\\IssueRepository\:\:issuesContainingVersion\(\) expects App\\Entity\\Version, App\\Entity\\Version\|null given\.$#' identifier: argument.type count: 1 path: tests/Integration/Repository/IssueRepositoryTest.php - - message: '#^Parameter \#4 \$projects of method App\\Repository\\IssueRepository\:\:findIssuesInDateRange\(\) expects array\\|null, array\ given\.$#' + message: '#^Parameter \#2 \$haystack of method App\\Tests\\Integration\\Repository\\IssueRepositoryTest\:\:assertStringContains\(\) expects string, string\|null given\.$#' identifier: argument.type count: 1 path: tests/Integration/Repository/IssueRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\IssueRepositoryTest\:\:\$entityManager \(Doctrine\\ORM\\EntityManagerInterface\) does not accept object\.$#' - identifier: assign.propertyType + message: '#^Parameter \#4 \$projects of method App\\Repository\\IssueRepository\:\:findIssuesInDateRange\(\) expects array\\|null, array\ given\.$#' + identifier: argument.type count: 1 path: tests/Integration/Repository/IssueRepositoryTest.php @@ -372,28 +240,10 @@ parameters: count: 1 path: tests/Integration/Repository/IssueRepositoryTest.php - - - message: '#^Property App\\Tests\\Integration\\Repository\\IssueRepositoryTest\:\:\$projectRepository \(App\\Repository\\ProjectRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/IssueRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\IssueRepositoryTest\:\:\$repository \(App\\Repository\\IssueRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/IssueRepositoryTest.php - - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Repository/ProductRepositoryTest.php - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' identifier: method.nonObject - count: 1 + count: 2 path: tests/Integration/Repository/ProductRepositoryTest.php - @@ -402,36 +252,12 @@ parameters: count: 1 path: tests/Integration/Repository/ProductRepositoryTest.php - - - message: '#^Property App\\Tests\\Integration\\Repository\\ProductRepositoryTest\:\:\$repository \(App\\Repository\\ProductRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/ProductRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\ProjectBillingRepositoryTest\:\:\$repository \(App\\Repository\\ProjectBillingRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/ProjectBillingRepositoryTest.php - - - - message: '#^Call to an undefined method object\:\:findAll\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' identifier: argument.type count: 2 path: tests/Integration/Repository/ProjectRepositoryTest.php - - - message: '#^Property App\\Tests\\Integration\\Repository\\ProjectRepositoryTest\:\:\$entityManager \(Doctrine\\ORM\\EntityManagerInterface\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\ProjectRepositoryTest\:\:\$entityManager is never read, only written\.$#' identifier: property.onlyWritten @@ -439,45 +265,9 @@ parameters: path: tests/Integration/Repository/ProjectRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\ProjectRepositoryTest\:\:\$repository \(App\\Repository\\ProjectRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\ServiceAgreementRepositoryTest\:\:\$repository \(App\\Repository\\ServiceAgreementRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/ServiceAgreementRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\SubscriptionRepositoryTest\:\:\$repository \(App\\Repository\\SubscriptionRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/SubscriptionRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\WorkerGroupRepositoryTest\:\:\$repository \(App\\Repository\\WorkerGroupRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/WorkerGroupRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\WorkerRepositoryTest\:\:\$entityManager \(Doctrine\\ORM\\EntityManagerInterface\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/WorkerRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\WorkerRepositoryTest\:\:\$repository \(App\\Repository\\WorkerRepository\) does not accept object\.$#' - identifier: assign.propertyType + message: '#^Cannot call method getId\(\) on App\\Entity\\InvoiceEntry\|null\.$#' + identifier: method.nonObject count: 1 - path: tests/Integration/Repository/WorkerRepositoryTest.php - - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 5 path: tests/Integration/Repository/WorklogRepositoryTest.php - @@ -493,9 +283,9 @@ parameters: path: tests/Integration/Repository/WorklogRepositoryTest.php - - message: '#^Property App\\Tests\\Integration\\Repository\\WorklogRepositoryTest\:\:\$entityManager \(Doctrine\\ORM\\EntityManagerInterface\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 + message: '#^Parameter \#2 \$invoiceEntry of method App\\Repository\\WorklogRepository\:\:findByFilterData\(\) expects App\\Entity\\InvoiceEntry, App\\Entity\\InvoiceEntry\|null given\.$#' + identifier: argument.type + count: 5 path: tests/Integration/Repository/WorklogRepositoryTest.php - @@ -504,18 +294,6 @@ parameters: count: 1 path: tests/Integration/Repository/WorklogRepositoryTest.php - - - message: '#^Property App\\Tests\\Integration\\Repository\\WorklogRepositoryTest\:\:\$projectRepository \(App\\Repository\\ProjectRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\WorklogRepositoryTest\:\:\$repository \(App\\Repository\\WorklogRepository\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - message: '#^You should use assertCount\(\$expectedCount, \$variable\) instead of assertSame\(\$expectedCount, \$variable\-\>count\(\)\)\.$#' identifier: phpunit.assertCount diff --git a/tests/Integration/Controller/AbstractControllerTestCase.php b/tests/Integration/Controller/AbstractControllerTestCase.php index af006b1bf..7f9c49175 100644 --- a/tests/Integration/Controller/AbstractControllerTestCase.php +++ b/tests/Integration/Controller/AbstractControllerTestCase.php @@ -24,7 +24,9 @@ protected function getUserWithRole(string $role): User $email = self::ROLE_USER_FIXTURES[$role] ?? throw new \InvalidArgumentException(sprintf('No fixture user for role %s.', $role)); - $user = static::getContainer()->get(UserRepository::class)->findOneBy(['email' => $email]); + $userRepository = static::getContainer()->get(UserRepository::class); + \assert($userRepository instanceof UserRepository); + $user = $userRepository->findOneBy(['email' => $email]); if (null === $user) { throw new \RuntimeException(sprintf('Fixture user %s not found; run `task fixtures`.', $email)); } diff --git a/tests/Integration/Controller/HourReportFilterTest.php b/tests/Integration/Controller/HourReportFilterTest.php index 96ad9dfae..ea52bf8fb 100644 --- a/tests/Integration/Controller/HourReportFilterTest.php +++ b/tests/Integration/Controller/HourReportFilterTest.php @@ -8,7 +8,9 @@ class HourReportFilterTest extends AbstractControllerTestCase { public function testFilterSubmissionRendersReportForSelectedProject(): void { - $project = static::getContainer()->get(ProjectRepository::class)->getIncluded() + $projectRepository = static::getContainer()->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); + $project = $projectRepository->getIncluded() ->setMaxResults(1)->getQuery()->getOneOrNullResult(); $this->assertNotNull($project, 'Expected an included project from fixtures.'); diff --git a/tests/Integration/Controller/InvoiceEntryFlowTest.php b/tests/Integration/Controller/InvoiceEntryFlowTest.php index 5c2a61960..27253528a 100644 --- a/tests/Integration/Controller/InvoiceEntryFlowTest.php +++ b/tests/Integration/Controller/InvoiceEntryFlowTest.php @@ -152,8 +152,11 @@ private function setupInvoiceWithManualEntry(bool $recorded = false): array { $container = static::getContainer(); $em = $container->get(EntityManagerInterface::class); + \assert($em instanceof EntityManagerInterface); - $project = $container->get(ProjectRepository::class)->getIncluded() + $projectRepository = $container->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); + $project = $projectRepository->getIncluded() ->setMaxResults(1)->getQuery()->getOneOrNullResult(); $this->assertNotNull($project, 'Expected an included project from fixtures.'); @@ -181,8 +184,12 @@ private function setupInvoiceWithManualEntry(bool $recorded = false): array $entryId = $entry->getId(); $em->clear(); - $invoice = $container->get(InvoiceRepository::class)->find($invoiceId); - $entry = $container->get(InvoiceEntryRepository::class)->find($entryId); + $invoiceRepository = $container->get(InvoiceRepository::class); + \assert($invoiceRepository instanceof InvoiceRepository); + $invoice = $invoiceRepository->find($invoiceId); + $invoiceEntryRepository = $container->get(InvoiceEntryRepository::class); + \assert($invoiceEntryRepository instanceof InvoiceEntryRepository); + $entry = $invoiceEntryRepository->find($entryId); $this->assertInstanceOf(Invoice::class, $invoice); $this->assertInstanceOf(InvoiceEntry::class, $entry); @@ -192,7 +199,10 @@ private function setupInvoiceWithManualEntry(bool $recorded = false): array private function markInvoiceRecorded(int $invoiceId): void { $em = static::getContainer()->get(EntityManagerInterface::class); - $invoice = static::getContainer()->get(InvoiceRepository::class)->find($invoiceId); + \assert($em instanceof EntityManagerInterface); + $invoiceRepository = static::getContainer()->get(InvoiceRepository::class); + \assert($invoiceRepository instanceof InvoiceRepository); + $invoice = $invoiceRepository->find($invoiceId); $this->assertInstanceOf(Invoice::class, $invoice); $invoice->setRecorded(true); $invoice->setRecordedDate(new \DateTime()); @@ -203,8 +213,12 @@ private function markInvoiceRecorded(int $invoiceId): void private function reloadEntry(int $id): ?InvoiceEntry { $em = static::getContainer()->get(EntityManagerInterface::class); + \assert($em instanceof EntityManagerInterface); $em->clear(); - return static::getContainer()->get(InvoiceEntryRepository::class)->find($id); + $invoiceEntryRepository = static::getContainer()->get(InvoiceEntryRepository::class); + \assert($invoiceEntryRepository instanceof InvoiceEntryRepository); + + return $invoiceEntryRepository->find($id); } } diff --git a/tests/Integration/Controller/InvoiceFlowTest.php b/tests/Integration/Controller/InvoiceFlowTest.php index 144b239eb..43b77bb38 100644 --- a/tests/Integration/Controller/InvoiceFlowTest.php +++ b/tests/Integration/Controller/InvoiceFlowTest.php @@ -14,7 +14,9 @@ public function testCreateInvoicePersistsAndRedirectsToEdit(): void $crawler = $client->request('GET', '/admin/invoices/new'); $this->assertResponseIsSuccessful(); - $project = static::getContainer()->get(ProjectRepository::class)->getIncluded() + $projectRepository = static::getContainer()->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); + $project = $projectRepository->getIncluded() ->setMaxResults(1)->getQuery()->getOneOrNullResult(); $this->assertNotNull($project, 'Expected an included project from fixtures.'); @@ -27,7 +29,9 @@ public function testCreateInvoicePersistsAndRedirectsToEdit(): void $this->assertResponseRedirects(); $this->assertMatchesRegularExpression('#/admin/invoices/\d+/edit$#', $client->getResponse()->headers->get('Location')); - $created = static::getContainer()->get(InvoiceRepository::class)->findOneBy(['name' => $name]); + $invoiceRepository = static::getContainer()->get(InvoiceRepository::class); + \assert($invoiceRepository instanceof InvoiceRepository); + $created = $invoiceRepository->findOneBy(['name' => $name]); $this->assertInstanceOf(Invoice::class, $created); $this->assertSame($project->getId(), $created->getProject()->getId()); $this->assertFalse($created->isRecorded()); @@ -35,7 +39,9 @@ public function testCreateInvoicePersistsAndRedirectsToEdit(): void public function testEditExistingInvoiceRendersForm(): void { - $invoice = static::getContainer()->get(InvoiceRepository::class)->findOneBy([]); + $invoiceRepository = static::getContainer()->get(InvoiceRepository::class); + \assert($invoiceRepository instanceof InvoiceRepository); + $invoice = $invoiceRepository->findOneBy([]); $this->assertNotNull($invoice, 'Expected at least one invoice from fixtures.'); $client = $this->createClientLoggedInAs(['ROLE_INVOICE']); diff --git a/tests/Integration/Controller/InvoiceFullFlowTest.php b/tests/Integration/Controller/InvoiceFullFlowTest.php index 80e744da6..97442848f 100644 --- a/tests/Integration/Controller/InvoiceFullFlowTest.php +++ b/tests/Integration/Controller/InvoiceFullFlowTest.php @@ -24,12 +24,15 @@ public function testFullInvoiceLifecycle(): void $client = $this->createClientLoggedInAs(['ROLE_INVOICE']); $container = static::getContainer(); - $project = $container->get(ProjectRepository::class)->getIncluded() + $projectRepository = $container->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); + $project = $projectRepository->getIncluded() ->setMaxResults(1)->getQuery()->getOneOrNullResult(); $this->assertNotNull($project, 'Expected an included project from fixtures.'); - $internalClient = $container->get(ClientRepository::class) - ->findOneBy(['type' => ClientTypeEnum::INTERNAL]); + $clientRepository = $container->get(ClientRepository::class); + \assert($clientRepository instanceof ClientRepository); + $internalClient = $clientRepository->findOneBy(['type' => ClientTypeEnum::INTERNAL]); $this->assertInstanceOf(Client::class, $internalClient, 'Expected an internal client fixture.'); // 1. Create invoice. @@ -147,6 +150,7 @@ public function testFullInvoiceLifecycle(): void // 6. Attach worklogs to the WORKLOG entry. $worklogRepository = static::getContainer()->get(WorklogRepository::class); + \assert($worklogRepository instanceof WorklogRepository); $unbilled = $worklogRepository->findBy( ['project' => $project, 'isBilled' => false], ['id' => 'ASC'], @@ -212,8 +216,10 @@ public function testFullInvoiceLifecycle(): void // Worklogs attached to the WORKLOG entry should be marked billed. $selectedIds = array_map(static fn (Worklog $wl) => $wl->getId(), $selected); $em = static::getContainer()->get(EntityManagerInterface::class); + \assert($em instanceof EntityManagerInterface); $em->clear(); $worklogRepository = static::getContainer()->get(WorklogRepository::class); + \assert($worklogRepository instanceof WorklogRepository); foreach ($selectedIds as $wlId) { $wl = $worklogRepository->find($wlId); $this->assertNotNull($wl); @@ -273,6 +279,7 @@ private function submitNewEntry( // MANUAL redirects back to invoice edit — fetch the most recently created entry for this invoice. $repository = static::getContainer()->get(InvoiceEntryRepository::class); + \assert($repository instanceof InvoiceEntryRepository); $latest = $repository->findBy([], ['id' => 'DESC'], 1); $this->assertNotEmpty($latest); @@ -282,16 +289,24 @@ private function submitNewEntry( private function reloadInvoice(int $id): ?Invoice { $em = static::getContainer()->get(EntityManagerInterface::class); + \assert($em instanceof EntityManagerInterface); $em->clear(); - return static::getContainer()->get(InvoiceRepository::class)->find($id); + $invoiceRepository = static::getContainer()->get(InvoiceRepository::class); + \assert($invoiceRepository instanceof InvoiceRepository); + + return $invoiceRepository->find($id); } private function reloadInvoiceEntry(int $id): ?InvoiceEntry { $em = static::getContainer()->get(EntityManagerInterface::class); + \assert($em instanceof EntityManagerInterface); $em->clear(); - return static::getContainer()->get(InvoiceEntryRepository::class)->find($id); + $invoiceEntryRepository = static::getContainer()->get(InvoiceEntryRepository::class); + \assert($invoiceEntryRepository instanceof InvoiceEntryRepository); + + return $invoiceEntryRepository->find($id); } } diff --git a/tests/Integration/Controller/ProjectBillingFlowTest.php b/tests/Integration/Controller/ProjectBillingFlowTest.php index 8f1181015..45df55302 100644 --- a/tests/Integration/Controller/ProjectBillingFlowTest.php +++ b/tests/Integration/Controller/ProjectBillingFlowTest.php @@ -14,7 +14,9 @@ public function testCreateProjectBillingPersistsAndRedirectsToEdit(): void $crawler = $client->request('GET', '/admin/project-billing/new'); $this->assertResponseIsSuccessful(); - $project = static::getContainer()->get(ProjectRepository::class)->getIncluded() + $projectRepository = static::getContainer()->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); + $project = $projectRepository->getIncluded() ->setMaxResults(1)->getQuery()->getOneOrNullResult(); $this->assertNotNull($project, 'Expected an included project from fixtures.'); @@ -29,7 +31,9 @@ public function testCreateProjectBillingPersistsAndRedirectsToEdit(): void $this->assertResponseRedirects(); $this->assertMatchesRegularExpression('#/admin/project-billing/\d+/edit$#', $client->getResponse()->headers->get('Location')); - $created = static::getContainer()->get(ProjectBillingRepository::class)->findOneBy(['name' => $name]); + $projectBillingRepository = static::getContainer()->get(ProjectBillingRepository::class); + \assert($projectBillingRepository instanceof ProjectBillingRepository); + $created = $projectBillingRepository->findOneBy(['name' => $name]); $this->assertInstanceOf(ProjectBilling::class, $created); $this->assertSame($project->getId(), $created->getProject()->getId()); } diff --git a/tests/Integration/Controller/ProjectBillingFullFlowTest.php b/tests/Integration/Controller/ProjectBillingFullFlowTest.php index e78144c84..fe53753ea 100644 --- a/tests/Integration/Controller/ProjectBillingFullFlowTest.php +++ b/tests/Integration/Controller/ProjectBillingFullFlowTest.php @@ -20,7 +20,9 @@ public function testFullProjectBillingLifecycle(): void // versions `PB-0-0`/`PB-0-1` (matching the data-provider-0 clients) and have // DONE-status issues with worklogs, but ProjectBillingServiceTest depends on // `project-0-0`'s worklogs staying unbilled, so we must not record against it. - $project = $container->get(ProjectRepository::class)->findOneBy(['name' => 'project-0-2']); + $projectRepository = $container->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); + $project = $projectRepository->findOneBy(['name' => 'project-0-2']); $this->assertNotNull($project, 'Expected fixture project `project-0-2`.'); $periodStart = '2020-01-01'; @@ -161,9 +163,12 @@ private function submitRecordForm(KernelBrowser $client, int $projectBillingId, private function reload(int $id): ?ProjectBilling { $em = static::getContainer()->get(EntityManagerInterface::class); + \assert($em instanceof EntityManagerInterface); $em->clear(); - $pb = static::getContainer()->get(ProjectBillingRepository::class)->find($id); + $projectBillingRepository = static::getContainer()->get(ProjectBillingRepository::class); + \assert($projectBillingRepository instanceof ProjectBillingRepository); + $pb = $projectBillingRepository->find($id); if (null !== $pb) { $pb->getInvoices()->count(); foreach ($pb->getInvoices() as $invoice) { diff --git a/tests/Integration/Repository/AccountRepositoryTest.php b/tests/Integration/Repository/AccountRepositoryTest.php index 63129e24f..15bba9340 100644 --- a/tests/Integration/Repository/AccountRepositoryTest.php +++ b/tests/Integration/Repository/AccountRepositoryTest.php @@ -13,7 +13,9 @@ class AccountRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(AccountRepository::class); + $repository = self::getContainer()->get(AccountRepository::class); + \assert($repository instanceof AccountRepository); + $this->repository = $repository; } public function testGetAllChoices(): void diff --git a/tests/Integration/Repository/ClientRepositoryTest.php b/tests/Integration/Repository/ClientRepositoryTest.php index cdb8e7a47..b5fe0fb9e 100644 --- a/tests/Integration/Repository/ClientRepositoryTest.php +++ b/tests/Integration/Repository/ClientRepositoryTest.php @@ -13,7 +13,9 @@ class ClientRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(ClientRepository::class); + $repository = self::getContainer()->get(ClientRepository::class); + \assert($repository instanceof ClientRepository); + $this->repository = $repository; } public function testGetFilteredPaginationNoFilter(): void diff --git a/tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php b/tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php index d467f2d1e..c87d9f1e4 100644 --- a/tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php +++ b/tests/Integration/Repository/CybersecurityAgreementRepositoryTest.php @@ -12,7 +12,9 @@ class CybersecurityAgreementRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(CybersecurityAgreementRepository::class); + $repository = self::getContainer()->get(CybersecurityAgreementRepository::class); + \assert($repository instanceof CybersecurityAgreementRepository); + $this->repository = $repository; } public function testFindAllIndexed(): void diff --git a/tests/Integration/Repository/InvoiceRepositoryTest.php b/tests/Integration/Repository/InvoiceRepositoryTest.php index ad7702f30..0d2256358 100644 --- a/tests/Integration/Repository/InvoiceRepositoryTest.php +++ b/tests/Integration/Repository/InvoiceRepositoryTest.php @@ -13,7 +13,9 @@ class InvoiceRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(InvoiceRepository::class); + $repository = self::getContainer()->get(InvoiceRepository::class); + \assert($repository instanceof InvoiceRepository); + $this->repository = $repository; } public function testGetByRecordedDateBetween(): void diff --git a/tests/Integration/Repository/IssueRepositoryTest.php b/tests/Integration/Repository/IssueRepositoryTest.php index 81b30b22a..cdebd5412 100644 --- a/tests/Integration/Repository/IssueRepositoryTest.php +++ b/tests/Integration/Repository/IssueRepositoryTest.php @@ -24,9 +24,15 @@ protected function setUp(): void { self::bootKernel(); $container = self::getContainer(); - $this->entityManager = $container->get(EntityManagerInterface::class); - $this->repository = $container->get(IssueRepository::class); - $this->projectRepository = $container->get(ProjectRepository::class); + $entityManager = $container->get(EntityManagerInterface::class); + \assert($entityManager instanceof EntityManagerInterface); + $this->entityManager = $entityManager; + $repository = $container->get(IssueRepository::class); + \assert($repository instanceof IssueRepository); + $this->repository = $repository; + $projectRepository = $container->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); + $this->projectRepository = $projectRepository; } public function testGetFilteredPaginationNoFilter(): void @@ -101,6 +107,7 @@ public function testGetClosedIssuesFromIntervalNoResults(): void public function testIssuesContainingVersion(): void { $versionRepository = self::getContainer()->get(VersionRepository::class); + \assert($versionRepository instanceof VersionRepository); $version = $versionRepository->findOneBy([], ['id' => 'ASC']); $result = $this->repository->issuesContainingVersion($version); @@ -126,6 +133,7 @@ public function testFindIssuesInDateRange(): void public function testFindIssuesInDateRangeWithWorkerGroup(): void { $workerGroupRepo = self::getContainer()->get(WorkerGroupRepository::class); + \assert($workerGroupRepo instanceof WorkerGroupRepository); $group = $workerGroupRepo->findOneBy(['name' => 'Group Alpha']); $this->assertInstanceOf(WorkerGroup::class, $group); diff --git a/tests/Integration/Repository/ProductRepositoryTest.php b/tests/Integration/Repository/ProductRepositoryTest.php index e7408594d..0113e681e 100644 --- a/tests/Integration/Repository/ProductRepositoryTest.php +++ b/tests/Integration/Repository/ProductRepositoryTest.php @@ -14,7 +14,9 @@ class ProductRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(ProductRepository::class); + $repository = self::getContainer()->get(ProductRepository::class); + \assert($repository instanceof ProductRepository); + $this->repository = $repository; } public function testGetFilteredPaginationNoFilter(): void @@ -40,6 +42,7 @@ public function testGetFilteredPaginationByName(): void public function testGetFilteredPaginationByProject(): void { $projectRepo = self::getContainer()->get(ProjectRepository::class); + \assert($projectRepo instanceof ProjectRepository); $project = $projectRepo->findOneBy(['name' => 'project-0-0']); $filterData = new ProductFilterData(); diff --git a/tests/Integration/Repository/ProjectBillingRepositoryTest.php b/tests/Integration/Repository/ProjectBillingRepositoryTest.php index f9ab1ff73..ae840492e 100644 --- a/tests/Integration/Repository/ProjectBillingRepositoryTest.php +++ b/tests/Integration/Repository/ProjectBillingRepositoryTest.php @@ -13,7 +13,9 @@ class ProjectBillingRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(ProjectBillingRepository::class); + $repository = self::getContainer()->get(ProjectBillingRepository::class); + \assert($repository instanceof ProjectBillingRepository); + $this->repository = $repository; } public function testGetFilteredPaginationRecorded(): void diff --git a/tests/Integration/Repository/ProjectRepositoryTest.php b/tests/Integration/Repository/ProjectRepositoryTest.php index 8b8b1fb74..393b7058e 100644 --- a/tests/Integration/Repository/ProjectRepositoryTest.php +++ b/tests/Integration/Repository/ProjectRepositoryTest.php @@ -18,8 +18,12 @@ protected function setUp(): void { self::bootKernel(); $container = self::getContainer(); - $this->entityManager = $container->get(EntityManagerInterface::class); - $this->repository = $container->get(ProjectRepository::class); + $entityManager = $container->get(EntityManagerInterface::class); + \assert($entityManager instanceof EntityManagerInterface); + $this->entityManager = $entityManager; + $repository = $container->get(ProjectRepository::class); + \assert($repository instanceof ProjectRepository); + $this->repository = $repository; } public function testGetIncluded(): void @@ -91,6 +95,7 @@ public function testGetFilteredPaginationByKey(): void public function testGetProjectTrackerIdsByDataProviders(): void { $dpRepo = self::getContainer()->get(DataProviderRepository::class); + \assert($dpRepo instanceof DataProviderRepository); $dataProviders = $dpRepo->findAll(); $this->assertNotEmpty($dataProviders); diff --git a/tests/Integration/Repository/ServiceAgreementRepositoryTest.php b/tests/Integration/Repository/ServiceAgreementRepositoryTest.php index fd397ff7d..ef198f35d 100644 --- a/tests/Integration/Repository/ServiceAgreementRepositoryTest.php +++ b/tests/Integration/Repository/ServiceAgreementRepositoryTest.php @@ -14,7 +14,9 @@ class ServiceAgreementRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(ServiceAgreementRepository::class); + $repository = self::getContainer()->get(ServiceAgreementRepository::class); + \assert($repository instanceof ServiceAgreementRepository); + $this->repository = $repository; } public function testGetFilteredPaginationNoFilter(): void diff --git a/tests/Integration/Repository/SubscriptionRepositoryTest.php b/tests/Integration/Repository/SubscriptionRepositoryTest.php index b95564fdb..f22c60837 100644 --- a/tests/Integration/Repository/SubscriptionRepositoryTest.php +++ b/tests/Integration/Repository/SubscriptionRepositoryTest.php @@ -14,7 +14,9 @@ class SubscriptionRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(SubscriptionRepository::class); + $repository = self::getContainer()->get(SubscriptionRepository::class); + \assert($repository instanceof SubscriptionRepository); + $this->repository = $repository; } public function testFindByCustom(): void diff --git a/tests/Integration/Repository/WorkerGroupRepositoryTest.php b/tests/Integration/Repository/WorkerGroupRepositoryTest.php index 4cc115744..6c677513f 100644 --- a/tests/Integration/Repository/WorkerGroupRepositoryTest.php +++ b/tests/Integration/Repository/WorkerGroupRepositoryTest.php @@ -13,7 +13,9 @@ class WorkerGroupRepositoryTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->repository = self::getContainer()->get(WorkerGroupRepository::class); + $repository = self::getContainer()->get(WorkerGroupRepository::class); + \assert($repository instanceof WorkerGroupRepository); + $this->repository = $repository; } public function testGetFilteredPaginationNoFilter(): void diff --git a/tests/Integration/Repository/WorkerRepositoryTest.php b/tests/Integration/Repository/WorkerRepositoryTest.php index 516d5dc24..674fcd847 100644 --- a/tests/Integration/Repository/WorkerRepositoryTest.php +++ b/tests/Integration/Repository/WorkerRepositoryTest.php @@ -16,8 +16,12 @@ protected function setUp(): void { self::bootKernel(); $container = self::getContainer(); - $this->entityManager = $container->get(EntityManagerInterface::class); - $this->repository = $container->get(WorkerRepository::class); + $entityManager = $container->get(EntityManagerInterface::class); + \assert($entityManager instanceof EntityManagerInterface); + $this->entityManager = $entityManager; + $repository = $container->get(WorkerRepository::class); + \assert($repository instanceof WorkerRepository); + $this->repository = $repository; } public function testFindAllIncludedInReports(): void diff --git a/tests/Integration/Repository/WorklogRepositoryTest.php b/tests/Integration/Repository/WorklogRepositoryTest.php index 53c902772..ce5b2e661 100644 --- a/tests/Integration/Repository/WorklogRepositoryTest.php +++ b/tests/Integration/Repository/WorklogRepositoryTest.php @@ -19,15 +19,22 @@ protected function setUp(): void { self::bootKernel(); $container = self::getContainer(); - $this->entityManager = $container->get(EntityManagerInterface::class); - $this->repository = $container->get(WorklogRepository::class); - $this->projectRepository = $container->get(ProjectRepository::class); + $entityManager = $container->get(EntityManagerInterface::class); + \assert($entityManager instanceof EntityManagerInterface); + $this->entityManager = $entityManager; + $repository = $container->get(WorklogRepository::class); + \assert($repository instanceof WorklogRepository); + $this->repository = $repository; + $projectRepository = $container->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); + $this->projectRepository = $projectRepository; } public function testFindByFilterDataBasic(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); + \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); $filterData = new InvoiceEntryWorklogsFilterData(); @@ -42,6 +49,7 @@ public function testFindByFilterDataByWorker(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); + \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); $filterData = new InvoiceEntryWorklogsFilterData(); @@ -60,6 +68,7 @@ public function testFindByFilterDataByDateRange(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); + \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); $year = (new \DateTime())->format('Y'); @@ -83,6 +92,7 @@ public function testFindByFilterDataByBilled(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); + \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); $filterData = new InvoiceEntryWorklogsFilterData(); @@ -101,6 +111,7 @@ public function testFindByFilterDataOnlyAvailable(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); + \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); $filterData = new InvoiceEntryWorklogsFilterData(); diff --git a/tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php b/tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php index 7cd0cb253..a46b9a7f2 100644 --- a/tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php +++ b/tests/Integration/Service/BillableUnbilledHoursReportServiceTest.php @@ -12,8 +12,8 @@ public function testGetBillableUnbilledHoursReportAggregatesBillableUnbilledWork self::bootKernel(); $container = self::getContainer(); - /** @var BillableUnbilledHoursReportService $service */ $service = $container->get(BillableUnbilledHoursReportService::class); + \assert($service instanceof BillableUnbilledHoursReportService); $year = (int) (new \DateTime())->format('Y'); @@ -41,8 +41,8 @@ public function testGetBillableUnbilledHoursReportRestrictsToQuarter(): void self::bootKernel(); $container = self::getContainer(); - /** @var BillableUnbilledHoursReportService $service */ $service = $container->get(BillableUnbilledHoursReportService::class); + \assert($service instanceof BillableUnbilledHoursReportService); $year = (int) (new \DateTime())->format('Y'); diff --git a/tests/Integration/Service/ForecastReportServiceTest.php b/tests/Integration/Service/ForecastReportServiceTest.php index 7e15ccf9e..ec0454ad1 100644 --- a/tests/Integration/Service/ForecastReportServiceTest.php +++ b/tests/Integration/Service/ForecastReportServiceTest.php @@ -12,8 +12,8 @@ public function testGetForecastReportRunsAgainstFixturesAndReturnsConsistentTota self::bootKernel(); $container = self::getContainer(); - /** @var ForecastReportService $service */ $service = $container->get(ForecastReportService::class); + \assert($service instanceof ForecastReportService); // Cover the entire fixture year plus a year of headroom on either side. $year = (int) (new \DateTime())->format('Y'); diff --git a/tests/Integration/Service/HourReportServiceTest.php b/tests/Integration/Service/HourReportServiceTest.php index 8bdc64790..3cc0de558 100644 --- a/tests/Integration/Service/HourReportServiceTest.php +++ b/tests/Integration/Service/HourReportServiceTest.php @@ -14,10 +14,10 @@ public function testGetHourReportAggregatesWorklogsAcrossIssuesAndEpicTag(): voi self::bootKernel(); $container = self::getContainer(); - /** @var ProjectRepository $projectRepository */ $projectRepository = $container->get(ProjectRepository::class); - /** @var HourReportService $service */ + \assert($projectRepository instanceof ProjectRepository); $service = $container->get(HourReportService::class); + \assert($service instanceof HourReportService); // project-0-0 is the only project whose first issue is tagged with "Epic 1" in fixtures. $project = $projectRepository->findOneBy(['name' => 'project-0-0']); @@ -56,10 +56,10 @@ public function testGetHourReportDateRangeExcludesOutOfRangeWorklogs(): void self::bootKernel(); $container = self::getContainer(); - /** @var ProjectRepository $projectRepository */ $projectRepository = $container->get(ProjectRepository::class); - /** @var HourReportService $service */ + \assert($projectRepository instanceof ProjectRepository); $service = $container->get(HourReportService::class); + \assert($service instanceof HourReportService); $project = $projectRepository->findOneBy(['name' => 'project-0-0']); $this->assertInstanceOf(Project::class, $project); diff --git a/tests/Integration/Service/InvoicingRateReportServiceTest.php b/tests/Integration/Service/InvoicingRateReportServiceTest.php index 4d1f38303..610bbeaa4 100644 --- a/tests/Integration/Service/InvoicingRateReportServiceTest.php +++ b/tests/Integration/Service/InvoicingRateReportServiceTest.php @@ -15,8 +15,8 @@ public function testGetInvoicingRateReportProducesPeriodsForEveryIncludedWorker( self::bootKernel(); $container = self::getContainer(); - /** @var InvoicingRateReportService $service */ $service = $container->get(InvoicingRateReportService::class); + \assert($service instanceof InvoicingRateReportService); $year = (int) (new \DateTime())->format('Y'); diff --git a/tests/Integration/Service/LeantimeApiServiceTest.php b/tests/Integration/Service/LeantimeApiServiceTest.php index bed32a50d..e3e1a7c80 100644 --- a/tests/Integration/Service/LeantimeApiServiceTest.php +++ b/tests/Integration/Service/LeantimeApiServiceTest.php @@ -33,18 +33,18 @@ public function testUpdate(): void $container = self::getContainer(); $messageBus = $container->get(MessageBusInterface::class); - $dataProviderRepository = $container->get(DataProviderRepository::class); - $projectRepository = $container->get(ProjectRepository::class); - $versionRepository = $container->get(VersionRepository::class); - $issueRepository = $container->get(IssueRepository::class); - $worklogRepository = $container->get(WorklogRepository::class); - $entityManager = $container->get(EntityManagerInterface::class); \assert($messageBus instanceof MessageBusInterface); + $dataProviderRepository = $container->get(DataProviderRepository::class); \assert($dataProviderRepository instanceof DataProviderRepository); + $projectRepository = $container->get(ProjectRepository::class); \assert($projectRepository instanceof ProjectRepository); + $versionRepository = $container->get(VersionRepository::class); \assert($versionRepository instanceof VersionRepository); + $issueRepository = $container->get(IssueRepository::class); \assert($issueRepository instanceof IssueRepository); + $worklogRepository = $container->get(WorklogRepository::class); \assert($worklogRepository instanceof WorklogRepository); + $entityManager = $container->get(EntityManagerInterface::class); \assert($entityManager instanceof EntityManagerInterface); $loggerMock = $this->createMock(LoggerInterface::class); @@ -182,18 +182,18 @@ public function testDeleted(): void $container = self::getContainer(); $messageBus = $container->get(MessageBusInterface::class); - $dataProviderRepository = $container->get(DataProviderRepository::class); - $projectRepository = $container->get(ProjectRepository::class); - $versionRepository = $container->get(VersionRepository::class); - $issueRepository = $container->get(IssueRepository::class); - $worklogRepository = $container->get(WorklogRepository::class); - $entityManager = $container->get(EntityManagerInterface::class); \assert($messageBus instanceof MessageBusInterface); + $dataProviderRepository = $container->get(DataProviderRepository::class); \assert($dataProviderRepository instanceof DataProviderRepository); + $projectRepository = $container->get(ProjectRepository::class); \assert($projectRepository instanceof ProjectRepository); + $versionRepository = $container->get(VersionRepository::class); \assert($versionRepository instanceof VersionRepository); + $issueRepository = $container->get(IssueRepository::class); \assert($issueRepository instanceof IssueRepository); + $worklogRepository = $container->get(WorklogRepository::class); \assert($worklogRepository instanceof WorklogRepository); + $entityManager = $container->get(EntityManagerInterface::class); \assert($entityManager instanceof EntityManagerInterface); $loggerMock = $this->createMock(LoggerInterface::class); diff --git a/tests/Integration/Service/ProjectBillingServiceTest.php b/tests/Integration/Service/ProjectBillingServiceTest.php index ba8c3f380..4b79fc53b 100644 --- a/tests/Integration/Service/ProjectBillingServiceTest.php +++ b/tests/Integration/Service/ProjectBillingServiceTest.php @@ -18,20 +18,20 @@ public function testGetIssuesNotIncludedInProjectBilling(): void $container = self::getContainer(); - /** @var EntityManagerInterface $entityManager */ $entityManager = $container->get(EntityManagerInterface::class); + \assert($entityManager instanceof EntityManagerInterface); - /** @var ProjectRepository $projectRepository */ $projectRepository = $container->get(ProjectRepository::class); + \assert($projectRepository instanceof ProjectRepository); - /** @var ProjectBillingService $projectBillingService */ $projectBillingService = $container->get(ProjectBillingService::class); + \assert($projectBillingService instanceof ProjectBillingService); - /** @var BillingService $billingService */ $billingService = $container->get(BillingService::class); + \assert($billingService instanceof BillingService); - /** @var IssueRepository $issueRepository */ $issueRepository = $container->get(IssueRepository::class); + \assert($issueRepository instanceof IssueRepository); $project = $projectRepository->findOneBy([], ['id' => 'asc']); \assert(null !== $project); diff --git a/tests/Integration/Service/WorkloadReportServiceTest.php b/tests/Integration/Service/WorkloadReportServiceTest.php index 21e2caca2..af8da211f 100644 --- a/tests/Integration/Service/WorkloadReportServiceTest.php +++ b/tests/Integration/Service/WorkloadReportServiceTest.php @@ -15,8 +15,8 @@ public function testGetWorkloadReportProducesPeriodsForEveryIncludedWorker(): vo self::bootKernel(); $container = self::getContainer(); - /** @var WorkloadReportService $service */ $service = $container->get(WorkloadReportService::class); + \assert($service instanceof WorkloadReportService); $year = (int) (new \DateTime())->format('Y'); From 0ce5e1f76a7a68784a98aeea860d84e5b79280d3 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Wed, 20 May 2026 15:20:37 +0200 Subject: [PATCH 20/23] 7273: Added asserts --- .phpstan-no-baseline.neon | 7 +++++++ .../Controller/AbstractControllerTestCase.php | 1 + tests/Integration/Controller/InvoiceEntryFlowTest.php | 2 ++ .../Controller/ProjectBillingFullFlowTest.php | 1 + tests/Integration/Repository/IssueRepositoryTest.php | 6 ++++++ .../Integration/Repository/ProductRepositoryTest.php | 1 + .../Integration/Repository/WorklogRepositoryTest.php | 10 ++++++++++ tests/Integration/Service/LeantimeApiServiceTest.php | 11 +++++++++++ .../Integration/Service/ProjectBillingServiceTest.php | 1 + 9 files changed, 40 insertions(+) create mode 100644 .phpstan-no-baseline.neon diff --git a/.phpstan-no-baseline.neon b/.phpstan-no-baseline.neon new file mode 100644 index 000000000..dd83cf7cf --- /dev/null +++ b/.phpstan-no-baseline.neon @@ -0,0 +1,7 @@ +parameters: + level: 8 + paths: + - src + - tests + excludePaths: + - src/Kernel.php diff --git a/tests/Integration/Controller/AbstractControllerTestCase.php b/tests/Integration/Controller/AbstractControllerTestCase.php index 7f9c49175..05ba3603a 100644 --- a/tests/Integration/Controller/AbstractControllerTestCase.php +++ b/tests/Integration/Controller/AbstractControllerTestCase.php @@ -27,6 +27,7 @@ protected function getUserWithRole(string $role): User $userRepository = static::getContainer()->get(UserRepository::class); \assert($userRepository instanceof UserRepository); $user = $userRepository->findOneBy(['email' => $email]); + $this->assertNotNull($user); if (null === $user) { throw new \RuntimeException(sprintf('Fixture user %s not found; run `task fixtures`.', $email)); } diff --git a/tests/Integration/Controller/InvoiceEntryFlowTest.php b/tests/Integration/Controller/InvoiceEntryFlowTest.php index 27253528a..b6c27af12 100644 --- a/tests/Integration/Controller/InvoiceEntryFlowTest.php +++ b/tests/Integration/Controller/InvoiceEntryFlowTest.php @@ -187,9 +187,11 @@ private function setupInvoiceWithManualEntry(bool $recorded = false): array $invoiceRepository = $container->get(InvoiceRepository::class); \assert($invoiceRepository instanceof InvoiceRepository); $invoice = $invoiceRepository->find($invoiceId); + $this->assertNotNull($invoice); $invoiceEntryRepository = $container->get(InvoiceEntryRepository::class); \assert($invoiceEntryRepository instanceof InvoiceEntryRepository); $entry = $invoiceEntryRepository->find($entryId); + $this->assertNotNull($entry); $this->assertInstanceOf(Invoice::class, $invoice); $this->assertInstanceOf(InvoiceEntry::class, $entry); diff --git a/tests/Integration/Controller/ProjectBillingFullFlowTest.php b/tests/Integration/Controller/ProjectBillingFullFlowTest.php index fe53753ea..ae7fda62c 100644 --- a/tests/Integration/Controller/ProjectBillingFullFlowTest.php +++ b/tests/Integration/Controller/ProjectBillingFullFlowTest.php @@ -169,6 +169,7 @@ private function reload(int $id): ?ProjectBilling $projectBillingRepository = static::getContainer()->get(ProjectBillingRepository::class); \assert($projectBillingRepository instanceof ProjectBillingRepository); $pb = $projectBillingRepository->find($id); + $this->assertNotNull($pb); if (null !== $pb) { $pb->getInvoices()->count(); foreach ($pb->getInvoices() as $invoice) { diff --git a/tests/Integration/Repository/IssueRepositoryTest.php b/tests/Integration/Repository/IssueRepositoryTest.php index cdebd5412..f376ea684 100644 --- a/tests/Integration/Repository/IssueRepositoryTest.php +++ b/tests/Integration/Repository/IssueRepositoryTest.php @@ -58,6 +58,7 @@ public function testGetFilteredPaginationByName(): void public function testGetFilteredPaginationByProject(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $filterData = new IssueFilterData(); $filterData->project = $project; $result = $this->repository->getFilteredPagination($filterData); @@ -72,6 +73,7 @@ public function testFindEpicOptionsByProject(): void { // project-0-0 has issue-0-0 linked to 'Epic 1' $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $result = $this->repository->findEpicOptionsByProject($project); $this->assertArrayHasKey('Epic 1', $result); @@ -81,6 +83,7 @@ public function testGetClosedIssuesFromInterval(): void { // Even-index projects have DONE status issues with resolutionDate=today $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $periodStart = new \DateTime('-1 day'); $periodEnd = new \DateTime('+1 day'); @@ -96,6 +99,7 @@ public function testGetClosedIssuesFromIntervalNoResults(): void { // Odd-index projects have NEW status issues $project = $this->projectRepository->findOneBy(['name' => 'project-0-1']); + $this->assertNotNull($project); $periodStart = new \DateTime('-1 day'); $periodEnd = new \DateTime('+1 day'); @@ -109,6 +113,7 @@ public function testIssuesContainingVersion(): void $versionRepository = self::getContainer()->get(VersionRepository::class); \assert($versionRepository instanceof VersionRepository); $version = $versionRepository->findOneBy([], ['id' => 'ASC']); + $this->assertNotNull($version); $result = $this->repository->issuesContainingVersion($version); @@ -151,6 +156,7 @@ public function testFindIssuesInDateRangeWithWorkerGroup(): void public function testFindIssuesInDateRangeWithProjects(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $startDate = (new \DateTime('-1 day'))->format('Y-m-d'); $endDate = (new \DateTime('+2 days'))->format('Y-m-d'); diff --git a/tests/Integration/Repository/ProductRepositoryTest.php b/tests/Integration/Repository/ProductRepositoryTest.php index 0113e681e..bb4b271c5 100644 --- a/tests/Integration/Repository/ProductRepositoryTest.php +++ b/tests/Integration/Repository/ProductRepositoryTest.php @@ -44,6 +44,7 @@ public function testGetFilteredPaginationByProject(): void $projectRepo = self::getContainer()->get(ProjectRepository::class); \assert($projectRepo instanceof ProjectRepository); $project = $projectRepo->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $filterData = new ProductFilterData(); $filterData->project = $project; diff --git a/tests/Integration/Repository/WorklogRepositoryTest.php b/tests/Integration/Repository/WorklogRepositoryTest.php index ce5b2e661..a5ba913f2 100644 --- a/tests/Integration/Repository/WorklogRepositoryTest.php +++ b/tests/Integration/Repository/WorklogRepositoryTest.php @@ -33,9 +33,11 @@ protected function setUp(): void public function testFindByFilterDataBasic(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); + $this->assertNotNull($invoiceEntry); $filterData = new InvoiceEntryWorklogsFilterData(); $filterData->onlyAvailable = false; @@ -48,9 +50,11 @@ public function testFindByFilterDataBasic(): void public function testFindByFilterDataByWorker(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); + $this->assertNotNull($invoiceEntry); $filterData = new InvoiceEntryWorklogsFilterData(); $filterData->onlyAvailable = false; @@ -67,9 +71,11 @@ public function testFindByFilterDataByWorker(): void public function testFindByFilterDataByDateRange(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); + $this->assertNotNull($invoiceEntry); $year = (new \DateTime())->format('Y'); $filterData = new InvoiceEntryWorklogsFilterData(); @@ -91,9 +97,11 @@ public function testFindByFilterDataByDateRange(): void public function testFindByFilterDataByBilled(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); + $this->assertNotNull($invoiceEntry); $filterData = new InvoiceEntryWorklogsFilterData(); $filterData->onlyAvailable = false; @@ -110,9 +118,11 @@ public function testFindByFilterDataByBilled(): void public function testFindByFilterDataOnlyAvailable(): void { $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $invoiceEntryRepo = self::getContainer()->get(InvoiceEntryRepository::class); \assert($invoiceEntryRepo instanceof InvoiceEntryRepository); $invoiceEntry = $invoiceEntryRepo->findOneBy([], ['id' => 'ASC']); + $this->assertNotNull($invoiceEntry); $filterData = new InvoiceEntryWorklogsFilterData(); $filterData->onlyAvailable = true; diff --git a/tests/Integration/Service/LeantimeApiServiceTest.php b/tests/Integration/Service/LeantimeApiServiceTest.php index e3e1a7c80..d3da7aa2b 100644 --- a/tests/Integration/Service/LeantimeApiServiceTest.php +++ b/tests/Integration/Service/LeantimeApiServiceTest.php @@ -114,6 +114,7 @@ public function testUpdate(): void $after = count($projectRepository->findAll()); $this->assertEquals($before + 2, $after); $project = $projectRepository->findOneBy(['projectTrackerId' => 50]); + $this->assertNotNull($project); \assert(null !== $project && null !== $project->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $project->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter @@ -121,6 +122,7 @@ public function testUpdate(): void $after = count($projectRepository->findAll()); $this->assertEquals($before + 2, $after); $project = $projectRepository->findOneBy(['projectTrackerId' => 50]); + $this->assertNotNull($project); \assert(null !== $project && null !== $project->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $project->getSourceModifiedDate()->getTimestamp()); @@ -131,6 +133,7 @@ public function testUpdate(): void $after = count($versionRepository->findAll()); $this->assertEquals($before + 2, $after); $version = $versionRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); + $this->assertNotNull($version); \assert(null !== $version && null !== $version->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $version->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter @@ -138,6 +141,7 @@ public function testUpdate(): void $after = count($versionRepository->findAll()); $this->assertEquals($before + 2, $after); $version = $versionRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); + $this->assertNotNull($version); \assert(null !== $version && null !== $version->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $version->getSourceModifiedDate()->getTimestamp()); @@ -148,6 +152,7 @@ public function testUpdate(): void $after = count($issueRepository->findAll()); $this->assertEquals($before + 2, $after); $issue = $issueRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); + $this->assertNotNull($issue); \assert(null !== $issue && null !== $issue->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $issue->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter @@ -155,6 +160,7 @@ public function testUpdate(): void $after = count($issueRepository->findAll()); $this->assertEquals($before + 2, $after); $issue = $issueRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); + $this->assertNotNull($issue); \assert(null !== $issue && null !== $issue->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $issue->getSourceModifiedDate()->getTimestamp()); @@ -165,6 +171,7 @@ public function testUpdate(): void $after = count($worklogRepository->findAll()); $this->assertEquals($before + 2, $after); $worklog = $worklogRepository->findOneBy(['worklogId' => 1, 'dataProvider' => $dataProvider]); + $this->assertNotNull($worklog); \assert(null !== $worklog && null !== $worklog->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $worklog->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter @@ -172,6 +179,7 @@ public function testUpdate(): void $after = count($worklogRepository->findAll()); $this->assertEquals($before + 2, $after); $worklog = $worklogRepository->findOneBy(['worklogId' => 1, 'dataProvider' => $dataProvider]); + $this->assertNotNull($worklog); \assert(null !== $worklog && null !== $worklog->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $worklog->getSourceModifiedDate()->getTimestamp()); } @@ -390,14 +398,17 @@ public function testDeleted(): void $this->assertEquals($countProjectsBeforeCreate + 1, $countProjectsAfterDelete); $project1 = $projectRepository->find($projectId1); + $this->assertNotNull($project1); \assert(null !== $project1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $project1->getSourceDeletedDate()); $issue1 = $issueRepository->find($issueId1); + $this->assertNotNull($issue1); \assert(null !== $issue1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $issue1->getSourceDeletedDate()); $worklog1 = $worklogRepository->find($worklogId1); + $this->assertNotNull($worklog1); \assert(null !== $worklog1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $worklog1->getSourceDeletedDate()); } diff --git a/tests/Integration/Service/ProjectBillingServiceTest.php b/tests/Integration/Service/ProjectBillingServiceTest.php index 4b79fc53b..4b8caa8ec 100644 --- a/tests/Integration/Service/ProjectBillingServiceTest.php +++ b/tests/Integration/Service/ProjectBillingServiceTest.php @@ -34,6 +34,7 @@ public function testGetIssuesNotIncludedInProjectBilling(): void \assert($issueRepository instanceof IssueRepository); $project = $projectRepository->findOneBy([], ['id' => 'asc']); + $this->assertNotNull($project); \assert(null !== $project); $periodStart = (new \DateTime())->sub(new \DateInterval('P1D')); From 8ade5ff9606fc4f8052ba6595001843172723555 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:45:54 +0200 Subject: [PATCH 21/23] 7273: Fixed new baseline issues --- phpstan-baseline.neon | 529 +----------------- src/Entity/ServiceAgreement.php | 2 +- src/Form/CybersecurityReportType.php | 3 + src/Repository/IssueRepository.php | 8 +- src/Repository/ProjectRepository.php | 5 +- src/Service/CybersecurityReportService.php | 28 +- .../Controller/AbstractControllerTestCase.php | 17 +- .../Controller/InvoiceEntryFlowTest.php | 10 +- .../Controller/InvoiceFlowTest.php | 8 +- .../Controller/InvoiceFullFlowTest.php | 25 +- .../Controller/ProjectBillingFlowTest.php | 8 +- .../Controller/ProjectBillingFullFlowTest.php | 18 +- .../Repository/ClientRepositoryTest.php | 4 +- .../Repository/InvoiceRepositoryTest.php | 2 +- .../Repository/IssueRepositoryTest.php | 17 +- .../Repository/ProductRepositoryTest.php | 6 +- .../Repository/ProjectRepositoryTest.php | 11 +- .../Repository/WorklogRepositoryTest.php | 40 +- .../CybersecurityReportServiceTest.php | 6 +- .../InvoicingRateReportServiceTest.php | 2 +- .../Service/LeantimeApiServiceTest.php | 19 +- .../Service/ProjectBillingServiceTest.php | 1 - .../Service/WorkloadReportServiceTest.php | 2 +- tests/Unit/Service/BillingServiceTest.php | 6 +- .../CybersecurityReportServiceTest.php | 9 +- .../Service/ForecastReportServiceTest.php | 5 +- tests/Unit/Service/HourReportServiceTest.php | 2 + .../InvoicingRateReportServiceTest.php | 5 +- .../Service/ManagementReportServiceTest.php | 4 +- tests/Unit/Service/PlanningServiceTest.php | 1 + .../Service/ProjectBillingServiceTest.php | 2 +- 31 files changed, 168 insertions(+), 637 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6f24c8bf5..f51e71c3f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,529 +1,2 @@ parameters: - ignoreErrors: - - - message: '#^Property App\\Entity\\ServiceAgreement\:\:\$project type mapping mismatch\: property can contain App\\Entity\\Project\|null but database expects App\\Entity\\Project\.$#' - identifier: doctrine.associationType - count: 1 - path: src/Entity/ServiceAgreement.php - - - - message: '#^Class App\\Form\\CybersecurityReportType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' - identifier: missingType.generics - count: 1 - path: src/Form/CybersecurityReportType.php - - - - message: '#^Method App\\Repository\\IssueRepository\:\:issuesContainingVersionTitle\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/IssueRepository.php - - - - message: '#^Method App\\Repository\\ProjectRepository\:\:getProjectIdsWithCybersecurityAgreement\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Repository/ProjectRepository.php - - - - message: '#^Method App\\Tests\\Integration\\Controller\\AbstractControllerTestCase\:\:assertDeniedFor\(\) has parameter \$roles with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Integration/Controller/AbstractControllerTestCase.php - - - - message: '#^Method App\\Tests\\Integration\\Controller\\AbstractControllerTestCase\:\:assertGrantedFor\(\) has parameter \$roles with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Integration/Controller/AbstractControllerTestCase.php - - - - message: '#^Method App\\Tests\\Integration\\Controller\\AbstractControllerTestCase\:\:assertSmokeMatrix\(\) has parameter \$allowedRoles with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Integration/Controller/AbstractControllerTestCase.php - - - - message: '#^Method App\\Tests\\Integration\\Controller\\AbstractControllerTestCase\:\:assertSmokeMatrix\(\) has parameter \$deniedRoles with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Integration/Controller/AbstractControllerTestCase.php - - - - message: '#^Method App\\Tests\\Integration\\Controller\\AbstractControllerTestCase\:\:createClientLoggedInAs\(\) has parameter \$roles with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: tests/Integration/Controller/AbstractControllerTestCase.php - - - - message: '#^Strict comparison using \=\=\= between null and App\\Entity\\User will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: tests/Integration/Controller/AbstractControllerTestCase.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Entity\\\\Invoice'' and App\\Entity\\Invoice will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Entity\\\\InvoiceEntry'' and App\\Entity\\InvoiceEntry will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - - message: '#^Parameter \#1 \$id of method App\\Tests\\Integration\\Controller\\InvoiceEntryFlowTest\:\:reloadEntry\(\) expects int, int\|null given\.$#' - identifier: argument.type - count: 5 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - - message: '#^Parameter \#1 \$invoiceId of method App\\Tests\\Integration\\Controller\\InvoiceEntryFlowTest\:\:markInvoiceRecorded\(\) expects int, int\|null given\.$#' - identifier: argument.type - count: 2 - path: tests/Integration/Controller/InvoiceEntryFlowTest.php - - - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFlowTest.php - - - - message: '#^Parameter \#2 \$string of method PHPUnit\\Framework\\Assert\:\:assertMatchesRegularExpression\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Integration/Controller/InvoiceFlowTest.php - - - - message: '#^Cannot call method getAmount\(\) on App\\Entity\\InvoiceEntry\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method getExportedDate\(\) on App\\Entity\\Invoice\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method getLockedContactName\(\) on App\\Entity\\Invoice\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method getLockedCustomerKey\(\) on App\\Entity\\Invoice\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method getLockedEan\(\) on App\\Entity\\Invoice\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method getLockedType\(\) on App\\Entity\\Invoice\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method getRecordedDate\(\) on App\\Entity\\Invoice\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method getTotalPrice\(\) on App\\Entity\\InvoiceEntry\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method isNoCost\(\) on App\\Entity\\Invoice\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method isRecorded\(\) on App\\Entity\\Invoice\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Method App\\Tests\\Integration\\Controller\\InvoiceFullFlowTest\:\:submitNewEntry\(\) should return int but returns int\|null\.$#' - identifier: return.type - count: 1 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Offset 1 might not exist on array\{\}\|array\{non\-falsy\-string, numeric\-string\}\.$#' - identifier: offsetAccess.notFound - count: 2 - path: tests/Integration/Controller/InvoiceFullFlowTest.php - - - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/ProjectBillingFlowTest.php - - - - message: '#^Parameter \#2 \$string of method PHPUnit\\Framework\\Assert\:\:assertMatchesRegularExpression\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Integration/Controller/ProjectBillingFlowTest.php - - - - message: '#^Cannot call method getDescription\(\) on App\\Entity\\ProjectBilling\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Cannot call method getExportedDate\(\) on App\\Entity\\ProjectBilling\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Cannot call method getInvoices\(\) on App\\Entity\\ProjectBilling\|null\.$#' - identifier: method.nonObject - count: 4 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Cannot call method getName\(\) on App\\Entity\\ProjectBilling\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Cannot call method isRecorded\(\) on App\\Entity\\ProjectBilling\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Method App\\Tests\\Integration\\Controller\\ProjectBillingFullFlowTest\:\:reload\(\) never returns null so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Offset 1 might not exist on array\{\}\|array\{non\-falsy\-string, numeric\-string\}\.$#' - identifier: offsetAccess.notFound - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Strict comparison using \!\=\= between null and App\\Entity\\ProjectBilling will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^You should use assertCount\(\$expectedCount, \$variable\) instead of assertSame\(\$expectedCount, \$variable\-\>count\(\)\)\.$#' - identifier: phpunit.assertCount - count: 1 - path: tests/Integration/Controller/ProjectBillingFullFlowTest.php - - - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 2 - path: tests/Integration/Repository/ClientRepositoryTest.php - - - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Integration/Repository/InvoiceRepositoryTest.php - - - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 2 - path: tests/Integration/Repository/IssueRepositoryTest.php - - - - message: '#^Parameter \#2 \$haystack of method App\\Tests\\Integration\\Repository\\IssueRepositoryTest\:\:assertStringContains\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Integration/Repository/IssueRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\IssueRepositoryTest\:\:\$entityManager is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: tests/Integration/Repository/IssueRepositoryTest.php - - - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Repository/ProductRepositoryTest.php - - - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Integration/Repository/ProductRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with array will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - - - message: '#^Cannot call method getId\(\) on App\\Entity\\Project\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 2 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\ProjectRepositoryTest\:\:\$entityManager is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: tests/Integration/Repository/ProjectRepositoryTest.php - - - - message: '#^Call to an undefined method object\:\:findOneBy\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Call to method getIssue\(\) on an unknown class App\\Tests\\Integration\\Repository\\Worklog\.$#' - identifier: class.notFound - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Cannot call method format\(\) on DateTimeInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Cannot call method getId\(\) on App\\Entity\\Issue\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Cannot call method getTimestamp\(\) on DateTimeInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Class App\\Tests\\Integration\\Repository\\Worklog not found\.$#' - identifier: class.notFound - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Property App\\Tests\\Integration\\Repository\\WorklogRepositoryTest\:\:\$entityManager is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: tests/Integration/Repository/WorklogRepositoryTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\CybersecurityReportData'' and App\\Model\\Reports\\CybersecurityReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/CybersecurityReportServiceTest.php - - - - message: '#^Property App\\Tests\\Integration\\Service\\CybersecurityReportServiceTest\:\:\$service \(App\\Service\\CybersecurityReportService\) does not accept object\.$#' - identifier: assign.propertyType - count: 1 - path: tests/Integration/Service/CybersecurityReportServiceTest.php - - - - message: '#^You should use assertCount\(\$expectedCount, \$variable\) instead of assertSame\(\$expectedCount, \$variable\-\>count\(\)\)\.$#' - identifier: phpunit.assertCount - count: 1 - path: tests/Integration/Service/InvoicingRateReportServiceTest.php - - - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 3 - path: tests/Integration/Service/LeantimeApiServiceTest.php - - - - message: '#^Strict comparison using \!\=\= between null and App\\Entity\\Issue will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 3 - path: tests/Integration/Service/LeantimeApiServiceTest.php - - - - message: '#^Strict comparison using \!\=\= between null and App\\Entity\\Project will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 3 - path: tests/Integration/Service/LeantimeApiServiceTest.php - - - - message: '#^Strict comparison using \!\=\= between null and App\\Entity\\Version will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 2 - path: tests/Integration/Service/LeantimeApiServiceTest.php - - - - message: '#^Strict comparison using \!\=\= between null and App\\Entity\\Worklog will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 3 - path: tests/Integration/Service/LeantimeApiServiceTest.php - - - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: tests/Integration/Service/ProjectBillingServiceTest.php - - - - message: '#^Strict comparison using \!\=\= between null and App\\Entity\\Project will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: tests/Integration/Service/ProjectBillingServiceTest.php - - - - message: '#^You should use assertCount\(\$expectedCount, \$variable\) instead of assertSame\(\$expectedCount, \$variable\-\>count\(\)\)\.$#' - identifier: phpunit.assertCount - count: 1 - path: tests/Integration/Service/WorkloadReportServiceTest.php - - - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|false given\.$#' - identifier: argument.type - count: 1 - path: tests/Unit/Service/BillingServiceTest.php - - - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 2 - path: tests/Unit/Service/BillingServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\IssueRepository\:\:expects\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/CybersecurityReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\IssueRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/CybersecurityReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\ProjectRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 4 - path: tests/Unit/Service/CybersecurityReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorklogRepository\:\:expects\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/Service/CybersecurityReportServiceTest.php - - - - message: '#^Call to an undefined method App\\Repository\\WorklogRepository\:\:method\(\)\.$#' - identifier: method.notFound - count: 3 - path: tests/Unit/Service/CybersecurityReportServiceTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''App\\\\Model\\\\Reports\\\\CybersecurityReportData'' and App\\Model\\Reports\\CybersecurityReportData will always evaluate to true\.$#' - identifier: method.alreadyNarrowedType - count: 1 - path: tests/Unit/Service/CybersecurityReportServiceTest.php - - - - message: '#^Cannot access property \$invoiced on App\\Model\\Reports\\ForecastReportProjectData\|null\.$#' - identifier: property.nonObject - count: 1 - path: tests/Unit/Service/ForecastReportServiceTest.php - - - - message: '#^Cannot access property \$issues on App\\Model\\Reports\\ForecastReportProjectData\|null\.$#' - identifier: property.nonObject - count: 1 - path: tests/Unit/Service/ForecastReportServiceTest.php - - - - message: '#^Cannot access property \$projectTickets on App\\Model\\Reports\\HourReportProjectTag\|null\.$#' - identifier: property.nonObject - count: 1 - path: tests/Unit/Service/HourReportServiceTest.php - - - - message: '#^Cannot access property \$tag on App\\Model\\Reports\\HourReportProjectTag\|null\.$#' - identifier: property.nonObject - count: 1 - path: tests/Unit/Service/HourReportServiceTest.php - - - - message: '#^Cannot access property \$totalEstimated on App\\Model\\Reports\\HourReportProjectTag\|null\.$#' - identifier: property.nonObject - count: 1 - path: tests/Unit/Service/HourReportServiceTest.php - - - - message: '#^Cannot access property \$totalSpent on App\\Model\\Reports\\HourReportProjectTag\|null\.$#' - identifier: property.nonObject - count: 1 - path: tests/Unit/Service/HourReportServiceTest.php - - - - message: '#^Cannot access property \$average on App\\Model\\Reports\\InvoicingRateReportWorker\|false\.$#' - identifier: property.nonObject - count: 1 - path: tests/Unit/Service/InvoicingRateReportServiceTest.php - - - - message: '#^Parameter \#2 \$timestamp of function date expects int\|null, int\|false given\.$#' - identifier: argument.type - count: 2 - path: tests/Unit/Service/InvoicingRateReportServiceTest.php - - - - message: '#^Parameter \#2 \$haystack of method PHPUnit\\Framework\\Assert\:\:assertStringContainsString\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 2 - path: tests/Unit/Service/ManagementReportServiceTest.php - - - - message: '#^Cannot access property \$displayName on App\\Model\\Planning\\Assignee\|null\.$#' - identifier: property.nonObject - count: 1 - path: tests/Unit/Service/PlanningServiceTest.php - - - - message: '#^Strict comparison using \=\=\= between ''Widget'' and bool will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: tests/Unit/Service/ProjectBillingServiceTest.php + ignoreErrors: [] diff --git a/src/Entity/ServiceAgreement.php b/src/Entity/ServiceAgreement.php index 0c9e50a29..934dc2318 100644 --- a/src/Entity/ServiceAgreement.php +++ b/src/Entity/ServiceAgreement.php @@ -15,7 +15,7 @@ class ServiceAgreement extends AbstractBaseEntity { #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'serviceAgreements')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: true)] private ?Project $project = null; #[ORM\ManyToOne(targetEntity: Client::class)] diff --git a/src/Form/CybersecurityReportType.php b/src/Form/CybersecurityReportType.php index d0523f9d6..72a025113 100644 --- a/src/Form/CybersecurityReportType.php +++ b/src/Form/CybersecurityReportType.php @@ -11,6 +11,9 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @extends AbstractType + */ class CybersecurityReportType extends AbstractType { private const string DEFAULT_CYBERSECURITY_MILESTONE = 'Cybersikkerhedsaftale'; diff --git a/src/Repository/IssueRepository.php b/src/Repository/IssueRepository.php index f88eb78a6..3320c43f3 100644 --- a/src/Repository/IssueRepository.php +++ b/src/Repository/IssueRepository.php @@ -147,14 +147,20 @@ public function findIssuesInDateRange(string $startDate, string $endDate, ?Worke return $query->getResult(); } + /** + * @return Issue[] + */ public function issuesContainingVersionTitle(string $versionTitle): array { - return $this->createQueryBuilder('issue') + /** @var Issue[] $result */ + $result = $this->createQueryBuilder('issue') ->select('DISTINCT issue') ->innerJoin('issue.versions', 'version') ->andWhere('version.name = :versionTitle') ->setParameter('versionTitle', $versionTitle) ->getQuery() ->getResult(); + + return $result; } } diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index 83193b846..23372fcaa 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -111,6 +111,9 @@ public function getProjectTrackerIdsByDataProviders(array $dataProviders): array return $qb->getQuery()->getSingleColumnResult(); } + /** + * @return int[] + */ public function getProjectIdsWithCybersecurityAgreement(): array { $result = $this->_em->createQueryBuilder() @@ -121,7 +124,7 @@ public function getProjectIdsWithCybersecurityAgreement(): array ->getQuery() ->getScalarResult(); - return array_column($result, 'id'); + return array_map('intval', array_column($result, 'id')); } /** diff --git a/src/Service/CybersecurityReportService.php b/src/Service/CybersecurityReportService.php index 240bedac8..5be558fc1 100644 --- a/src/Service/CybersecurityReportService.php +++ b/src/Service/CybersecurityReportService.php @@ -38,9 +38,23 @@ public function getCybersecurityReport( $issues = $this->issueRepository->issuesContainingVersionTitle($versionTitle); foreach ($issues as $issue) { + $issueId = $issue->getId(); + $issueProjectTrackerId = $issue->getProjectTrackerId(); + $issueName = $issue->getName(); + $issueLink = $issue->getLinkToIssue(); + $projectEntity = $issue->getProject(); + if (null === $issueId + || null === $issueProjectTrackerId + || null === $issueName + || null === $issueLink + || null === $projectEntity + ) { + continue; + } + // Fetch worklogs for this issue restricted to the period $worklogs = $this->worklogRepository->getWorklogsByIssueAndPeriod( - $issue->getId(), + $issueId, $fromDate, $toDate ); @@ -57,9 +71,11 @@ public function getCybersecurityReport( continue; } - $projectEntity = $issue->getProject(); $projectId = $projectEntity->getId(); $projectName = $projectEntity->getName(); + if (null === $projectName) { + continue; + } // Create project entry once if (!isset($report->projects[$projectName])) { @@ -83,11 +99,11 @@ public function getCybersecurityReport( // Create ticket DTO $ticket = new CybersecurityTicketData( - $issue->getId(), - $issue->getProjectTrackerId(), - $issue->getName(), + $issueId, + $issueProjectTrackerId, + $issueName, $totalTicketSpent, - $issue->getLinkToIssue(), + $issueLink, $worklogData ); diff --git a/tests/Integration/Controller/AbstractControllerTestCase.php b/tests/Integration/Controller/AbstractControllerTestCase.php index 05ba3603a..16d707063 100644 --- a/tests/Integration/Controller/AbstractControllerTestCase.php +++ b/tests/Integration/Controller/AbstractControllerTestCase.php @@ -27,14 +27,14 @@ protected function getUserWithRole(string $role): User $userRepository = static::getContainer()->get(UserRepository::class); \assert($userRepository instanceof UserRepository); $user = $userRepository->findOneBy(['email' => $email]); - $this->assertNotNull($user); - if (null === $user) { - throw new \RuntimeException(sprintf('Fixture user %s not found; run `task fixtures`.', $email)); - } + $this->assertInstanceOf(User::class, $user, sprintf('Fixture user %s not found; run `task fixtures`.', $email)); return $user; } + /** + * @param string[] $roles + */ protected function createClientLoggedInAs(array $roles): KernelBrowser { self::ensureKernelShutdown(); @@ -53,6 +53,9 @@ protected function assertAnonymousRedirectsToLogin(string $url): void $this->assertResponseRedirects(); } + /** + * @param string[] $roles + */ protected function assertGrantedFor(string $url, array $roles): void { $client = $this->createClientLoggedInAs($roles); @@ -62,6 +65,9 @@ protected function assertGrantedFor(string $url, array $roles): void $this->assertResponseIsSuccessful(sprintf('Expected 2xx at %s for roles [%s]', $url, implode(',', $roles))); } + /** + * @param string[] $roles + */ protected function assertDeniedFor(string $url, array $roles): void { $client = $this->createClientLoggedInAs($roles); @@ -72,6 +78,9 @@ protected function assertDeniedFor(string $url, array $roles): void /** * Smoke matrix: anonymous redirects, an allowed role gets 200, a denied role gets 403. + * + * @param string[] $allowedRoles + * @param string[] $deniedRoles */ protected function assertSmokeMatrix(string $url, array $allowedRoles, array $deniedRoles): void { diff --git a/tests/Integration/Controller/InvoiceEntryFlowTest.php b/tests/Integration/Controller/InvoiceEntryFlowTest.php index b6c27af12..989b03d58 100644 --- a/tests/Integration/Controller/InvoiceEntryFlowTest.php +++ b/tests/Integration/Controller/InvoiceEntryFlowTest.php @@ -187,19 +187,18 @@ private function setupInvoiceWithManualEntry(bool $recorded = false): array $invoiceRepository = $container->get(InvoiceRepository::class); \assert($invoiceRepository instanceof InvoiceRepository); $invoice = $invoiceRepository->find($invoiceId); - $this->assertNotNull($invoice); + $this->assertInstanceOf(Invoice::class, $invoice); $invoiceEntryRepository = $container->get(InvoiceEntryRepository::class); \assert($invoiceEntryRepository instanceof InvoiceEntryRepository); $entry = $invoiceEntryRepository->find($entryId); - $this->assertNotNull($entry); - $this->assertInstanceOf(Invoice::class, $invoice); $this->assertInstanceOf(InvoiceEntry::class, $entry); return [$invoice, $entry]; } - private function markInvoiceRecorded(int $invoiceId): void + private function markInvoiceRecorded(?int $invoiceId): void { + $this->assertNotNull($invoiceId); $em = static::getContainer()->get(EntityManagerInterface::class); \assert($em instanceof EntityManagerInterface); $invoiceRepository = static::getContainer()->get(InvoiceRepository::class); @@ -212,8 +211,9 @@ private function markInvoiceRecorded(int $invoiceId): void $em->clear(); } - private function reloadEntry(int $id): ?InvoiceEntry + private function reloadEntry(?int $id): ?InvoiceEntry { + $this->assertNotNull($id); $em = static::getContainer()->get(EntityManagerInterface::class); \assert($em instanceof EntityManagerInterface); $em->clear(); diff --git a/tests/Integration/Controller/InvoiceFlowTest.php b/tests/Integration/Controller/InvoiceFlowTest.php index 43b77bb38..60ed43802 100644 --- a/tests/Integration/Controller/InvoiceFlowTest.php +++ b/tests/Integration/Controller/InvoiceFlowTest.php @@ -27,13 +27,17 @@ public function testCreateInvoicePersistsAndRedirectsToEdit(): void $client->submit($form); $this->assertResponseRedirects(); - $this->assertMatchesRegularExpression('#/admin/invoices/\d+/edit$#', $client->getResponse()->headers->get('Location')); + $location = $client->getResponse()->headers->get('Location'); + $this->assertNotNull($location); + $this->assertMatchesRegularExpression('#/admin/invoices/\d+/edit$#', $location); $invoiceRepository = static::getContainer()->get(InvoiceRepository::class); \assert($invoiceRepository instanceof InvoiceRepository); $created = $invoiceRepository->findOneBy(['name' => $name]); $this->assertInstanceOf(Invoice::class, $created); - $this->assertSame($project->getId(), $created->getProject()->getId()); + $createdProject = $created->getProject(); + $this->assertNotNull($createdProject); + $this->assertSame($project->getId(), $createdProject->getId()); $this->assertFalse($created->isRecorded()); } diff --git a/tests/Integration/Controller/InvoiceFullFlowTest.php b/tests/Integration/Controller/InvoiceFullFlowTest.php index 97442848f..ae8a97487 100644 --- a/tests/Integration/Controller/InvoiceFullFlowTest.php +++ b/tests/Integration/Controller/InvoiceFullFlowTest.php @@ -50,6 +50,7 @@ public function testFullInvoiceLifecycle(): void $this->assertNotNull($location); $this->assertMatchesRegularExpression('#/admin/invoices/(\d+)/edit$#', $location, 'Expected redirect to invoice edit.'); preg_match('#/admin/invoices/(\d+)/edit$#', $location, $matches); + \assert(isset($matches[1])); $invoiceId = (int) $matches[1]; // 2. Edit invoice and set all fields. @@ -75,7 +76,6 @@ public function testFullInvoiceLifecycle(): void $this->assertResponseIsSuccessful(); $invoice = $this->reloadInvoice($invoiceId); - $this->assertInstanceOf(Invoice::class, $invoice); $this->assertSame($finalName, $invoice->getName()); $this->assertSame($description, $invoice->getDescription()); $this->assertNotNull($invoice->getClient()); @@ -102,7 +102,6 @@ public function testFullInvoiceLifecycle(): void ); $manualEntry = $this->reloadInvoiceEntry($manualEntryId); - $this->assertInstanceOf(InvoiceEntry::class, $manualEntry); $this->assertSame(InvoiceEntryTypeEnum::MANUAL, $manualEntry->getEntryType()); $this->assertSame($manualProduct, $manualEntry->getProduct()); $this->assertEqualsWithDelta(750.0, $manualEntry->getPrice(), 0.001); @@ -125,7 +124,6 @@ public function testFullInvoiceLifecycle(): void ); $productEntry = $this->reloadInvoiceEntry($productEntryId); - $this->assertInstanceOf(InvoiceEntry::class, $productEntry); $this->assertSame(InvoiceEntryTypeEnum::PRODUCT, $productEntry->getEntryType()); $this->assertEqualsWithDelta(1200.0, $productEntry->getTotalPrice(), 0.001); @@ -144,7 +142,6 @@ public function testFullInvoiceLifecycle(): void ); $worklogEntry = $this->reloadInvoiceEntry($worklogEntryId); - $this->assertInstanceOf(InvoiceEntry::class, $worklogEntry); $this->assertSame(InvoiceEntryTypeEnum::WORKLOG, $worklogEntry->getEntryType()); $this->assertSame(0.0, (float) $worklogEntry->getAmount()); @@ -273,6 +270,7 @@ private function submitNewEntry( if (str_contains($expectedRedirectPattern, '/entries/\d+/edit')) { preg_match('#/entries/(\d+)/edit$#', $location, $matches); + \assert(isset($matches[1])); return (int) $matches[1]; } @@ -283,10 +281,13 @@ private function submitNewEntry( $latest = $repository->findBy([], ['id' => 'DESC'], 1); $this->assertNotEmpty($latest); - return $latest[0]->getId(); + $id = $latest[0]->getId(); + $this->assertNotNull($id); + + return $id; } - private function reloadInvoice(int $id): ?Invoice + private function reloadInvoice(int $id): Invoice { $em = static::getContainer()->get(EntityManagerInterface::class); \assert($em instanceof EntityManagerInterface); @@ -295,10 +296,13 @@ private function reloadInvoice(int $id): ?Invoice $invoiceRepository = static::getContainer()->get(InvoiceRepository::class); \assert($invoiceRepository instanceof InvoiceRepository); - return $invoiceRepository->find($id); + $invoice = $invoiceRepository->find($id); + $this->assertInstanceOf(Invoice::class, $invoice); + + return $invoice; } - private function reloadInvoiceEntry(int $id): ?InvoiceEntry + private function reloadInvoiceEntry(int $id): InvoiceEntry { $em = static::getContainer()->get(EntityManagerInterface::class); \assert($em instanceof EntityManagerInterface); @@ -307,6 +311,9 @@ private function reloadInvoiceEntry(int $id): ?InvoiceEntry $invoiceEntryRepository = static::getContainer()->get(InvoiceEntryRepository::class); \assert($invoiceEntryRepository instanceof InvoiceEntryRepository); - return $invoiceEntryRepository->find($id); + $entry = $invoiceEntryRepository->find($id); + $this->assertInstanceOf(InvoiceEntry::class, $entry); + + return $entry; } } diff --git a/tests/Integration/Controller/ProjectBillingFlowTest.php b/tests/Integration/Controller/ProjectBillingFlowTest.php index 45df55302..d5b35b8fc 100644 --- a/tests/Integration/Controller/ProjectBillingFlowTest.php +++ b/tests/Integration/Controller/ProjectBillingFlowTest.php @@ -29,12 +29,16 @@ public function testCreateProjectBillingPersistsAndRedirectsToEdit(): void $client->submit($form); $this->assertResponseRedirects(); - $this->assertMatchesRegularExpression('#/admin/project-billing/\d+/edit$#', $client->getResponse()->headers->get('Location')); + $location = $client->getResponse()->headers->get('Location'); + $this->assertNotNull($location); + $this->assertMatchesRegularExpression('#/admin/project-billing/\d+/edit$#', $location); $projectBillingRepository = static::getContainer()->get(ProjectBillingRepository::class); \assert($projectBillingRepository instanceof ProjectBillingRepository); $created = $projectBillingRepository->findOneBy(['name' => $name]); $this->assertInstanceOf(ProjectBilling::class, $created); - $this->assertSame($project->getId(), $created->getProject()->getId()); + $createdProject = $created->getProject(); + $this->assertNotNull($createdProject); + $this->assertSame($project->getId(), $createdProject->getId()); } } diff --git a/tests/Integration/Controller/ProjectBillingFullFlowTest.php b/tests/Integration/Controller/ProjectBillingFullFlowTest.php index ae7fda62c..57daa8ba0 100644 --- a/tests/Integration/Controller/ProjectBillingFullFlowTest.php +++ b/tests/Integration/Controller/ProjectBillingFullFlowTest.php @@ -46,12 +46,12 @@ public function testFullProjectBillingLifecycle(): void $location = (string) $client->getResponse()->headers->get('Location'); $this->assertMatchesRegularExpression('#/admin/project-billing/(\d+)/edit$#', $location); preg_match('#/admin/project-billing/(\d+)/edit$#', $location, $matches); + \assert(isset($matches[1])); $projectBillingId = (int) $matches[1]; // The CreateProjectBillingMessage handler runs synchronously (no async routing in test env) // and should have generated invoices from the project's PB-* version issues. $projectBilling = $this->reload($projectBillingId); - $this->assertInstanceOf(ProjectBilling::class, $projectBilling); $this->assertSame($initialName, $projectBilling->getName()); $this->assertFalse((bool) $projectBilling->isRecorded()); $this->assertSame($initialDescription, $projectBilling->getDescription()); @@ -95,9 +95,9 @@ public function testFullProjectBillingLifecycle(): void $projectBilling = $this->reload($projectBillingId); $this->assertSame($updatedName, $projectBilling->getName()); $this->assertSame($updatedDescription, $projectBilling->getDescription()); - $this->assertSame( + $this->assertCount( $initialInvoiceCount, - $projectBilling->getInvoices()->count(), + $projectBilling->getInvoices(), 'Update should regenerate the same set of invoices for unchanged fixture data.', ); foreach ($projectBilling->getInvoices() as $invoice) { @@ -160,7 +160,7 @@ private function submitRecordForm(KernelBrowser $client, int $projectBillingId, $client->submit($form); } - private function reload(int $id): ?ProjectBilling + private function reload(int $id): ProjectBilling { $em = static::getContainer()->get(EntityManagerInterface::class); \assert($em instanceof EntityManagerInterface); @@ -169,12 +169,10 @@ private function reload(int $id): ?ProjectBilling $projectBillingRepository = static::getContainer()->get(ProjectBillingRepository::class); \assert($projectBillingRepository instanceof ProjectBillingRepository); $pb = $projectBillingRepository->find($id); - $this->assertNotNull($pb); - if (null !== $pb) { - $pb->getInvoices()->count(); - foreach ($pb->getInvoices() as $invoice) { - $invoice->getInvoiceEntries()->count(); - } + $this->assertInstanceOf(ProjectBilling::class, $pb); + $pb->getInvoices()->count(); + foreach ($pb->getInvoices() as $invoice) { + $invoice->getInvoiceEntries()->count(); } return $pb; diff --git a/tests/Integration/Repository/ClientRepositoryTest.php b/tests/Integration/Repository/ClientRepositoryTest.php index b5fe0fb9e..c34d0e4df 100644 --- a/tests/Integration/Repository/ClientRepositoryTest.php +++ b/tests/Integration/Repository/ClientRepositoryTest.php @@ -34,7 +34,7 @@ public function testGetFilteredPaginationByName(): void $this->assertGreaterThanOrEqual(2, $result->getTotalItemCount()); foreach ($result as $client) { - $this->assertStringContainsString('client 0', $client->getName()); + $this->assertStringContainsString('client 0', (string) $client->getName()); } } @@ -46,7 +46,7 @@ public function testGetFilteredPaginationByContact(): void $this->assertGreaterThan(0, $result->getTotalItemCount()); foreach ($result as $client) { - $this->assertStringContainsString('Kontakt Kontaktesen 0', $client->getContact()); + $this->assertStringContainsString('Kontakt Kontaktesen 0', (string) $client->getContact()); } } } diff --git a/tests/Integration/Repository/InvoiceRepositoryTest.php b/tests/Integration/Repository/InvoiceRepositoryTest.php index 0d2256358..ac239e4f3 100644 --- a/tests/Integration/Repository/InvoiceRepositoryTest.php +++ b/tests/Integration/Repository/InvoiceRepositoryTest.php @@ -75,7 +75,7 @@ public function testGetFilteredPaginationByQuery(): void $this->assertGreaterThanOrEqual(1, $result->getTotalItemCount()); foreach ($result as $invoice) { - $this->assertStringContainsString('Invoice Beta', $invoice->getName()); + $this->assertStringContainsString('Invoice Beta', (string) $invoice->getName()); } } diff --git a/tests/Integration/Repository/IssueRepositoryTest.php b/tests/Integration/Repository/IssueRepositoryTest.php index 29ba3ddd8..4da94e0bf 100644 --- a/tests/Integration/Repository/IssueRepositoryTest.php +++ b/tests/Integration/Repository/IssueRepositoryTest.php @@ -2,7 +2,6 @@ namespace App\Tests\Integration\Repository; -use App\Entity\Issue; use App\Entity\Version; use App\Entity\WorkerGroup; use App\Enum\IssueStatusEnum; @@ -11,12 +10,10 @@ use App\Repository\ProjectRepository; use App\Repository\VersionRepository; use App\Repository\WorkerGroupRepository; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class IssueRepositoryTest extends KernelTestCase { - private EntityManagerInterface $entityManager; private IssueRepository $repository; private ProjectRepository $projectRepository; @@ -24,9 +21,6 @@ protected function setUp(): void { self::bootKernel(); $container = self::getContainer(); - $entityManager = $container->get(EntityManagerInterface::class); - \assert($entityManager instanceof EntityManagerInterface); - $this->entityManager = $entityManager; $repository = $container->get(IssueRepository::class); \assert($repository instanceof IssueRepository); $this->repository = $repository; @@ -51,7 +45,7 @@ public function testGetFilteredPaginationByName(): void $this->assertGreaterThan(0, $result->getTotalItemCount()); foreach ($result as $issue) { - $this->assertStringContains('issue-0-0', $issue->getName()); + $this->assertStringContains('issue-0-0', (string) $issue->getName()); } } @@ -65,7 +59,9 @@ public function testGetFilteredPaginationByProject(): void $this->assertGreaterThan(0, $result->getTotalItemCount()); foreach ($result as $issue) { - $this->assertEquals($project->getId(), $issue->getProject()->getId()); + $issueProject = $issue->getProject(); + $this->assertNotNull($issueProject); + $this->assertEquals($project->getId(), $issueProject->getId()); } } @@ -132,7 +128,6 @@ public function testIssuesContainingVersionTitle(): void $this->assertNotEmpty($result); foreach ($result as $issue) { - $this->assertInstanceOf(Issue::class, $issue); $versionNames = $issue->getVersions()->map(fn (Version $v) => $v->getName())->toArray(); $this->assertContains('PB-0-0', $versionNames); } @@ -185,7 +180,9 @@ public function testFindIssuesInDateRangeWithProjects(): void $this->assertNotEmpty($result); foreach ($result as $issue) { - $this->assertEquals($project->getId(), $issue->getProject()->getId()); + $issueProject = $issue->getProject(); + $this->assertNotNull($issueProject); + $this->assertEquals($project->getId(), $issueProject->getId()); } } diff --git a/tests/Integration/Repository/ProductRepositoryTest.php b/tests/Integration/Repository/ProductRepositoryTest.php index bb4b271c5..455e70eb8 100644 --- a/tests/Integration/Repository/ProductRepositoryTest.php +++ b/tests/Integration/Repository/ProductRepositoryTest.php @@ -35,7 +35,7 @@ public function testGetFilteredPaginationByName(): void $this->assertGreaterThanOrEqual(1, $result->getTotalItemCount()); foreach ($result as $product) { - $this->assertStringContainsString('Alpha', $product->getName()); + $this->assertStringContainsString('Alpha', (string) $product->getName()); } } @@ -52,7 +52,9 @@ public function testGetFilteredPaginationByProject(): void $this->assertEquals(2, $result->getTotalItemCount()); foreach ($result as $product) { - $this->assertEquals($project->getId(), $product->getProject()->getId()); + $productProject = $product->getProject(); + $this->assertNotNull($productProject); + $this->assertEquals($project->getId(), $productProject->getId()); } } } diff --git a/tests/Integration/Repository/ProjectRepositoryTest.php b/tests/Integration/Repository/ProjectRepositoryTest.php index aa1a7ed99..9c68144b6 100644 --- a/tests/Integration/Repository/ProjectRepositoryTest.php +++ b/tests/Integration/Repository/ProjectRepositoryTest.php @@ -7,21 +7,16 @@ use App\Repository\DataProviderRepository; use App\Repository\ProjectRepository; use App\Service\LeantimeUrlGenerator; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class ProjectRepositoryTest extends KernelTestCase { - private EntityManagerInterface $entityManager; private ProjectRepository $repository; protected function setUp(): void { self::bootKernel(); $container = self::getContainer(); - $entityManager = $container->get(EntityManagerInterface::class); - \assert($entityManager instanceof EntityManagerInterface); - $this->entityManager = $entityManager; $repository = $container->get(ProjectRepository::class); \assert($repository instanceof ProjectRepository); $this->repository = $repository; @@ -76,7 +71,7 @@ public function testGetFilteredPaginationByName(): void $this->assertGreaterThan(0, $result->getTotalItemCount()); foreach ($result as $project) { - $this->assertStringContainsString('project-0-0', $project->getName()); + $this->assertStringContainsString('project-0-0', (string) $project->getName()); } } @@ -89,7 +84,7 @@ public function testGetFilteredPaginationByKey(): void $this->assertGreaterThan(0, $result->getTotalItemCount()); foreach ($result as $project) { - $this->assertStringContainsString('project-1-0', $project->getProjectTrackerKey()); + $this->assertStringContainsString('project-1-0', (string) $project->getProjectTrackerKey()); } } @@ -99,10 +94,10 @@ public function testGetProjectIdsWithCybersecurityAgreement(): void // project-0-0; no other project carries one. $result = $this->repository->getProjectIdsWithCybersecurityAgreement(); - $this->assertIsArray($result); $this->assertCount(1, $result); $project = $this->repository->findOneBy(['name' => 'project-0-0']); + $this->assertNotNull($project); $this->assertEquals([$project->getId()], $result); } diff --git a/tests/Integration/Repository/WorklogRepositoryTest.php b/tests/Integration/Repository/WorklogRepositoryTest.php index 176efdc4c..1c6631cee 100644 --- a/tests/Integration/Repository/WorklogRepositoryTest.php +++ b/tests/Integration/Repository/WorklogRepositoryTest.php @@ -7,12 +7,10 @@ use App\Repository\IssueRepository; use App\Repository\ProjectRepository; use App\Repository\WorklogRepository; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class WorklogRepositoryTest extends KernelTestCase { - private EntityManagerInterface $entityManager; private WorklogRepository $repository; private ProjectRepository $projectRepository; @@ -20,9 +18,6 @@ protected function setUp(): void { self::bootKernel(); $container = self::getContainer(); - $entityManager = $container->get(EntityManagerInterface::class); - \assert($entityManager instanceof EntityManagerInterface); - $this->entityManager = $entityManager; $repository = $container->get(WorklogRepository::class); \assert($repository instanceof WorklogRepository); $this->repository = $repository; @@ -65,7 +60,7 @@ public function testFindByFilterDataByWorker(): void $this->assertNotEmpty($result); foreach ($result as $worklog) { - $this->assertStringContainsString('admin@test.local', $worklog->getWorker()); + $this->assertStringContainsString('admin@test.local', (string) $worklog->getWorker()); } } @@ -246,24 +241,33 @@ public function testGetWorklogsAttachedToInvoiceInDateRange(): void public function testGetWorklogsByIssueAndPeriodReturnsAllWhenNoDates(): void { $issueRepository = self::getContainer()->get(IssueRepository::class); + \assert($issueRepository instanceof IssueRepository); $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $issue = $issueRepository->findOneBy(['project' => $project], ['id' => 'ASC']); + $this->assertNotNull($issue); + $issueId = $issue->getId(); + $this->assertNotNull($issueId); - $result = $this->repository->getWorklogsByIssueAndPeriod($issue->getId(), null, null); + $result = $this->repository->getWorklogsByIssueAndPeriod($issueId, null, null); // Fixtures attach 100 worklogs per issue. $this->assertCount(100, $result); foreach ($result as $worklog) { - $this->assertInstanceOf(Worklog::class, $worklog); - $this->assertSame($issue->getId(), $worklog->getIssue()->getId()); + $worklogIssue = $worklog->getIssue(); + $this->assertNotNull($worklogIssue); + $this->assertSame($issue->getId(), $worklogIssue->getId()); } } public function testGetWorklogsByIssueAndPeriodFiltersByPeriod(): void { $issueRepository = self::getContainer()->get(IssueRepository::class); + \assert($issueRepository instanceof IssueRepository); $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $issue = $issueRepository->findOneBy(['project' => $project], ['id' => 'ASC']); + $this->assertNotNull($issue); + $issueId = $issue->getId(); + $this->assertNotNull($issueId); $year = (new \DateTime())->format('Y'); @@ -271,14 +275,16 @@ public function testGetWorklogsByIssueAndPeriodFiltersByPeriod(): void // limiting to January should match worklogs where (k % 12) == 0, i.e. // k ∈ {0,12,24,36,48,60,72,84,96} — 9 worklogs. $result = $this->repository->getWorklogsByIssueAndPeriod( - $issue->getId(), + $issueId, new \DateTime("$year-01-01"), new \DateTime("$year-01-31"), ); $this->assertCount(9, $result); foreach ($result as $worklog) { - $this->assertSame('01', $worklog->getStarted()->format('m')); + $started = $worklog->getStarted(); + $this->assertNotNull($started); + $this->assertSame('01', $started->format('m')); } } @@ -292,21 +298,27 @@ public function testGetWorklogsByIssueAndPeriodEmptyForUnknownIssue(): void public function testGetWorklogsByIssueAndPeriodReturnsOrderedByStarted(): void { $issueRepository = self::getContainer()->get(IssueRepository::class); + \assert($issueRepository instanceof IssueRepository); $project = $this->projectRepository->findOneBy(['name' => 'project-0-0']); $issue = $issueRepository->findOneBy(['project' => $project], ['id' => 'ASC']); + $this->assertNotNull($issue); + $issueId = $issue->getId(); + $this->assertNotNull($issueId); - $result = $this->repository->getWorklogsByIssueAndPeriod($issue->getId(), null, null); + $result = $this->repository->getWorklogsByIssueAndPeriod($issueId, null, null); $previous = null; foreach ($result as $worklog) { + $started = $worklog->getStarted(); + $this->assertNotNull($started); if (null !== $previous) { $this->assertGreaterThanOrEqual( $previous->getTimestamp(), - $worklog->getStarted()->getTimestamp(), + $started->getTimestamp(), 'Worklogs should be returned ordered by started ASC.' ); } - $previous = $worklog->getStarted(); + $previous = $started; } } } diff --git a/tests/Integration/Service/CybersecurityReportServiceTest.php b/tests/Integration/Service/CybersecurityReportServiceTest.php index c3df8f666..41e440ae7 100644 --- a/tests/Integration/Service/CybersecurityReportServiceTest.php +++ b/tests/Integration/Service/CybersecurityReportServiceTest.php @@ -3,7 +3,6 @@ namespace App\Tests\Integration\Service; use App\Model\Reports\CybersecurityProjectData; -use App\Model\Reports\CybersecurityReportData; use App\Service\CybersecurityReportService; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -14,7 +13,9 @@ class CybersecurityReportServiceTest extends KernelTestCase protected function setUp(): void { self::bootKernel(); - $this->service = self::getContainer()->get(CybersecurityReportService::class); + $service = self::getContainer()->get(CybersecurityReportService::class); + \assert($service instanceof CybersecurityReportService); + $this->service = $service; } public function testGetDefaultFromDateIsFirstOfCurrentMonth(): void @@ -33,7 +34,6 @@ public function testGetCybersecurityReportUnknownVersionReturnsEmpty(): void { $report = $this->service->getCybersecurityReport(null, null, 'no-such-version'); - $this->assertInstanceOf(CybersecurityReportData::class, $report); $this->assertSame([], $report->projects); $this->assertSame(0.0, $report->totalSpent); } diff --git a/tests/Integration/Service/InvoicingRateReportServiceTest.php b/tests/Integration/Service/InvoicingRateReportServiceTest.php index 610bbeaa4..5d3c2a805 100644 --- a/tests/Integration/Service/InvoicingRateReportServiceTest.php +++ b/tests/Integration/Service/InvoicingRateReportServiceTest.php @@ -44,6 +44,6 @@ public function testGetInvoicingRateReportProducesPeriodsForEveryIncludedWorker( $worker = $report->workers->first(); $this->assertGreaterThanOrEqual(0.0, $worker->average); $this->assertLessThanOrEqual(100.0, $worker->average); - $this->assertSame($report->period->count(), $worker->dataByPeriod->count()); + $this->assertCount($report->period->count(), $worker->dataByPeriod); } } diff --git a/tests/Integration/Service/LeantimeApiServiceTest.php b/tests/Integration/Service/LeantimeApiServiceTest.php index d3da7aa2b..64d644482 100644 --- a/tests/Integration/Service/LeantimeApiServiceTest.php +++ b/tests/Integration/Service/LeantimeApiServiceTest.php @@ -115,7 +115,7 @@ public function testUpdate(): void $this->assertEquals($before + 2, $after); $project = $projectRepository->findOneBy(['projectTrackerId' => 50]); $this->assertNotNull($project); - \assert(null !== $project && null !== $project->getSourceModifiedDate()); + \assert(null !== $project->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $project->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter $service->updateAsJob(Project::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); @@ -123,7 +123,7 @@ public function testUpdate(): void $this->assertEquals($before + 2, $after); $project = $projectRepository->findOneBy(['projectTrackerId' => 50]); $this->assertNotNull($project); - \assert(null !== $project && null !== $project->getSourceModifiedDate()); + \assert(null !== $project->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $project->getSourceModifiedDate()->getTimestamp()); // Milestones @@ -134,7 +134,7 @@ public function testUpdate(): void $this->assertEquals($before + 2, $after); $version = $versionRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); $this->assertNotNull($version); - \assert(null !== $version && null !== $version->getSourceModifiedDate()); + \assert(null !== $version->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $version->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter $service->updateAsJob(Version::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); @@ -142,7 +142,7 @@ public function testUpdate(): void $this->assertEquals($before + 2, $after); $version = $versionRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); $this->assertNotNull($version); - \assert(null !== $version && null !== $version->getSourceModifiedDate()); + \assert(null !== $version->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $version->getSourceModifiedDate()->getTimestamp()); // Tickets @@ -153,7 +153,7 @@ public function testUpdate(): void $this->assertEquals($before + 2, $after); $issue = $issueRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); $this->assertNotNull($issue); - \assert(null !== $issue && null !== $issue->getSourceModifiedDate()); + \assert(null !== $issue->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $issue->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter $service->updateAsJob(Issue::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); @@ -161,7 +161,7 @@ public function testUpdate(): void $this->assertEquals($before + 2, $after); $issue = $issueRepository->findOneBy(['projectTrackerId' => 10, 'dataProvider' => $dataProvider]); $this->assertNotNull($issue); - \assert(null !== $issue && null !== $issue->getSourceModifiedDate()); + \assert(null !== $issue->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $issue->getSourceModifiedDate()->getTimestamp()); // Timesheets @@ -172,7 +172,7 @@ public function testUpdate(): void $this->assertEquals($before + 2, $after); $worklog = $worklogRepository->findOneBy(['worklogId' => 1, 'dataProvider' => $dataProvider]); $this->assertNotNull($worklog); - \assert(null !== $worklog && null !== $worklog->getSourceModifiedDate()); + \assert(null !== $worklog->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2024-10-03T13:47:30.000000Z'))->getTimestamp(), $worklog->getSourceModifiedDate()->getTimestamp()); // Repeat process to test that no extra entries are added and test modifiedAfter $service->updateAsJob(Worklog::class, 0, 100, $dataProviderId, [], false, new \DateTime('2025-01-01')); @@ -180,7 +180,7 @@ public function testUpdate(): void $this->assertEquals($before + 2, $after); $worklog = $worklogRepository->findOneBy(['worklogId' => 1, 'dataProvider' => $dataProvider]); $this->assertNotNull($worklog); - \assert(null !== $worklog && null !== $worklog->getSourceModifiedDate()); + \assert(null !== $worklog->getSourceModifiedDate()); $this->assertEquals((new \DateTime('2025-10-03T13:47:30.000000Z'))->getTimestamp(), $worklog->getSourceModifiedDate()->getTimestamp()); } @@ -399,17 +399,14 @@ public function testDeleted(): void $project1 = $projectRepository->find($projectId1); $this->assertNotNull($project1); - \assert(null !== $project1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $project1->getSourceDeletedDate()); $issue1 = $issueRepository->find($issueId1); $this->assertNotNull($issue1); - \assert(null !== $issue1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $issue1->getSourceDeletedDate()); $worklog1 = $worklogRepository->find($worklogId1); $this->assertNotNull($worklog1); - \assert(null !== $worklog1); $this->assertEquals(new \DateTime('2025-10-24T11:36:08.000000Z'), $worklog1->getSourceDeletedDate()); } diff --git a/tests/Integration/Service/ProjectBillingServiceTest.php b/tests/Integration/Service/ProjectBillingServiceTest.php index 4b8caa8ec..2c6b63d8a 100644 --- a/tests/Integration/Service/ProjectBillingServiceTest.php +++ b/tests/Integration/Service/ProjectBillingServiceTest.php @@ -35,7 +35,6 @@ public function testGetIssuesNotIncludedInProjectBilling(): void $project = $projectRepository->findOneBy([], ['id' => 'asc']); $this->assertNotNull($project); - \assert(null !== $project); $periodStart = (new \DateTime())->sub(new \DateInterval('P1D')); $periodEnd = (new \DateTime())->add(new \DateInterval('P1D')); diff --git a/tests/Integration/Service/WorkloadReportServiceTest.php b/tests/Integration/Service/WorkloadReportServiceTest.php index af8da211f..e44d27686 100644 --- a/tests/Integration/Service/WorkloadReportServiceTest.php +++ b/tests/Integration/Service/WorkloadReportServiceTest.php @@ -37,7 +37,7 @@ public function testGetWorkloadReportProducesPeriodsForEveryIncludedWorker(): vo /** @var WorkloadReportWorker $worker */ $worker = $report->workers->first(); - $this->assertSame($report->period->count(), $worker->loggedPercentage->count()); + $this->assertCount($report->period->count(), $worker->loggedPercentage); $this->assertGreaterThanOrEqual(0.0, $worker->average); } } diff --git a/tests/Unit/Service/BillingServiceTest.php b/tests/Unit/Service/BillingServiceTest.php index 8ed3c822f..5624d6d40 100644 --- a/tests/Unit/Service/BillingServiceTest.php +++ b/tests/Unit/Service/BillingServiceTest.php @@ -596,8 +596,8 @@ public function testGenerateSpreadsheetCsvResponseHeaders(): void $response = $this->billingService->generateSpreadsheetCsvResponse([1]); $this->assertSame('text/csv', $response->headers->get('Content-Type')); - $this->assertStringContainsString('attachment', $response->headers->get('Content-Disposition')); - $this->assertStringContainsString('.csv', $response->headers->get('Content-Disposition')); + $this->assertStringContainsString('attachment', (string) $response->headers->get('Content-Disposition')); + $this->assertStringContainsString('.csv', (string) $response->headers->get('Content-Disposition')); } public function testGenerateSpreadsheetCsvResponseUsesSemicolonDelimiter(): void @@ -625,7 +625,7 @@ public function testGenerateSpreadsheetCsvResponseUsesSemicolonDelimiter(): void $response = $this->billingService->generateSpreadsheetCsvResponse([1]); - $content = $response->getContent(); + $content = (string) $response->getContent(); $this->assertStringContainsString(';', $content); } diff --git a/tests/Unit/Service/CybersecurityReportServiceTest.php b/tests/Unit/Service/CybersecurityReportServiceTest.php index c267a97ec..06e0f1b4a 100644 --- a/tests/Unit/Service/CybersecurityReportServiceTest.php +++ b/tests/Unit/Service/CybersecurityReportServiceTest.php @@ -5,18 +5,18 @@ use App\Entity\Issue; use App\Entity\Project; use App\Entity\Worklog; -use App\Model\Reports\CybersecurityReportData; use App\Repository\IssueRepository; use App\Repository\ProjectRepository; use App\Repository\WorklogRepository; use App\Service\CybersecurityReportService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class CybersecurityReportServiceTest extends TestCase { - private IssueRepository $issueRepository; - private WorklogRepository $worklogRepository; - private ProjectRepository $projectRepository; + private IssueRepository&MockObject $issueRepository; + private WorklogRepository&MockObject $worklogRepository; + private ProjectRepository&MockObject $projectRepository; private CybersecurityReportService $service; protected function setUp(): void @@ -109,7 +109,6 @@ public function testReturnsEmptyReportWhenNoIssues(): void $result = $this->service->getCybersecurityReport(null, null, 'Cybersikkerhedsaftale'); - $this->assertInstanceOf(CybersecurityReportData::class, $result); $this->assertSame([], $result->projects); $this->assertSame(0.0, $result->totalSpent); } diff --git a/tests/Unit/Service/ForecastReportServiceTest.php b/tests/Unit/Service/ForecastReportServiceTest.php index ebf1449c2..44986c62a 100644 --- a/tests/Unit/Service/ForecastReportServiceTest.php +++ b/tests/Unit/Service/ForecastReportServiceTest.php @@ -105,7 +105,9 @@ public function testAggregatesProjectHours(): void $this->assertEqualsWithDelta(2.0, $result->totalInvoiced, 0.001); $this->assertEqualsWithDelta(0.0, $result->totalInvoicedAndRecorded, 0.001); $this->assertArrayHasKey(1, $result->projects); - $this->assertEqualsWithDelta(2.0, $result->projects[1]->invoiced, 0.001); + $projectData = $result->projects[1]; + $this->assertNotNull($projectData); + $this->assertEqualsWithDelta(2.0, $projectData->invoiced, 0.001); } public function testBilledWorklogsCountAsRecorded(): void @@ -185,6 +187,7 @@ public function testWorkerNameMapping(): void ); $projectData = $result->projects[1]; + $this->assertNotNull($projectData); $issueData = $projectData->issues['[no tag]']; $versionData = $issueData->versions['[no version]']; $worklogData = $versionData->worklogs[1]; diff --git a/tests/Unit/Service/HourReportServiceTest.php b/tests/Unit/Service/HourReportServiceTest.php index 4cf57e4c7..570be5d53 100644 --- a/tests/Unit/Service/HourReportServiceTest.php +++ b/tests/Unit/Service/HourReportServiceTest.php @@ -204,6 +204,7 @@ public function testGetHourReportGroupsByEpic(): void $this->assertTrue($result->projectTags->containsKey('Backend Work')); $tag = $result->projectTags->get('Backend Work'); + $this->assertNotNull($tag); $this->assertSame('Backend Work', $tag->tag); } @@ -332,6 +333,7 @@ public function testGetHourReportMultipleIssuesSameEpicAggregated(): void $this->assertEqualsWithDelta(8.0, $result->projectTotalEstimated, 0.001); $tag = $result->projectTags->get('Frontend'); + $this->assertNotNull($tag); $this->assertEqualsWithDelta(8.0, $tag->totalEstimated, 0.001); $this->assertEqualsWithDelta(3.0, $tag->totalSpent, 0.001); $this->assertCount(2, $tag->projectTickets); diff --git a/tests/Unit/Service/InvoicingRateReportServiceTest.php b/tests/Unit/Service/InvoicingRateReportServiceTest.php index 53744da50..1f00a63b2 100644 --- a/tests/Unit/Service/InvoicingRateReportServiceTest.php +++ b/tests/Unit/Service/InvoicingRateReportServiceTest.php @@ -34,7 +34,7 @@ protected function setUp(): void public function testMonthPeriodReturns12Periods(): void { $this->workerRepository->method('findAllIncludedInReports')->willReturn([]); - $this->dateTimeHelper->method('getMonthName')->willReturnCallback(fn ($m) => date('F', mktime(0, 0, 0, $m, 10))); + $this->dateTimeHelper->method('getMonthName')->willReturnCallback(fn ($m) => date('F', (int) mktime(0, 0, 0, $m, 10))); $result = $this->service->getInvoicingRateReport(2024, PeriodTypeEnum::MONTH); @@ -87,7 +87,7 @@ public function testPercentageCalculation(): void $this->worklogRepository->method('findBillableWorklogsByWorkerAndDateRange')->willReturn([]); $this->worklogRepository->method('findBilledWorklogsByWorkerAndDateRange')->willReturn([]); - $this->dateTimeHelper->method('getMonthName')->willReturnCallback(fn ($m) => date('F', mktime(0, 0, 0, $m, 10))); + $this->dateTimeHelper->method('getMonthName')->willReturnCallback(fn ($m) => date('F', (int) mktime(0, 0, 0, $m, 10))); $this->dateTimeHelper->method('getFirstAndLastDateOfMonth')->willReturn([ 'dateFrom' => new \DateTime('2024-01-01'), 'dateTo' => new \DateTime('2024-01-31'), @@ -99,6 +99,7 @@ public function testPercentageCalculation(): void // With 0 logged hours, average should be 0 $workerData = $result->workers->first(); + $this->assertNotFalse($workerData); $this->assertEqualsWithDelta(0.0, $workerData->average, 0.001); } diff --git a/tests/Unit/Service/ManagementReportServiceTest.php b/tests/Unit/Service/ManagementReportServiceTest.php index 77a95b90b..bed57ac99 100644 --- a/tests/Unit/Service/ManagementReportServiceTest.php +++ b/tests/Unit/Service/ManagementReportServiceTest.php @@ -47,8 +47,8 @@ public function testGenerateSpreadsheetCsvResponseReturnsStreamedResponse(): voi $response = $this->service->generateSpreadsheetCsvResponse($groupedInvoices, $dateInterval); $this->assertInstanceOf(StreamedResponse::class, $response); - $this->assertStringContainsString('application/vnd.ms-excel', $response->headers->get('Content-Type')); - $this->assertStringContainsString('management-report', $response->headers->get('Content-Disposition')); + $this->assertStringContainsString('application/vnd.ms-excel', (string) $response->headers->get('Content-Type')); + $this->assertStringContainsString('management-report', (string) $response->headers->get('Content-Disposition')); } public function testGenerateSpreadsheetCsvResponseCalculatesQuarterSums(): void diff --git a/tests/Unit/Service/PlanningServiceTest.php b/tests/Unit/Service/PlanningServiceTest.php index 5da4d78fb..3001dd873 100644 --- a/tests/Unit/Service/PlanningServiceTest.php +++ b/tests/Unit/Service/PlanningServiceTest.php @@ -215,6 +215,7 @@ public function testUnassignedIssues(): void $this->assertTrue($result->assignees->containsKey('unassigned')); $assignee = $result->assignees->get('unassigned'); + $this->assertNotNull($assignee); $this->assertSame('Unassigned', $assignee->displayName); } diff --git a/tests/Unit/Service/ProjectBillingServiceTest.php b/tests/Unit/Service/ProjectBillingServiceTest.php index 59aae2bda..f7f3c51da 100644 --- a/tests/Unit/Service/ProjectBillingServiceTest.php +++ b/tests/Unit/Service/ProjectBillingServiceTest.php @@ -420,7 +420,7 @@ public function testCreateProjectBillingCreatesProductInvoiceEntries(): void $this->service->createProjectBilling(1); - $productEntries = array_filter($persistedEntities, fn ($e) => $e instanceof InvoiceEntry && 'Widget' === str_contains($e->getProduct() ?? '', 'Widget')); + $productEntries = array_filter($persistedEntities, fn ($e) => $e instanceof InvoiceEntry && str_contains($e->getProduct() ?? '', 'Widget')); $this->assertNotEmpty($projectBilling->getInvoices()); } From d19f4349b637e5ad81fbd9e8c494025a8877a2ca Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:39:29 +0200 Subject: [PATCH 22/23] 7273: Shortened changelog entry --- CHANGELOG.md | 77 ++++++++++++++++------------------------------------ 1 file changed, 23 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5337f3d53..ab6cf443b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,60 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] * [PR-308](https://github.com/itk-dev/economics/pull/308) - * PHPStan baseline reduced from 530 → 0 in `src/` and the pre-merge `tests/`, - then bounced to 444 when develop's new integration/unit test suites - arrived, and is now at 91. The first pass cleaned `src/` to zero - suppressions via PHPDoc/typehint annotations, generic-class shapes, - dropped scaffold `@method find*` docblocks on 17 repositories, and - null-guards. Post-merge passes intersected PHPUnit mock properties - with `MockObject` across 16 test files (−257 entries); dropped - redundant `assertInstanceOf` / `assertIsArray` / `assertIsFloat` / - `assertArrayHasKey` calls whose target type was already statically - proved across 24 test files (−46 entries); and narrowed - `Container::get()` results in integration tests with inline - `\assert($x instanceof T)` (and removed redundant `/** @var */` - PHPDoc lines that drift silently when the underlying types change) - across 28 test files (−50 entries). - Items below are behavior- or schema-affecting and worth calling out. - * Migration `Version20260517131038`: made 18 columns nullable on synced - entities (`issue`, `project`, `version`, `worklog`) and three DateTime - fields (`project_billing.period_{start,end}`, `service_agreement.valid_from`) - to match property types. - * Migration `Version20260517151632`: relaxed 11 `NOT NULL` ManyToOne FKs to - `NULL` (`cybersecurity_agreement`, `invoice_entry`, `issue_product`, - `product`, `project_billing`, `service_agreement`, `version`, `worklog`). - Application-layer validation (`#[Assert\NotNull]`, form validators) - continues to enforce required-ness. - * Replaced `?T $prop = null` with non-null defaults on 25 app-managed entity - properties to satisfy the existing `NOT NULL` columns: `''` for strings, - `false` for bools, `0`/`0.0` for numerics, `'0'` for `Product::$price` - (decimal), `HostingProviderEnum::ADM` for `ServiceAgreement::$hostingProvider`. - `Worker::__toString()` and `WorkerGroup::__toString()` switched from `??` - to `?:` to preserve empty-string-falls-through behavior. - * Real bugs surfaced and fixed during the cleanup: - * `ForecastReportService` was calling non-existent `Epic::getName()` (epic - tags rendered empty in the forecast); now uses `Epic::getTitle()`. - * `SubscriptionHandlerService::getVersion()` was looking up `Version` by a - non-existent `versionId` field; now uses `find($versionId)`. - * `DataProviderService::setTimeSpentSeconds()` was passing `float` through - to an `int` setter (silent precision loss); explicit `(int)` cast added. - * Report services (`ForecastReportService`, - `BillableUnbilledHoursReportService`, `InvoicingRateReportService`) - skip worklogs with a null project or issue instead of crashing on - chained getters. - * `SubscriptionController::check()` returns HTTP 400 on null - `User::getEmail()` rather than passing `?string` downstream; - `User::getUserIdentifier()` throws on empty email - (Symfony's `UserInterface` requires `non-empty-string`). - * `LeantimeApiService` switched from `json_decode($json, null)` to - `json_decode($json, true)`; all `$data->property` accesses converted to - array access. Wire shape unchanged. - * Misc dead code removed: `readonly string $id` on three report-data models - (never assigned, never read), an unreachable `break;` after `throw`, a - `null === $row` check on a `Row` iterator, an unused `PaginatorInterface - $paginator` constructor arg in `SubscriptionRepository`. Fixed an - `Epic::getIssues()` PHPDoc typo (`Collection` → - `Collection`). + * PHPStan baseline: 530 → 0 in `src/`, bounced to 444 after develop's test suites merged, + now 91. Items below are behavior- or schema-affecting and worth calling out. + * Migration `Version20260517131038`: 18 columns made nullable on synced entities + (`issue`, `project`, `version`, `worklog`) plus 3 DateTime fields, to match property types. + * Migration `Version20260517151632`: 11 `NOT NULL` ManyToOne FKs relaxed to `NULL`; + application-layer `#[Assert\NotNull]` / form validators still enforce required-ness. + * Replaced `?T $prop = null` with non-null defaults on 25 entity properties to satisfy + `NOT NULL` columns; `Worker`/`WorkerGroup` `__toString()` switched from `??` to `?:`. + * `ForecastReportService` rendered empty epic tags — called non-existent `Epic::getName()`; + now uses `Epic::getTitle()`. + * `SubscriptionHandlerService::getVersion()` looked up `Version` by non-existent + `versionId` field; now uses `find($versionId)`. + * `DataProviderService::setTimeSpentSeconds()` silently lost precision passing `float` to an + `int` setter; explicit `(int)` cast added. + * Report services (`ForecastReportService`, `BillableUnbilledHoursReportService`, + `InvoicingRateReportService`) now skip worklogs with null project/issue instead of crashing. + * `SubscriptionController::check()` returns HTTP 400 on null `User::getEmail()`; + `User::getUserIdentifier()` throws on empty email (Symfony requires `non-empty-string`). + * `LeantimeApiService` switched from `json_decode($json, null)` to `json_decode($json, true)`; + all `$data->property` accesses converted to array access. Wire shape unchanged. + * Removed dead code: `readonly string $id` on 3 report-data models, unreachable `break;` after + `throw`, null check on `Row` iterator, unused `$paginator` arg in `SubscriptionRepository`. + * Fixed `Epic::getIssues()` PHPDoc typo (`Collection` → `Collection`). ## [3.5.0] - 2026-05-26 From 698f1858494645212417563e0480e959d50f148a Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:10:18 +0200 Subject: [PATCH 23/23] 7273: Cleaned up changelog entry --- CHANGELOG.md | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab6cf443b..f459b87ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,29 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] * [PR-308](https://github.com/itk-dev/economics/pull/308) - * PHPStan baseline: 530 → 0 in `src/`, bounced to 444 after develop's test suites merged, - now 91. Items below are behavior- or schema-affecting and worth calling out. - * Migration `Version20260517131038`: 18 columns made nullable on synced entities - (`issue`, `project`, `version`, `worklog`) plus 3 DateTime fields, to match property types. - * Migration `Version20260517151632`: 11 `NOT NULL` ManyToOne FKs relaxed to `NULL`; - application-layer `#[Assert\NotNull]` / form validators still enforce required-ness. - * Replaced `?T $prop = null` with non-null defaults on 25 entity properties to satisfy - `NOT NULL` columns; `Worker`/`WorkerGroup` `__toString()` switched from `??` to `?:`. - * `ForecastReportService` rendered empty epic tags — called non-existent `Epic::getName()`; - now uses `Epic::getTitle()`. - * `SubscriptionHandlerService::getVersion()` looked up `Version` by non-existent - `versionId` field; now uses `find($versionId)`. - * `DataProviderService::setTimeSpentSeconds()` silently lost precision passing `float` to an - `int` setter; explicit `(int)` cast added. - * Report services (`ForecastReportService`, `BillableUnbilledHoursReportService`, - `InvoicingRateReportService`) now skip worklogs with null project/issue instead of crashing. - * `SubscriptionController::check()` returns HTTP 400 on null `User::getEmail()`; - `User::getUserIdentifier()` throws on empty email (Symfony requires `non-empty-string`). - * `LeantimeApiService` switched from `json_decode($json, null)` to `json_decode($json, true)`; - all `$data->property` accesses converted to array access. Wire shape unchanged. - * Removed dead code: `readonly string $id` on 3 report-data models, unreachable `break;` after - `throw`, null check on `Row` iterator, unused `$paginator` arg in `SubscriptionRepository`. - * Fixed `Epic::getIssues()` PHPDoc typo (`Collection` → `Collection`). + * PHPStan baseline cleanup in src/ (530 → 0); behavior- and schema-affecting items below. + * Migrations Version20260517131038 and Version20260517151632: relaxed 18 columns and + 11 ManyToOne FKs on synced entities (issue, project, version, worklog) to nullable + to match property types; application-layer asserts/form validators still enforce required-ness. + * Fixed report services crashing on worklogs with null project/issue and rendering empty epic + tags (Epic::getName() → getTitle()); fixed SubscriptionHandlerService::getVersion() + lookup by non-existent field. + * LeantimeApiService switched from object to array json_decode; wire shape unchanged. ## [3.5.0] - 2026-05-26