Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/book/exceptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Exceptions

Every exception this package throws is built by a `static` named constructor on a
component-owned exception class. The message template sits beside it as a
`final public const string`. No exception is constructed with a message assembled at
the throw site.

## The convention

```php
namespace PhpDb\TableGateway\Exception;

use PhpDb\Exception;

use function sprintf;

class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
final public const string NO_PRIMARY_KEY = 'No information was provided to the RowGatewayFeature'
. ' and/or no MetadataFeature could be consulted to find the primary key'
. ' necessary for RowGateway object creation.';

final public const string INVALID_MAGIC_CALL = 'Invalid method (%s) called, caught by %s::__call()';

public static function forNoPrimaryKey(): self
{
return new self(self::NO_PRIMARY_KEY);
}

public static function forInvalidMagicCall(string $method, string $class): self
{
return new self(sprintf(self::INVALID_MAGIC_CALL, $method, $class));
}
}
```

- Named constructors are `static`, return `self` and start with `for`.
- Each one has a single `final public const string` template beside it.
- The constant name mirrors the method name: `forNoPrimaryKey()` uses `NO_PRIMARY_KEY`,
`forInvalidMagicCall()` uses `INVALID_MAGIC_CALL`. Strip the `for` prefix and
upper-snake the rest.
- Use `new self(self::CONST)` when the template has no placeholder and
`new self(sprintf(self::CONST, ...))` when it does. `sprintf()` on a template with no
`%` placeholder returns its own argument, so it is not written.
- Quote a `%s` that holds a caller-supplied identifier, such as a column or parameter
name. Leave a class name, method name or count unquoted.

The constants are `final` because the component base classes are not. Adapter packages
extend `PhpDb\Adapter\Exception\RuntimeException`, so without `final` a subclass could
redefine a published template.

## When a condition earns its own exception class

Naming a condition in a class instead of a method is a departure from the convention,
not an application of it. A condition earns its own class only when it carries something
a named constructor cannot:

1. **Extra state, with accessors.** `PhpDb\Adapter\Exception\InvalidConnectionParametersException`
holds the connection parameters it rejected.
2. **An external interface contract.** `PhpDb\Exception\ContainerException` implements
PSR-11's `ContainerExceptionInterface`.
3. **A condition a consumer can act on.** `PhpDb\Adapter\Exception\InvalidQueryException`
is thrown by every adapter and caught to log the failing SQL or retry.

Everything else is a named constructor on the component's SPL-shaped class.

## Component namespaces

There is exactly one marker interface, `PhpDb\Exception\ExceptionInterface`, and every
exception this package throws implements it. It extends `Throwable`, so it can be used as a
type and not only in a `catch` clause.

Components do **not** declare their own `ExceptionInterface`. Each owns an `Exception`
namespace holding the SPL-shaped classes it needs, and those extend their counterparts in
`PhpDb\Exception`, which is where the marker is declared — so a component class inherits it
rather than restating it. A sub-namespace such as `PhpDb\Sql\Predicate\Exception` does the
same through its parent component.

To catch every failure from one component, name its classes:

```php
catch (PhpDb\Sql\Exception\InvalidArgumentException | PhpDb\Sql\Exception\RuntimeException $e)
```

A class named after an SPL exception extends that SPL exception, so `catch (\RuntimeException)` and
`catch (PhpDb\Exception\ExceptionInterface)` both behave as a reader expects.
`PhpDbTest\Exception\ExceptionHierarchyTest` enforces both rules across `src/`, for every class listed in its `EXCEPTION_CLASSES` map.
18 changes: 0 additions & 18 deletions lint-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2868,24 +2868,6 @@ code = "prefer-test-attribute"
message = "Use `#[Test]` attribute instead of `test` prefix on method `testInvokeUsesDefaultSeparatorWhenSeparatorKeyIsAbsent`."
count = 1

[[issues]]
file = "test/unit/Exception/ContainerExceptionTest.php"
code = "assertion-style"
message = "Inconsistent assertions style."
count = 4

[[issues]]
file = "test/unit/Exception/ContainerExceptionTest.php"
code = "prefer-test-attribute"
message = "Use `#[Test]` attribute instead of `test` prefix on method `testForServiceCreatesFormattedExceptionMessage`."
count = 1

[[issues]]
file = "test/unit/Exception/ContainerExceptionTest.php"
code = "prefer-test-attribute"
message = "Use `#[Test]` attribute instead of `test` prefix on method `testImplementsContainerExceptionInterface`."
count = 1

[[issues]]
file = "test/unit/ResultSet/AbstractResultSetIntegrationTest.php"
code = "assertion-style"
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ nav:
- Metadata Objects: metadata/objects.md
- Examples: metadata/examples.md
- Profiler: profiler.md
- Exceptions: exceptions.md
- "Application Integration":
- "Integrating in a Laminas MVC application": application-integration/usage-in-a-laminas-mvc-application.md
- "Integrating in a Mezzio application": application-integration/usage-in-a-mezzio-application.md
Expand Down
2 changes: 1 addition & 1 deletion src/Adapter/Exception/ErrorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class ErrorException extends Exception\ErrorException implements ExceptionInterface {}
class ErrorException extends Exception\ErrorException {}
9 changes: 0 additions & 9 deletions src/Adapter/Exception/ExceptionInterface.php

This file was deleted.

2 changes: 1 addition & 1 deletion src/Adapter/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

use PhpDb\Exception;

class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface {}
class InvalidArgumentException extends Exception\InvalidArgumentException {}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace PhpDb\Adapter\Exception;

class InvalidConnectionParametersException extends RuntimeException implements ExceptionInterface
class InvalidConnectionParametersException extends RuntimeException
{
protected array $parameters;

Expand Down
2 changes: 1 addition & 1 deletion src/Adapter/Exception/InvalidQueryException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

namespace PhpDb\Adapter\Exception;

class InvalidQueryException extends UnexpectedValueException implements ExceptionInterface {}
class InvalidQueryException extends UnexpectedValueException {}
2 changes: 1 addition & 1 deletion src/Adapter/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class RuntimeException extends Exception\RuntimeException implements ExceptionInterface {}
class RuntimeException extends Exception\RuntimeException {}
2 changes: 1 addition & 1 deletion src/Adapter/Exception/UnexpectedValueException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class UnexpectedValueException extends Exception\UnexpectedValueException implements ExceptionInterface {}
class UnexpectedValueException extends Exception\UnexpectedValueException {}
2 changes: 1 addition & 1 deletion src/Adapter/Exception/VunerablePlatformQuoteException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

use function sprintf;

final class VunerablePlatformQuoteException extends RuntimeException implements ExceptionInterface
final class VunerablePlatformQuoteException extends RuntimeException
{
public static function forPlatformAndMethod(string $platformName, string $methodName): self
{
return new self(
sprintf(
'Attempting to quote in %s::%s without extension/driver support'

Check warning on line 15 in src/Adapter/Exception/VunerablePlatformQuoteException.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ { return new self( sprintf( - 'Attempting to quote in %s::%s without extension/driver support' - . ' can introduce security vulnerabilities in a production environment.', + 'Attempting to quote in %s::%s without extension/driver support', $platformName, $methodName, ),

Check warning on line 15 in src/Adapter/Exception/VunerablePlatformQuoteException.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "ConcatOperandRemoval": @@ @@ { return new self( sprintf( - 'Attempting to quote in %s::%s without extension/driver support' - . ' can introduce security vulnerabilities in a production environment.', + ' can introduce security vulnerabilities in a production environment.', $platformName, $methodName, ),

Check warning on line 15 in src/Adapter/Exception/VunerablePlatformQuoteException.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "Concat": @@ @@ { return new self( sprintf( - 'Attempting to quote in %s::%s without extension/driver support' - . ' can introduce security vulnerabilities in a production environment.', + ' can introduce security vulnerabilities in a production environment.' . 'Attempting to quote in %s::%s without extension/driver support', $platformName, $methodName, ),
. ' can introduce security vulnerabilities in a production environment.',
$platformName,
$methodName,
Expand Down
8 changes: 4 additions & 4 deletions src/Exception/ContainerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@
namespace PhpDb\Exception;

use Psr\Container\ContainerExceptionInterface;
use RuntimeException as SplRuntimeException;

use function sprintf;

final class ContainerException extends SplRuntimeException implements ContainerExceptionInterface
final class ContainerException extends RuntimeException implements ContainerExceptionInterface
{
final public const string SERVICE = 'Failed to create service "%s" in factory %s Reason: %s';

public static function forService(
string $serviceName,
string $factoryClass,
string $reason,
): self {
return new self(
sprintf(
'Failed to create service "%s" in factory %s Reason: %s',
self::SERVICE,
$serviceName,
$factoryClass,
$reason,
),
0,
);
}
}
5 changes: 4 additions & 1 deletion src/Exception/ExceptionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@

namespace PhpDb\Exception;

interface ExceptionInterface {}
use Throwable;

/** Marker for every exception this package throws. */
interface ExceptionInterface extends Throwable {}
9 changes: 0 additions & 9 deletions src/ResultSet/Exception/ExceptionInterface.php

This file was deleted.

2 changes: 1 addition & 1 deletion src/ResultSet/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface {}
class InvalidArgumentException extends Exception\InvalidArgumentException {}
2 changes: 1 addition & 1 deletion src/ResultSet/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class RuntimeException extends Exception\RuntimeException implements ExceptionInterface {}
class RuntimeException extends Exception\RuntimeException {}
9 changes: 0 additions & 9 deletions src/RowGateway/Exception/ExceptionInterface.php

This file was deleted.

2 changes: 1 addition & 1 deletion src/RowGateway/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface {}
class InvalidArgumentException extends Exception\InvalidArgumentException {}
2 changes: 1 addition & 1 deletion src/RowGateway/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class RuntimeException extends Exception\RuntimeException implements ExceptionInterface {}
class RuntimeException extends Exception\RuntimeException {}
9 changes: 0 additions & 9 deletions src/Sql/Exception/ExceptionInterface.php

This file was deleted.

2 changes: 1 addition & 1 deletion src/Sql/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface {}
class InvalidArgumentException extends Exception\InvalidArgumentException {}
2 changes: 1 addition & 1 deletion src/Sql/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class RuntimeException extends Exception\RuntimeException implements ExceptionInterface {}
class RuntimeException extends Exception\RuntimeException {}
11 changes: 11 additions & 0 deletions src/TableGateway/AbstractTableGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ abstract class AbstractTableGateway implements TableGatewayInterface

protected string|int|false|null $lastInsertValue = null;

/** @throws Exception\RuntimeException */
#[Override]
public function delete(Where|Closure|array|string $where): int
{
Expand All @@ -67,6 +68,7 @@ public function delete(Where|Closure|array|string $where): int
return $this->executeDelete($delete);
}

/** @throws Exception\RuntimeException */
public function deleteWith(Delete $delete): int
{
$this->initialize();
Expand All @@ -84,6 +86,7 @@ public function getColumns(): array
return $this->columns;
}

/** @throws Exception\RuntimeException */
public function getFeatureSet(): Feature\FeatureSet
{
if (! $this->isInitialized) {
Expand All @@ -98,6 +101,7 @@ public function getLastInsertValue(): string|int|false|null
return $this->lastInsertValue;
}

/** @throws Exception\RuntimeException */
public function getResultSetPrototype(): ResultSetInterface
{
if (! $this->isInitialized) {
Expand All @@ -107,6 +111,7 @@ public function getResultSetPrototype(): ResultSetInterface
return $this->resultSetPrototype;
}

/** @throws Exception\RuntimeException */
public function getSql(): Sql
{
if (! $this->isInitialized) {
Expand Down Expand Up @@ -161,6 +166,7 @@ public function initialize(): void
$this->isInitialized = true;
}

/** @throws Exception\RuntimeException */
#[Override]
public function insert(array $set): int
{
Expand All @@ -173,6 +179,7 @@ public function insert(array $set): int
return $this->executeInsert($insert);
}

/** @throws Exception\RuntimeException */
public function insertWith(Insert $insert): int
{
if (! $this->isInitialized) {
Expand All @@ -187,6 +194,7 @@ public function isInitialized(): bool
return $this->isInitialized;
}

/** @throws Exception\RuntimeException */
#[Override]
public function select(Where|Closure|string|array|null $where = null): ResultSetInterface
{
Expand All @@ -205,6 +213,7 @@ public function select(Where|Closure|string|array|null $where = null): ResultSet
return $this->selectWith($select);
}

/** @throws Exception\RuntimeException */
public function selectWith(Select $select): ResultSetInterface
{
if (! $this->isInitialized) {
Expand All @@ -214,6 +223,7 @@ public function selectWith(Select $select): ResultSetInterface
return $this->executeSelect($select);
}

/** @throws Exception\RuntimeException */
#[Override]
public function update(
array $set,
Expand All @@ -240,6 +250,7 @@ public function update(
return $this->executeUpdate($update);
}

/** @throws Exception\RuntimeException */
public function updateWith(Update $update): int
{
if (! $this->isInitialized) {
Expand Down
9 changes: 0 additions & 9 deletions src/TableGateway/Exception/ExceptionInterface.php

This file was deleted.

2 changes: 1 addition & 1 deletion src/TableGateway/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface {}
class InvalidArgumentException extends Exception\InvalidArgumentException {}
2 changes: 1 addition & 1 deletion src/TableGateway/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

use PhpDb\Exception;

class RuntimeException extends Exception\InvalidArgumentException implements ExceptionInterface {}
class RuntimeException extends Exception\RuntimeException {}
Loading
Loading