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
8 changes: 8 additions & 0 deletions docs/book/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ a named constructor cannot:

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

### The one exception to the rule

`PhpDb\Container\AdapterInterfaceDelegator` renders a template at the throw site rather than
calling a named constructor. The ServiceManager decides the type it must throw, and Laminas
annotates `ServiceNotFoundException` `@final`, so this package cannot own the class. It owns
the wording instead, as `PhpDb\Exception\ContainerException::MISSING_SERVICE`. Anywhere the
thrown type is ours, the rule above applies without exception.

## Component namespaces

There is exactly one marker interface, `PhpDb\Exception\ExceptionInterface`, and every
Expand Down
6 changes: 1 addition & 5 deletions src/Container/AbstractAdapterInterfaceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,7 @@ public function __invoke(
$driverClass = $this->config[$requestedName]['driver'] ?? null;

if (null === $driverClass) {
throw ContainerException::forService(
$requestedName,
self::class,
'no driver configured',
);
throw ContainerException::forMissingDriver($requestedName, self::class);
}

/** @var DriverInterface|PdoDriverInterface $driver */
Expand Down
13 changes: 3 additions & 10 deletions src/Container/AdapterInterfaceDelegator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
use PhpDb\Adapter\AdapterAwareInterface;
use PhpDb\Adapter\AdapterInterface;
use PhpDb\Exception;
use PhpDb\Exception\ContainerException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
Expand All @@ -33,18 +33,11 @@
$instance = $callback();

if (! $instance instanceof AdapterAwareInterface) {
throw new Exception\RuntimeException(sprintf(
'Delegated service "%s" must implement %s',
$name,
AdapterAwareInterface::class,
));
throw ContainerException::forNonAdapterAwareDelegate($name, AdapterAwareInterface::class);
}

if (! $container->has($this->adapterName)) {
throw new ServiceNotFoundException(sprintf(
'Service "%s" not found in container',
$this->adapterName,
));
throw new ServiceNotFoundException(sprintf(ContainerException::MISSING_SERVICE, $this->adapterName));
}

$databaseAdapter = $container->get($this->adapterName);
Expand All @@ -60,6 +53,6 @@

public static function __set_state(array $state): self
{
return new self($state['adapterName'] ?? AdapterInterface::class);

Check warning on line 56 in src/Container/AdapterInterfaceDelegator.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "Coalesce": @@ @@ public static function __set_state(array $state): self { - return new self($state['adapterName'] ?? AdapterInterface::class); + return new self(AdapterInterface::class ?? $state['adapterName']); } }
}
}
12 changes: 2 additions & 10 deletions src/Container/AdapterInterfaceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,14 @@ public function __invoke(
string $requestedName,
): AdapterInterface&Adapter {
if (! $container->has('config')) {
throw ContainerException::forService(
$requestedName,
self::class,
'Container is missing a config service',
);
throw ContainerException::forMissingConfigService($requestedName, self::class);
}

$config = $container->get('config') ?? [];
$adapterConfig = $config[AdapterInterface::class] ?? $config[Adapter::class] ?? [];

if ([] === $adapterConfig) {
throw ContainerException::forService(
AdapterInterface::class,
self::class,
"No configuration found for {$requestedName}",
);
throw ContainerException::forMissingConfiguration(AdapterInterface::class, self::class, $requestedName);
}

/** @var class-string<DriverInterface>|class-string<PdoDriverInterface>|null $driverClass */
Expand Down
38 changes: 38 additions & 0 deletions src/Exception/ContainerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,46 @@

final class ContainerException extends RuntimeException implements ContainerExceptionInterface
{
/**
* The ServiceManager dictates the thrown type here, and Laminas annotates
* ServiceNotFoundException @final, so this package owns the wording without owning the
* class.
*/
final public const string MISSING_SERVICE = 'Service "%s" not found in container';

final public const string MISSING_CONFIGURATION = 'No configuration found for %s';

final public const string MISSING_CONFIG_SERVICE = 'Container is missing a config service';

final public const string MISSING_DRIVER = 'no driver configured';

final public const string NON_ADAPTER_AWARE_DELEGATE = 'Delegated service "%s" must implement %s';

final public const string SERVICE = 'Failed to create service "%s" in factory %s Reason: %s';

public static function forMissingConfigService(string $serviceName, string $factoryClass): self
{
return self::forService($serviceName, $factoryClass, self::MISSING_CONFIG_SERVICE);
}

public static function forMissingConfiguration(
string $serviceName,
string $factoryClass,
string $requestedName,
): self {
return self::forService($serviceName, $factoryClass, sprintf(self::MISSING_CONFIGURATION, $requestedName));
}

public static function forMissingDriver(string $serviceName, string $factoryClass): self
{
return self::forService($serviceName, $factoryClass, self::MISSING_DRIVER);
}

public static function forNonAdapterAwareDelegate(string $name, string $interface): self
{
return new self(sprintf(self::NON_ADAPTER_AWARE_DELEGATE, $name, $interface));
}

public static function forService(
string $serviceName,
string $factoryClass,
Expand Down
2 changes: 1 addition & 1 deletion src/ResultSet/AbstractResultSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ abstract class AbstractResultSet implements ResultSetInterface
public function buffer(): ResultSetInterface
{
if ($this->buffer === -2) {
throw new RuntimeException('Buffering must be enabled before iteration is started');
throw RuntimeException::forUnbufferedIteration();
} elseif ($this->buffer === null) {
$this->buffer = [];
if ($this->dataSource instanceof ResultInterface) {
Expand Down
10 changes: 9 additions & 1 deletion src/ResultSet/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@

use PhpDb\Exception;

class RuntimeException extends Exception\RuntimeException {}
class RuntimeException extends Exception\RuntimeException
{
final public const string UNBUFFERED_ITERATION = 'Buffering must be enabled before iteration is started';

public static function forUnbufferedIteration(): self
{
return new self(self::UNBUFFERED_ITERATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function testGetConfigReturnsEmptyWhenContainerHasNoConfig(): void
#[DataProvider('providerInvalidService')]
public function testInvalidService(string $service): void
{
$this->expectException(ServiceNotFoundException::class);
self::expectException(ServiceNotFoundException::class);
$this->serviceManager->get($service);
}

Expand All @@ -111,8 +111,8 @@ public function testInvokeThrowsWhenDriverNotConfigured(): void
$factory = new AbstractAdapterInterfaceFactory();
$factory->canCreate($container, 'PhpDb\Adapter\NoDriver');

$this->expectException(ContainerException::class);
$this->expectExceptionMessage('no driver configured');
self::expectException(ContainerException::class);
self::expectExceptionMessage(ContainerException::MISSING_DRIVER);
$factory($container, 'PhpDb\Adapter\NoDriver');
}

Expand Down
18 changes: 12 additions & 6 deletions test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
use Laminas\ServiceManager\ServiceManager;
use PhpDb\Adapter\Adapter;
use PhpDb\Adapter\AdapterAwareInterface;
use PhpDb\Adapter\AdapterInterface;
use PhpDb\Adapter\Driver\DriverInterface;
use PhpDb\Adapter\Platform\PlatformInterface;
use PhpDb\Container\AdapterInterfaceDelegator;
use PhpDb\Exception\ContainerException;
use PhpDb\Exception\RuntimeException;
use PhpDb\ResultSet\ResultSetInterface;
use PhpDbTest\Adapter\TestAsset\ConcreteAdapterAwareObject;
Expand All @@ -24,6 +26,8 @@
use Psr\Container\NotFoundExceptionInterface;
use stdClass;

use function sprintf;

#[Group('unit')]
#[CoversMethod(AdapterInterfaceDelegator::class, '__construct')]
#[CoversMethod(AdapterInterfaceDelegator::class, '__set_state')]
Expand Down Expand Up @@ -247,8 +251,8 @@ public function testSetAdapterShouldNotBeCalledForMissingAdapter(): void

$callback = static fn(): ConcreteAdapterAwareObject => new ConcreteAdapterAwareObject();

$this->expectException(ServiceNotFoundException::class);
$this->expectExceptionMessage('Service "PhpDb\Adapter\AdapterInterface" not found in container');
self::expectException(ServiceNotFoundException::class);
self::expectExceptionMessage(sprintf(ContainerException::MISSING_SERVICE, AdapterInterface::class));

(new AdapterInterfaceDelegator())(
$container,
Expand All @@ -268,10 +272,12 @@ public function testSetAdapterShouldNotBeCalledForWrongClassInstance(): void

$callback = static fn(): stdClass => new stdClass();

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(
'Delegated service "stdClass" must implement PhpDb\Adapter\AdapterAwareInterface',
);
self::expectException(RuntimeException::class);
self::expectExceptionMessage(sprintf(
ContainerException::NON_ADAPTER_AWARE_DELEGATE,
stdClass::class,
AdapterAwareInterface::class,
));

(new AdapterInterfaceDelegator())(
$container,
Expand Down
8 changes: 4 additions & 4 deletions test/unit/Container/AdapterInterfaceFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ public function testInvokeThrowsWhenAdapterConfigIsEmpty(): void

$factory = new AdapterInterfaceFactory();

$this->expectException(ContainerException::class);
$this->expectExceptionMessage('No configuration found for');
self::expectException(ContainerException::class);
self::expectExceptionMessage('No configuration found for');
$factory($container, AdapterInterface::class);
}

Expand All @@ -130,8 +130,8 @@ public function testInvokeThrowsWhenContainerHasNoConfig(): void

$factory = new AdapterInterfaceFactory();

$this->expectException(ContainerException::class);
$this->expectExceptionMessage('Container is missing a config service');
self::expectException(ContainerException::class);
self::expectExceptionMessage(ContainerException::MISSING_CONFIG_SERVICE);
$factory($container, AdapterInterface::class);
}
}
50 changes: 50 additions & 0 deletions test/unit/Exception/ContainerExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,59 @@
use function sprintf;

#[Group('unit')]
#[CoversMethod(ContainerException::class, 'forMissingConfigService')]
#[CoversMethod(ContainerException::class, 'forMissingConfiguration')]
#[CoversMethod(ContainerException::class, 'forMissingDriver')]
#[CoversMethod(ContainerException::class, 'forNonAdapterAwareDelegate')]
#[CoversMethod(ContainerException::class, 'forService')]
final class ContainerExceptionTest extends TestCase
{
#[Test]
public function forMissingConfigServiceComposesTheReasonIntoTheServiceTemplate(): void
{
self::assertSame(
sprintf(
ContainerException::SERVICE,
'Svc',
'Factory',
ContainerException::MISSING_CONFIG_SERVICE,
),
ContainerException::forMissingConfigService('Svc', 'Factory')->getMessage(),
);
}

#[Test]
public function forMissingConfigurationNamesTheRequestedService(): void
{
self::assertSame(
sprintf(
ContainerException::SERVICE,
'Svc',
'Factory',
sprintf(ContainerException::MISSING_CONFIGURATION, 'Requested'),
),
ContainerException::forMissingConfiguration('Svc', 'Factory', 'Requested')->getMessage(),
);
}

#[Test]
public function forMissingDriverComposesTheReasonIntoTheServiceTemplate(): void
{
self::assertSame(
sprintf(ContainerException::SERVICE, 'Svc', 'Factory', ContainerException::MISSING_DRIVER),
ContainerException::forMissingDriver('Svc', 'Factory')->getMessage(),
);
}

#[Test]
public function forNonAdapterAwareDelegateRendersItsTemplate(): void
{
self::assertSame(
sprintf(ContainerException::NON_ADAPTER_AWARE_DELEGATE, 'Svc', 'SomeInterface'),
ContainerException::forNonAdapterAwareDelegate('Svc', 'SomeInterface')->getMessage(),
);
}

#[Test]
public function forServiceIsAPsrContainerException(): void
{
Expand Down
6 changes: 3 additions & 3 deletions test/unit/ResultSet/AbstractResultSetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ public function testBuffer(): void
]));
$resultSet->next(); // start iterator
// Verify buffer() throws exception when called after iteration starts
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Buffering must be enabled before iteration is started');
self::expectException(RuntimeException::class);
self::expectExceptionMessage(RuntimeException::UNBUFFERED_ITERATION);
$resultSet->buffer();
}

Expand Down Expand Up @@ -248,7 +248,7 @@ public function testInitialize(): void
]));

// Verify invalid data type throws exception
$this->expectException(TypeError::class);
self::expectException(TypeError::class);
/** @noinspection ALL */
$resultSet->initialize('foo');
}
Expand Down
32 changes: 32 additions & 0 deletions test/unit/ResultSet/Exception/RuntimeExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace PhpDbTest\ResultSet\Exception;

use PhpDb\Exception\ExceptionInterface;
use PhpDb\ResultSet\Exception\RuntimeException;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

#[Group('unit')]
#[CoversMethod(RuntimeException::class, 'forUnbufferedIteration')]
final class RuntimeExceptionTest extends TestCase
{
#[Test]
public function forUnbufferedIterationRendersItsTemplate(): void
{
self::assertSame(
RuntimeException::UNBUFFERED_ITERATION,
RuntimeException::forUnbufferedIteration()->getMessage(),
);
}

#[Test]
public function forUnbufferedIterationReturnsTheComponentExceptionType(): void
{
self::assertInstanceOf(ExceptionInterface::class, RuntimeException::forUnbufferedIteration());
}
}
8 changes: 4 additions & 4 deletions test/unit/ResultSet/ResultSetIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ public function testBufferCalledAfterIterationThrowsException(): void
$this->resultSet->current();

// Verify buffer() throws exception when called after iteration has started
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Buffering must be enabled before iteration is started');
self::expectException(RuntimeException::class);
self::expectExceptionMessage(RuntimeException::UNBUFFERED_ITERATION);
$this->resultSet->buffer();
}

Expand Down Expand Up @@ -261,7 +261,7 @@ public function testInvalidDataSourceRaisesException(mixed $dataSource): void
}

// Verify invalid data source throws TypeError
$this->expectException(TypeError::class);
self::expectException(TypeError::class);
$this->resultSet->initialize($dataSource);
}

Expand Down Expand Up @@ -309,7 +309,7 @@ public function testRowObjectPrototypeMayBePassedToConstructor(): void
public function testSettingInvalidReturnTypeRaisesException(mixed $type): void
{
// Verify invalid return type throws TypeError
$this->expectException(TypeError::class);
self::expectException(TypeError::class);
new ResultSet(ResultSet::TYPE_ARRAYOBJECT, $type);
}

Expand Down
Loading