Skip to content

Introduce the named exception constructor convention - #190

Merged
simon-mundy merged 4 commits into
php-db:0.6.xfrom
simon-mundy:exception/pr-1-convention-and-hierarchy
Sep 29, 2026
Merged

simon-mundy merged 4 commits into
php-db:0.6.xfrom
simon-mundy:exception/pr-1-convention-and-hierarchy

Conversation

@simon-mundy

@simon-mundy simon-mundy commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Types of changes

  • Documentation improvement
  • Bugfix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • QA improvement (unit/integration tests, CS fixes, etc.) that does not change code behavior

Checklist:

  • An issue has been created for this pull request.
  • I have added tests for my changes.
  • All new and existing tests passed.
  • Documentation has been updated accordingly.
  • This pull request targets the correct branch.

Description

PR 1 of 6 from #189. Establishes the named exception constructor convention and fixes the two hierarchy defects the audit found. No throw sites move in this PR.

  • docs/book/exceptions.md documents the convention, the constant naming rule (forFooBar() uses FOO_BAR) and the three tests for when a condition earns its own class rather than a named constructor.
  • TableGateway\Exception\RuntimeException extended PhpDb\Exception\InvalidArgumentException, so it was not a \RuntimeException at all. It now extends Exception\RuntimeException.
  • That retype exposed 13 missing @throws annotations on AbstractTableGateway, TableGateway::__construct and GlobalAdapterFeature::preInitialize. The wrong parent had been letting @throws Exception\InvalidArgumentException cover a thrown RuntimeException, so the analyzer accepted them. Declared.
  • ContainerException implemented PSR-11's ContainerExceptionInterface but not PhpDb\Exception\ExceptionInterface, unlike every other exception in the package. It now extends Exception\RuntimeException, picking the marker up through its parent, and its message moved to a SERVICE template.
  • ExceptionHierarchyTest guards all three rules by scanning src/, so a new exception class is covered as soon as it is written: every concrete exception implements the marker interface, a class named after an SPL exception extends that SPL exception, and every for* factory has the template constant its name implies.

VunerablePlatformQuoteException is exempt from the naming guard, with the reason in code: its factory is named for its arguments rather than the fault and holds its message inline. PR 5 removes the class, which retires the exemption.

ErrorException is deliberately absent from the SPL-parent map, also documented in code. Both PhpDb\Exception\ErrorException and PhpDb\Adapter\Exception\ErrorException extend \Exception rather than \ErrorException, and \ErrorException's third constructor argument is $severity where \Exception's is $previous. Correcting the parent would change a released constructor contract that all three adapters construct against, so it needs its own decision.

Motivation and Context

Refs #189. src/ has 119 throw new sites that build their messages inline, several of them building the same message in more than one class. This PR lands the rule and the guards so PRs 2 to 6 have something to conform to, and fixes the two defects that make the current hierarchy wrong.

On the target branch. This targets 0.6.x rather than the next major, against the template's guidance for backwards incompatible changes. Retyping TableGateway\Exception\RuntimeException breaks catch (\InvalidArgumentException) around any of the 27 TableGateway throws that use it. The reasoning, agreed on #189, is that 0.5.0 is the current tag and 0.6.0 is unreleased, so this is the window, and shipping a RuntimeException that is an InvalidArgumentException into 1.0 is the worse outcome. Happy to retarget if the TSC would rather hold it for the next major.

How has this been tested?

mago format --check, mago lint and mago analyze all exit 0. Full unit suite passes: 1561 tests, 3437 assertions, verified on this branch alone rather than only as part of the stack.

Each of the three guards was verified to fail on the defect it exists for, by reintroducing that defect and confirming the failure. ContainerException::forService() is at 1/1 methods and 9/9 lines covered.

Baseline changes are limited to removing three test/unit/Exception/ContainerExceptionTest.php entries from lint-baseline.toml that this PR makes stale. analyzer-baseline.toml is untouched.

@simon-mundy simon-mundy added the enhancement New feature or request label Sep 28, 2026
@simon-mundy simon-mundy mentioned this pull request Sep 28, 2026
8 tasks done
@codecov

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@simon-mundy

Copy link
Copy Markdown
Member Author

PR 1 of 6 from #189. #191 is stacked on this one and should merge after it.

- Document the convention, the constant naming rule and the three tests for
  when a condition earns its own class in docs/book/exceptions.md
- Point TableGateway\Exception\RuntimeException at Exception\RuntimeException
  rather than Exception\InvalidArgumentException, so it is a \RuntimeException
- Declare the @throws the retype exposed on AbstractTableGateway,
  TableGateway::__construct and GlobalAdapterFeature::preInitialize, which the
  wrong parent had been covering
- Extend ContainerException from Exception\RuntimeException so it carries the
  package marker interface, and move its message to a SERVICE template
- Guard the marker interface, the SPL parents and the constant naming rule in
  ExceptionHierarchyTest, scanning src so new classes are covered on sight
- Assert ContainerException's message against its template and drop the file's
  stale lint baseline entries

Refs php-db#189

Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>

@tyrsson tyrsson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lets keep it simple and just glob the exception classes if we are going to approach it from this direction.

Comment thread test/unit/Exception/TestAsset/ExceptionClassLocator.php Outdated
- List every concrete exception in ExceptionHierarchyTest::EXCEPTION_CLASSES,
  mapped to the SPL class it must extend, in place of scanning src
- Assert the SPL parent for every class rather than only those named after an
  SPL exception, and drop the name-based SPL_PARENTS map
- Move the named constructor helpers into the test and delete the
  ExceptionClassLocator test asset

Refs php-db#189

Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>
tyrsson
tyrsson previously approved these changes Sep 29, 2026
…rowable

Two changes to the marker interface, both surfaced while reviewing php-db#189's
convention against the code.

The root interface did not extend Throwable, which was unintentional. It left
the interface usable only in a catch clause: a parameter typed against it was
not known to be throwable and had no getMessage(), so `mago analyze` reported
non-existent-method and invalid-throw on any code that tried. It now extends
Throwable, and ExceptionHierarchyTest pins that so it cannot regress. The
interface carries a one-line docblock saying what it marks, and nothing about the
change itself.

Each component also declared its own ExceptionInterface extending the root one.
Those are removed: Adapter, ResultSet, RowGateway, Sql and TableGateway. Every
concrete exception class extends its counterpart in PhpDb\Exception, which is
where the marker is declared, so all 20 still implement it by inheritance and
the 15 redundant `implements` clauses are gone. docs/book/exceptions.md records
the single-interface rule and shows how to catch a whole component by naming its
classes.

BC: the five component interfaces ship in 0.5.0, so `catch
(PhpDb\Sql\Exception\ExceptionInterface)` and the equivalents stop resolving.
Nothing in this repository or the adapter packages catches one; phpdb-mysql
names them in @throws docblocks only, which its own RFC covers.

Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>
- ContainerException::forService() passed 0 explicitly as $code, which is the
  RuntimeException default. Infection reported two escaped mutants on it,
  IncrementInteger and DecrementInteger, because nothing asserts the code.
  Removing the argument removes the literal rather than adding an assertion for
  a value PHP already supplies; getCode() is unchanged and Covered Code MSI for
  the file goes from 33% to 100%
- ExceptionHierarchyTest's naming-guard exemption named the pull request that
  would remove the class. The reason for the exemption stays; the reference to
  work that will be merged and forgotten does not

Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>
@simon-mundy
simon-mundy force-pushed the exception/pr-1-convention-and-hierarchy branch from 9c7bb07 to 2886bc7 Compare September 29, 2026 03:32
@simon-mundy
simon-mundy requested a review from tyrsson September 29, 2026 03:38
@simon-mundy
simon-mundy merged commit 56cfaa9 into php-db:0.6.x Sep 29, 2026
15 checks passed
simon-mundy added a commit that referenced this pull request Sep 29, 2026
Drops 18 entries for issues that no longer exist: the seven per-component
ExceptionInterface files deleted in PR #190, four exception classes that
are now final, three resolved unhandled-thrown-type and mixed-argument
sites, and the unfulfilled-expect and prefer-test-attribute entries
settled by the mutation coverage work in PR #188.

Suppressed issue totals are unchanged at 3948 lint and 1215 analyzer, so
no new findings are masked.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants