Skip to content

declare strict_types in all files#474

Open
Art4 wants to merge 2 commits intov2.xfrom
464-strict-types
Open

declare strict_types in all files#474
Art4 wants to merge 2 commits intov2.xfrom
464-strict-types

Conversation

@Art4
Copy link
Copy Markdown
Collaborator

@Art4 Art4 commented Apr 5, 2026

closes #464.

Summary by CodeRabbit

  • Chores
    • Enabled strict type checking across the codebase to improve type safety and reliability.
    • Harmonized legacy listing calls to ensure boolean flags are passed consistently.
  • Bug Fixes
    • Improved XML serialization to consistently handle values as strings, reducing edge-case parsing issues.

@Art4 Art4 added this to the v2.10.0 milestone Apr 5, 2026
@Art4 Art4 self-assigned this Apr 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

Adds PHP strict typing across the repository by enabling declare(strict_types=1); via php-cs-fixer and inserting the directive into many source and test files; XmlSerializer also narrows exception catches and adds explicit casts for XML values.

Changes

Cohort / File(s) Summary
PHP-CS-Fixer Configuration
\.php-cs-fixer.dist.php
Added the declare_strict_types rule to enforce declare(strict_types=1); across files.
API classes
src/Redmine/Api/.../*.php, src/Redmine/Api.php
Inserted declare(strict_types=1); into API files; several deprecated listing(...) wrappers cast $forceUpdate to (bool) before calling typed doListing(...).
Client & Traits
src/Redmine/Client/Client.php, src/Redmine/Client/ClientApiTrait.php
Added declare(strict_types=1);.
Exceptions
src/Redmine/Exception.php, src/Redmine/Exception/*
Added declare(strict_types=1); to exception classes.
Serializers (JSON/Path)
src/Redmine/Serializer/JsonSerializer.php, src/Redmine/Serializer/PathSerializer.php
Added declare(strict_types=1);.
XML Serializer
src/Redmine/Serializer/XmlSerializer.php
Added declare(strict_types=1);, changed catch Throwablecatch Exception (use Exception), and added explicit casts for rootElementName, id, custom field id and value when creating XML nodes.
Tests
tests/Unit/... (many files, tests across Api/Client/Exception/...)
Added declare(strict_types=1); to unit tests. Several tests that previously called listing(true) now call listing(1) with @phpstan-ignore comments to suppress static-analysis warnings where an int is passed for a boolean parameter.
Misc (other small files)
src/... (client/serializer/other files listed in diff)
Added file-level declare(strict_types=1); where present in diff.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Housekeeping for v2.10.0-dev #473 — touches php-cs-fixer and some of the same files; related to adding strict types / style changes.
  • Issue #464 — request to "Declare strict types in all files" which this change implements.

🐰 I nibble bugs, I tidy code,
Strict types hop down every road,
From src to tests I softly tread,
Safe and tidy — carrots fed! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'declare strict_types in all files' is concise, clear, and accurately describes the main objective of the changeset.
Linked Issues check ✅ Passed All changes in the PR fulfill the requirement from issue #464 to add declare(strict_types=1) to all files using the php-cs-fixer rule, with necessary boolean type casts in deprecated methods.
Out of Scope Changes check ✅ Passed All changes are scoped to adding declare(strict_types=1) directives and fixing necessary type casts; one file (XmlSerializer.php) includes exception type narrowing which is closely related to strict types.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 464-strict-types

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.72%. Comparing base (9e16d0e) to head (47748bd).

Additional details and impacted files
@@            Coverage Diff            @@
##               v2.x     #474   +/-   ##
=========================================
  Coverage     98.72%   98.72%           
  Complexity      783      783           
=========================================
  Files            29       29           
  Lines          2275     2275           
=========================================
  Hits           2246     2246           
  Misses           29       29           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Art4 Art4 marked this pull request as ready for review April 5, 2026 14:54
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Redmine/Api/CustomField.php`:
- Line 3: The listing() method currently forwards an untyped $forceUpdate to
doListing(bool $forceUpdate, ...) under declare(strict_types=1), causing a
TypeError when callers pass non-bool values; update listing() (in
CustomField.php) to coerce or validate $forceUpdate to a bool before calling
doListing — e.g., explicitly cast (bool)$forceUpdate or perform is_bool check
and convert — so the forwarded argument matches doListing(bool $forceUpdate,
...) and avoids strict-type failures.

In `@src/Redmine/Api/Search.php`:
- Line 3: The search() method currently forwards an untyped $query to
listByQuery(string $query, ...) under strict_types, causing a TypeError; fix
this by explicitly casting $query to string when calling listByQuery (e.g., pass
(string)$query) inside the search() implementation so the call matches
listByQuery's signature and avoids uncaught Error types; update any related call
sites in the same method (references: search(), listByQuery()) accordingly.

In `@src/Redmine/Serializer/XmlSerializer.php`:
- Line 80: The catch blocks in XmlSerializer (the catch clauses currently
written as "catch (Exception $e)") are too narrow and will let TypeError/other
Error subclasses escape under strict_types; update both catch clauses in the
XmlSerializer class (the blocks that currently catch Exception and wrap/rethrow
as SerializerException) to catch Throwable instead, preserving the existing
handling logic (wrap/throw SerializerException with the same message/context) so
TypeError and other Errors are captured and converted to SerializerException as
callers expect.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34594283-87b3-4479-a24f-4084a0dace48

📥 Commits

Reviewing files that changed from the base of the PR and between 9e16d0e and ec15698.

📒 Files selected for processing (144)
  • .php-cs-fixer.dist.php
  • src/Redmine/Api.php
  • src/Redmine/Api/AbstractApi.php
  • src/Redmine/Api/Attachment.php
  • src/Redmine/Api/CustomField.php
  • src/Redmine/Api/Group.php
  • src/Redmine/Api/Issue.php
  • src/Redmine/Api/IssueCategory.php
  • src/Redmine/Api/IssuePriority.php
  • src/Redmine/Api/IssueRelation.php
  • src/Redmine/Api/IssueStatus.php
  • src/Redmine/Api/Membership.php
  • src/Redmine/Api/News.php
  • src/Redmine/Api/Project.php
  • src/Redmine/Api/Query.php
  • src/Redmine/Api/Role.php
  • src/Redmine/Api/Search.php
  • src/Redmine/Api/TimeEntry.php
  • src/Redmine/Api/TimeEntryActivity.php
  • src/Redmine/Api/Tracker.php
  • src/Redmine/Api/User.php
  • src/Redmine/Api/Version.php
  • src/Redmine/Api/Wiki.php
  • src/Redmine/Client/Client.php
  • src/Redmine/Client/ClientApiTrait.php
  • src/Redmine/Exception.php
  • src/Redmine/Exception/ClientException.php
  • src/Redmine/Exception/InvalidApiNameException.php
  • src/Redmine/Exception/InvalidParameterException.php
  • src/Redmine/Exception/MissingParameterException.php
  • src/Redmine/Exception/SerializerException.php
  • src/Redmine/Serializer/JsonSerializer.php
  • src/Redmine/Serializer/PathSerializer.php
  • src/Redmine/Serializer/XmlSerializer.php
  • tests/Unit/Api/AbstractApiTest.php
  • tests/Unit/Api/Attachment/DownloadTest.php
  • tests/Unit/Api/Attachment/FromHttpClientTest.php
  • tests/Unit/Api/Attachment/ShowTest.php
  • tests/Unit/Api/Attachment/UpdateTest.php
  • tests/Unit/Api/Attachment/UploadTest.php
  • tests/Unit/Api/AttachmentTest.php
  • tests/Unit/Api/CustomField/FromHttpClientTest.php
  • tests/Unit/Api/CustomField/ListTest.php
  • tests/Unit/Api/CustomFieldTest.php
  • tests/Unit/Api/Group/AddUserTest.php
  • tests/Unit/Api/Group/FromHttpClientTest.php
  • tests/Unit/Api/Group/ListTest.php
  • tests/Unit/Api/Group/ShowTest.php
  • tests/Unit/Api/Issue/AddWatcherTest.php
  • tests/Unit/Api/Issue/AttachManyTest.php
  • tests/Unit/Api/Issue/AttachTest.php
  • tests/Unit/Api/Issue/CreateTest.php
  • tests/Unit/Api/Issue/FromHttpClientTest.php
  • tests/Unit/Api/Issue/ListTest.php
  • tests/Unit/Api/Issue/RemoveTest.php
  • tests/Unit/Api/Issue/RemoveWatcherTest.php
  • tests/Unit/Api/Issue/SetIssueStatusTest.php
  • tests/Unit/Api/Issue/ShowTest.php
  • tests/Unit/Api/IssueCategory/CreateTest.php
  • tests/Unit/Api/IssueCategory/FromHttpClientTest.php
  • tests/Unit/Api/IssueCategory/ListByProjectTest.php
  • tests/Unit/Api/IssueCategory/RemoveTest.php
  • tests/Unit/Api/IssueCategory/ShowTest.php
  • tests/Unit/Api/IssueCategoryTest.php
  • tests/Unit/Api/IssuePriority/FromHttpClientTest.php
  • tests/Unit/Api/IssuePriority/ListTest.php
  • tests/Unit/Api/IssuePriorityTest.php
  • tests/Unit/Api/IssueRelation/CreateTest.php
  • tests/Unit/Api/IssueRelation/FromHttpClientTest.php
  • tests/Unit/Api/IssueRelation/ListByIssueIdTest.php
  • tests/Unit/Api/IssueRelation/RemoveTest.php
  • tests/Unit/Api/IssueRelation/ShowTest.php
  • tests/Unit/Api/IssueRelationTest.php
  • tests/Unit/Api/IssueStatus/FromHttpClientTest.php
  • tests/Unit/Api/IssueStatus/ListTest.php
  • tests/Unit/Api/IssueStatusTest.php
  • tests/Unit/Api/IssueTest.php
  • tests/Unit/Api/Membership/CreateTest.php
  • tests/Unit/Api/Membership/FromHttpClientTest.php
  • tests/Unit/Api/Membership/ListByProjectTest.php
  • tests/Unit/Api/Membership/RemoveMemberTest.php
  • tests/Unit/Api/Membership/RemoveTest.php
  • tests/Unit/Api/Membership/UpdateTest.php
  • tests/Unit/Api/MembershipTest.php
  • tests/Unit/Api/News/FromHttpClientTest.php
  • tests/Unit/Api/News/ListByProjectTest.php
  • tests/Unit/Api/News/ListTest.php
  • tests/Unit/Api/NewsTest.php
  • tests/Unit/Api/Project/CloseTest.php
  • tests/Unit/Api/Project/CreateTest.php
  • tests/Unit/Api/Project/FromHttpClientTest.php
  • tests/Unit/Api/Project/ListTest.php
  • tests/Unit/Api/Project/RemoveTest.php
  • tests/Unit/Api/Project/ReopenTest.php
  • tests/Unit/Api/Project/ShowTest.php
  • tests/Unit/Api/Project/UnarchiveTest.php
  • tests/Unit/Api/ProjectTest.php
  • tests/Unit/Api/Query/FromHttpClientTest.php
  • tests/Unit/Api/Query/ListTest.php
  • tests/Unit/Api/QueryTest.php
  • tests/Unit/Api/Role/FromHttpClientTest.php
  • tests/Unit/Api/Role/ListTest.php
  • tests/Unit/Api/Role/ShowTest.php
  • tests/Unit/Api/RoleTest.php
  • tests/Unit/Api/Search/FromHttpClientTest.php
  • tests/Unit/Api/Search/ListByQueryTest.php
  • tests/Unit/Api/Search/SearchTest.php
  • tests/Unit/Api/SearchTest.php
  • tests/Unit/Api/TimeEntry/CreateTest.php
  • tests/Unit/Api/TimeEntry/FromHttpClientTest.php
  • tests/Unit/Api/TimeEntry/ListTest.php
  • tests/Unit/Api/TimeEntry/RemoveTest.php
  • tests/Unit/Api/TimeEntry/ShowTest.php
  • tests/Unit/Api/TimeEntryActivity/FromHttpClientTest.php
  • tests/Unit/Api/TimeEntryActivity/ListTest.php
  • tests/Unit/Api/TimeEntryActivityTest.php
  • tests/Unit/Api/TimeEntryTest.php
  • tests/Unit/Api/Tracker/FromHttpClientTest.php
  • tests/Unit/Api/Tracker/ListTest.php
  • tests/Unit/Api/TrackerTest.php
  • tests/Unit/Api/User/CreateTest.php
  • tests/Unit/Api/User/FromHttpClientTest.php
  • tests/Unit/Api/User/ListTest.php
  • tests/Unit/Api/User/RemoveTest.php
  • tests/Unit/Api/User/ShowTest.php
  • tests/Unit/Api/UserTest.php
  • tests/Unit/Api/Version/CreateTest.php
  • tests/Unit/Api/Version/FromHttpClientTest.php
  • tests/Unit/Api/Version/ListByProjectTest.php
  • tests/Unit/Api/Version/RemoveTest.php
  • tests/Unit/Api/Version/ShowTest.php
  • tests/Unit/Api/Version/UpdateTest.php
  • tests/Unit/Api/VersionTest.php
  • tests/Unit/Api/Wiki/FromHttpClientTest.php
  • tests/Unit/Api/Wiki/ListByProjectTest.php
  • tests/Unit/Api/Wiki/ShowTest.php
  • tests/Unit/Api/WikiTest.php
  • tests/Unit/Client/NativeCurlClient/RequestTest.php
  • tests/Unit/Client/Psr18Client/RequestTest.php
  • tests/Unit/Client/Psr18ClientTest.php
  • tests/Unit/Exception/ClientExceptionTest.php
  • tests/Unit/Exception/InvalidApiNameExceptionTest.php
  • tests/Unit/Exception/InvalidParameterExceptionTest.php
  • tests/Unit/Exception/MissingParameterExceptionTest.php

@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=php '\bpublic function search\(\$query, array \$params = \[\]\)|\blistByQuery\(\$query, \$params\)|catch \(Exception \$e\)' src/Redmine/Api/Search.php

Repository: kbsali/php-redmine-api

Length of output: 222


🏁 Script executed:

cd src/Redmine/Api && cat -n Search.php | head -100

Repository: kbsali/php-redmine-api

Length of output: 4157


🏁 Script executed:

grep -n "public function listByQuery" src/Redmine/Api/Search.php -A 2

Repository: kbsali/php-redmine-api

Length of output: 191


🏁 Script executed:

python3 << 'EOF'
# In PHP 7.0+, TypeError is a subclass of Error, not Exception
# So catch(Exception) would NOT catch TypeError
# Let's verify this understanding is correct

print("PHP Type Hierarchy (PHP 7+):")
print("- Error (base for all errors)")
print("  - TypeError (inherits from Error, NOT Exception)")
print("- Exception (separate from Error)")
print("\nConclusion: catch(Exception) will NOT catch TypeError")
EOF

Repository: kbsali/php-redmine-api

Length of output: 264


Cast $query to string before passing to listByQuery().

With declare(strict_types=1);, the untyped $query parameter in search() will cause a TypeError when passed to listByQuery(string $query, ...), and this error won't be caught by catch (Exception $e) since TypeError extends Error, not Exception.

Fix
 public function search($query, array $params = [])
 {
     `@trigger_error`('`' . __METHOD__ . '()` is deprecated since v2.4.0, use `' . self::class . '::listByQuery()` instead.', E_USER_DEPRECATED);

     try {
-        $results = $this->listByQuery($query, $params);
+        $results = $this->listByQuery((string) $query, $params);
     } catch (Exception $e) {
         if ($this->getLastResponse()->getContent() === '') {
             return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Redmine/Api/Search.php` at line 3, The search() method currently forwards
an untyped $query to listByQuery(string $query, ...) under strict_types, causing
a TypeError; fix this by explicitly casting $query to string when calling
listByQuery (e.g., pass (string)$query) inside the search() implementation so
the call matches listByQuery's signature and avoids uncaught Error types; update
any related call sites in the same method (references: search(), listByQuery())
accordingly.

try {
$this->deserialized = new SimpleXMLElement($encoded);
} catch (Throwable $e) {
} catch (Exception $e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Narrowing catch from Throwable to Exception may let TypeError escape uncaught.

With declare(strict_types=1), type mismatches now throw TypeError which extends Error, not Exception. This catch block will no longer capture:

  • TypeError from strict type violations during XML processing
  • Other Error subclasses that were previously caught

Call sites like AbstractApi::getResponseAsArray() and various API create()/update() methods expect SerializerException but will now receive raw TypeError if caller-provided data has incorrect types.

Proposed fix: Keep catching Throwable
-        } catch (Exception $e) {
+        } catch (\Throwable $e) {

Apply to both catch blocks at lines 80 and 129.

Also applies to: 129-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Redmine/Serializer/XmlSerializer.php` at line 80, The catch blocks in
XmlSerializer (the catch clauses currently written as "catch (Exception $e)")
are too narrow and will let TypeError/other Error subclasses escape under
strict_types; update both catch clauses in the XmlSerializer class (the blocks
that currently catch Exception and wrap/rethrow as SerializerException) to catch
Throwable instead, preserving the existing handling logic (wrap/throw
SerializerException with the same message/context) so TypeError and other Errors
are captured and converted to SerializerException as callers expect.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 3 file(s) based on 3 unresolved review comments.

A stacked PR containing fixes has been created.

  • Stacked PR: #475
  • Files modified:
  • src/Redmine/Api/CustomField.php
  • src/Redmine/Api/Search.php
  • src/Redmine/Serializer/XmlSerializer.php

Time taken: 2m 12s

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/Unit/Api/RoleTest.php (1)

258-259: Consider splitting this into a separate test case for clarity.

The test now verifies two different scenarios: listing(true) and listing(1). While this validates that the $forceUpdate parameter correctly handles truthy int values (important given the untyped parameter in Role::listing()), mixing these cases in one test method slightly obscures the intent.

If this is intentionally testing backward compatibility for non-bool inputs, a dedicated test method like testListingAcceptsIntAsForceUpdate() would make the purpose clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Unit/Api/RoleTest.php` around lines 258 - 259, The current test mixes
two scenarios (calling Role::listing() with a boolean and with an int) which
reduces clarity; extract the assertion that calls $api->listing(1) into its own
test method (e.g. testListingAcceptsIntAsForceUpdate) that sets up the same
expected return and invokes listing(1) (retaining the // `@phpstan-ignore` comment
if needed), and leave the original test to only assert listing(true); ensure
both tests use the same mock/setup for $api and $expectedReturn so behavior is
preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/Unit/Api/RoleTest.php`:
- Around line 258-259: The current test mixes two scenarios (calling
Role::listing() with a boolean and with an int) which reduces clarity; extract
the assertion that calls $api->listing(1) into its own test method (e.g.
testListingAcceptsIntAsForceUpdate) that sets up the same expected return and
invokes listing(1) (retaining the // `@phpstan-ignore` comment if needed), and
leave the original test to only assert listing(true); ensure both tests use the
same mock/setup for $api and $expectedReturn so behavior is preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 004e6551-2925-4c29-953e-2c055c2024d7

📥 Commits

Reviewing files that changed from the base of the PR and between ec15698 and 47748bd.

📒 Files selected for processing (17)
  • src/Redmine/Api/CustomField.php
  • src/Redmine/Api/IssueCategory.php
  • src/Redmine/Api/IssueStatus.php
  • src/Redmine/Api/Project.php
  • src/Redmine/Api/Tracker.php
  • src/Redmine/Api/User.php
  • src/Redmine/Api/Version.php
  • tests/Unit/Api/CustomFieldTest.php
  • tests/Unit/Api/GroupTest.php
  • tests/Unit/Api/IssueCategoryTest.php
  • tests/Unit/Api/IssueStatusTest.php
  • tests/Unit/Api/ProjectTest.php
  • tests/Unit/Api/RoleTest.php
  • tests/Unit/Api/TimeEntryActivityTest.php
  • tests/Unit/Api/TrackerTest.php
  • tests/Unit/Api/UserTest.php
  • tests/Unit/Api/VersionTest.php
✅ Files skipped from review due to trivial changes (2)
  • tests/Unit/Api/IssueStatusTest.php
  • tests/Unit/Api/TimeEntryActivityTest.php
🚧 Files skipped from review as they are similar to previous changes (7)
  • tests/Unit/Api/ProjectTest.php
  • src/Redmine/Api/Project.php
  • tests/Unit/Api/CustomFieldTest.php
  • tests/Unit/Api/VersionTest.php
  • src/Redmine/Api/CustomField.php
  • tests/Unit/Api/IssueCategoryTest.php
  • tests/Unit/Api/TrackerTest.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Declare strict types in all files

1 participant