Skip to content

Modernize for PHP 8.4+, Symfony 6.4/7.4/8.1+, PHPUnit 12 - #238

Open
lyrixx wants to merge 4 commits into
mainfrom
modernize
Open

Modernize for PHP 8.4+, Symfony 6.4/7.4/8.1+, PHPUnit 12#238
lyrixx wants to merge 4 commits into
mainfrom
modernize

Conversation

@lyrixx

@lyrixx lyrixx commented Sep 1, 2026

Copy link
Copy Markdown
Member

Summary

  • Raises the minimum requirements: PHP ^8.4, symfony/process ^6.4 || ^7.4 || ^8.1, phpunit/phpunit ^12.0, psr/log ^3.0; drops phpspec/prophecy-phpunit (its one use site now uses native PHPUnit mocks)
  • Adds full property/parameter/return type coverage across src/, with readonly wherever a property is genuinely write-once, and final/final readonly on every class that permits it (dropped the redundant per-property readonly on classes already declared readonly at the class level)
  • Converts the test suite from doc-comment metadata (@dataProvider, @before, @after, @beforeClass) to PHPUnit 12 attributes, since PHPUnit 12 no longer reads the old annotations; renames AbstractTest to AbstractTestCase so PHPUnit's *Test.php glob doesn't try to run it directly
  • Updates CI (PHP 8.4/8.5 matrix + a prefer-lowest job, composer validate), phpunit.xml.dist (PHPUnit 12 schema), and README/CONTRIBUTING for the new baseline
  • Drops StyleCI: no .styleci.yml or workflow step existed for it, so this removes every textual reference (README badges, CONTRIBUTING mentions) — actually uninstalling the GitHub App is a repo-settings action outside this PR
  • Adds php-cs-fixer, PHPStan and Castor tooling, mirroring JoliCode's own JoliNotif setup:
    • php-cs-fixer and phpstan each live in tools/<name>/ with their own isolated composer.json/lock, wired together via a root castor.php. castor install, castor cs, castor phpstan and castor phpunit all work end to end
    • .php-cs-fixer.php: @PHP84Migration + @PhpCsFixer + @Symfony(:risky), with gitlib's own license header enforced via header_comment — ran it for real across the codebase (58 files reformatted, all mechanical; it also filled in the license header on two files that were missing it entirely)
    • phpstan.neon: level 9 on src/. Fixed the ~20 findings that were real bugs or design smells rather than annotation gaps (see below); the remaining ~94 are baselined in phpstan-baseline.neon
    • CI: check-cs and phpstan jobs pinned to PHP 8.4 (our floor) using castor-php/setup-castor@v1.1.0
    • CONTRIBUTING.md: Castor install step, "Standard code" and "Static analysis" sections
  • Fixes bugs surfaced along the way:
    • Log's ReferenceNotFoundException call silently dropped the chained previous exception (the constructor didn't accept it)
    • DiffTest called restore_exception_handler() instead of restore_error_handler(), leaking error handlers between tests
    • a dead @dataProvider on ReferenceTest::testIsBranchMergedToMaster wastefully cloned repositories it never used
    • Reference\Tag::getCommit() could pass an undefined $commitHash to getCommit() when show-ref returned no matching row
    • Repository::getDiff(null) and Log::getCommits()'s catch block would fatal-error calling a method on null on a reachable path (a Log built with no revision filter)
    • Repository's $gitDir/$workingDir were readonly-assigned outside the constructor; moved to a static resolveDir() helper
    • Repository's single $objects cache (typed Commit|Tree|Blob) split into three properly-typed caches
    • RevisionList/Tree array-shape narrowing fixed by restructuring the loops that build them
    • ReferenceBag::update() and a new getAs() helper made generic so createBranch()/getTag()/etc. return their real narrow type
    • CommitReference was missing a getHash() getter entirely
    • Blob::getMimetype() honestly typed string|false (finfo::buffer() can fail)

The remaining PHPStan baseline entries are mostly Commit/Reference\Tag's untyped getData(): mixed internals, and Repository::run()'s ?string return propagating into code that assumes a string. Fixing those properly means redesigning that data-caching layer or auditing every run() call site for the debug=false null case — bigger than this PR's scope.

Test plan

  • vendor/bin/phpunit — 215 tests, 0 failures (against PHP 8.5.4 locally)
  • composer update --prefer-lowest then vendor/bin/phpunit — passes against the floor of the new dependency ranges (symfony/process 6.4.33)
  • composer validate --strict
  • castor cs --dry-run — clean
  • castor phpstan — clean (with baseline)

- composer.json: require php ^8.4, symfony/process ^6.4 || ^7.4 || ^8.1,
  bump phpunit/phpunit to ^12.0 and psr/log to ^3.0, drop
  phpspec/prophecy-phpunit (its one use site now uses native PHPUnit mocks)
- CI matrix now runs PHP 8.4/8.5 plus a prefer-lowest job; phpunit.xml.dist
  rewritten for the PHPUnit 12 schema
- README/CONTRIBUTING updated for the new PHP baseline and a couple of
  stale references
- Full property/parameter/return type coverage across src/, with readonly
  applied wherever a property is genuinely write-once (mostly via
  constructor promotion)
- Classes marked final wherever nothing extends them, and final readonly
  where every property is readonly; left plain where they're actually
  extended or inherit mutable state (Revision, Reference, ParserBase,
  CommitParser, Exception\RuntimeException)
- Tests converted from doc-comment metadata (@dataProvider/@before/@after)
  to PHPUnit 12 attributes, since PHPUnit 12 no longer reads the old
  annotations; AbstractTest renamed to AbstractTestCase so PHPUnit doesn't
  mistake it for a test case via the *Test.php glob
- Fixed bugs surfaced along the way: Log's ReferenceNotFoundException call
  silently dropped the previous exception (constructor didn't accept it);
  DiffTest called restore_exception_handler() instead of
  restore_error_handler(), leaking handlers between tests; a dead
  @dataProvider on ReferenceTest::testIsBranchMergedToMaster wastefully
  cloned repos it never used
StyleCI never had a .styleci.yml or a workflow step in this repo — it only
ran as a separate GitHub App integration. Removing it here means dropping
every textual reference; actually uninstalling the app is a repo-settings
action outside of this checkout.
Mirrors JoliCode's own JoliNotif project setup:

- php-cs-fixer and phpstan each live in tools/<name>/ with their own
  isolated composer.json/lock (avoids dependency conflicts with the
  library's own require-dev), wired together via a root castor.php that
  imports each tool's own castor.php. `castor install`, `castor cs`,
  `castor phpstan` and `castor phpunit` all work end to end.
- .php-cs-fixer.php: @PHP84Migration + @PhpCsFixer + @symfony(:risky),
  gitlib's own license header enforced via header_comment. Ran it for
  real across the codebase (58 files reformatted, all mechanical); it
  also filled in the license header on two files that were missing it
  entirely (RuntimeException.php, tests/bootstrap.php).
- phpstan.neon: level 9 on src/. Fixed the ~20 findings that were real
  bugs or design smells rather than annotation gaps:
    - Reference\Tag::getCommit() could pass an undefined $commitHash
      to getCommit() when show-ref returned no matching row
    - Repository::getDiff(null) and Log::getCommits()'s catch block
      would fatal-error calling a method on null on a reachable path
      (a Log built with no revision filter)
    - Repository's $gitDir/$workingDir were readonly-assigned outside
      the constructor; moved to a static resolveDir() helper
    - Repository's single $objects cache (typed Commit|Tree|Blob) split
      into three properly-typed caches
    - RevisionList/Tree array-shape narrowing fixed by restructuring
      the loops that build them
    - ReferenceBag::update() and a new getAs() helper made generic so
      createBranch()/getTag()/etc. return their real narrow type
    - CommitReference was missing a getHash() getter entirely
    - Blob::getMimetype() honestly typed string|false (finfo::buffer()
      can fail)
  The remaining ~94 findings are baselined in phpstan-baseline.neon:
  mostly Commit/Reference\Tag's untyped getData(): mixed internals, and
  Repository::run()'s ?string return propagating into code that assumes
  a string. Fixing those properly means redesigning that data-caching
  layer or auditing every run() call site for the debug=false null case,
  which is bigger than "add phpstan".
- CI: check-cs and phpstan jobs pinned to PHP 8.4 (our floor) using
  castor-php/setup-castor@v1.1.0.
- CONTRIBUTING.md: Castor install step, "Standard code" and "Static
  analysis" sections.
A class-level `readonly` modifier already makes every declared property
readonly, so repeating the keyword on each promoted constructor property
was dead weight.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant