Skip to content
Open
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
62 changes: 62 additions & 0 deletions docs/book/sql-ddl/alter-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,68 @@ $fk = new ForeignKey(
);
```

### Table Prefixes

`TableIdentifier` accepts an optional prefix and separator. When a prefix is
set, `getTable()` and `getTableAndSchema()` return the prefixed table name;
`getUnprefixedTable()` returns the table name as given:

```php title="Prefixed Table Identifiers"
use PhpDb\Sql\TableIdentifier;

$identifier = new TableIdentifier('users', null, 'backup');
$identifier->getTable(); // 'backup_users'
$identifier->getUnprefixedTable(); // 'users'

// Custom separator
$identifier = new TableIdentifier('users', null, 'backup', '__');
$identifier->getTable(); // 'backup__users'
```

The prefix must be a non-empty string or `null`. The separator defaults to
`'_'` and must also be a non-empty string; passing `''` for either throws
`PhpDb\Sql\Exception\InvalidArgumentException`.

### The TableIdentifierFactory

`PhpDb\Sql\TableIdentifierFactory` is a callable factory that creates
`TableIdentifier` instances sharing a preconfigured prefix and separator:

```php title="Creating Identifiers via the Factory"
use PhpDb\Sql\TableIdentifierFactory;

$factory = new TableIdentifierFactory('backup');

$factory('users')->getTable(); // 'backup_users'
$factory('orders', 'sales')->getTable(); // 'backup_orders'

// A prefix or separator passed at call time overrides the configured one
$factory('users', null, 'archive')->getTable(); // 'archive_users'
```

The configured separator is only applied when a prefix is passed, so a factory
created with a separator but no prefix produces unprefixed identifiers. Passing
`null` as the separator falls back to the default `'_'`; passing `''` — at
construction or at call time — throws
`PhpDb\Sql\Exception\InvalidArgumentException`.

`PhpDb\ConfigProvider` registers the factory as a container service. Configure
the prefix and separator through the application config:

```php title="Configuring the Factory Service"
use PhpDb\Sql\TableIdentifierFactory;

return [
TableIdentifierFactory::class => [
'prefix' => 'backup',
'separator' => '_',
],
];
```

When no configuration is present the service is created without a prefix, and
the separator defaults to `'_'`.

## Nullable and Default Values

### Setting Nullable
Expand Down
5 changes: 3 additions & 2 deletions src/ConfigProvider.php
Comment thread
simon-mundy marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace PhpDb;

final class ConfigProvider
final readonly class ConfigProvider
{
public const NAMED_ADAPTER_KEY = 'adapters';

Expand All @@ -25,7 +25,8 @@ public function getDependencies(): array
Adapter\AdapterInterface::class => Adapter\Adapter::class,
],
'factories' => [
Adapter\Adapter::class => Container\AdapterInterfaceFactory::class,
Adapter\Adapter::class => Container\AdapterInterfaceFactory::class,
Sql\TableIdentifierFactory::class => Container\TableIdentifierFactoryFactory::class,
],
];
}
Expand Down
44 changes: 44 additions & 0 deletions src/Container/TableIdentifierFactoryFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace PhpDb\Container;

use PhpDb\Sql\TableIdentifier;
use PhpDb\Sql\TableIdentifierFactory;
use Psr\Container\ContainerInterface;

/**
* Creates a {@see TableIdentifierFactory} configured from the application
* config service.
*
* Expected configuration:
*
* <code>
* return [
* TableIdentifierFactory::class => [
* 'prefix' => 'backup',
* 'separator' => '_',
* ],
* ];
* </code>
*
* When no configuration is present the factory is created without a prefix.
* The separator defaults to '_' when not configured.
*/
final class TableIdentifierFactoryFactory
{
public function __invoke(ContainerInterface $container): TableIdentifierFactory
{
/** @var array<string, mixed> $config */
$config = $container->has('config') ? $container->get('config') ?? [] : [];

/** @var array{prefix?: string|null, separator?: string} $factoryConfig */
$factoryConfig = $config[TableIdentifierFactory::class] ?? [];

return new TableIdentifierFactory(
$factoryConfig['prefix'] ?? null,
$factoryConfig['separator'] ?? TableIdentifier::SEPARATOR,
);
}
}
67 changes: 52 additions & 15 deletions src/Sql/TableIdentifier.php
Comment thread
simon-mundy marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,75 @@

namespace PhpDb\Sql;

class TableIdentifier
final readonly class TableIdentifier
{
protected string $table;
public const SEPARATOR = '_';

protected ?string $schema = null;

public function __construct(string $table, ?string $schema = null)
{
/**
* @throws Exception\InvalidArgumentException If $table, $schema, $prefix or $separator is an empty string.
*/
public function __construct(
protected string $table,
protected ?string $schema = null,
protected ?string $prefix = null,
protected ?string $separator = self::SEPARATOR,
) {
if ('' === $table) {
throw new Exception\InvalidArgumentException(
'$table must be a valid table name, empty string given'
);
}

$this->table = $table;
if ('' === $schema) {
throw new Exception\InvalidArgumentException(
'$schema must be a valid schema name or null, empty string given'
);
}

if ($schema !== null) {
if ('' === $schema) {
throw new Exception\InvalidArgumentException(
'$schema must be a valid schema name or null, empty string given'
);
}
if ('' === $prefix) {
throw new Exception\InvalidArgumentException(
'$prefix must be a valid table prefix or null, empty string given'
);
}

$this->schema = $schema;
if ('' === $separator) {
throw new Exception\InvalidArgumentException(
'$separator must be a valid table separator, empty string given'
);
}
}

/**
* Returns the table name with the prefix and separator applied, when a
* prefix is set.
*/
public function getTable(): string
{
if ($this->prefix === null) {
return $this->table;
}

return $this->prefix . $this->separator . $this->table;
}

/**
* Returns the table name as given, without the prefix applied.
*/
public function getUnprefixedTable(): string
{
return $this->table;
}

public function getPrefix(): ?string
{
return $this->prefix;
}

public function getSeparator(): string
{
return $this->separator;
}

public function getSchema(): ?string
{
return $this->schema;
Expand All @@ -44,6 +81,6 @@ public function getSchema(): ?string
/** @return array{0: string, 1: null|string} */
public function getTableAndSchema(): array
{
return [$this->table, $this->schema];
return [$this->getTable(), $this->schema];
}
}
65 changes: 65 additions & 0 deletions src/Sql/TableIdentifierFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace PhpDb\Sql;

/**
* Callable factory producing {@see TableIdentifier} instances with a
* preconfigured table prefix and separator.
*
* Register via {@see \PhpDb\ConfigProvider} and configure the prefix once
* (e.g. in application config) to have every identifier created through the
* factory share the same prefix — convenient for creating backup_* tables
* during a migration.
*/
final readonly class TableIdentifierFactory
{
/**
* @param null|string $separator Null falls back to {@see TableIdentifier::SEPARATOR}.
* @throws Exception\InvalidArgumentException If $prefix or $separator is an empty string.
*/
public function __construct(
private ?string $prefix = null,
private ?string $separator = TableIdentifier::SEPARATOR,
) {
if ('' === $prefix) {
throw new Exception\InvalidArgumentException(
'$prefix must be a valid table prefix or null, empty string given'
);
}

if ('' === $separator) {
throw new Exception\InvalidArgumentException(
'$separator must be a valid table separator, empty string given'
);
}
}

public function getPrefix(): ?string
{
return $this->prefix;
}

public function getSeparator(): string
{
return $this->separator ?? TableIdentifier::SEPARATOR;
}

/**
* Creates a TableIdentifier carrying the configured prefix and separator.
*
* A prefix or separator passed at call time takes precedence over the
* configured one.
*
* @throws Exception\InvalidArgumentException If $prefix or $separator is an empty string.
*/
public function __invoke(
string $table,
?string $schema = null,
?string $prefix = null,
?string $separator = null,
): TableIdentifier {
return new TableIdentifier($table, $schema, $prefix ?? $this->prefix, $separator ?? $this->getSeparator());
}
}
8 changes: 7 additions & 1 deletion test/unit/ConfigProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
use PhpDb\Adapter;
use PhpDb\ConfigProvider;
use PhpDb\Container;
use PhpDb\Sql;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

#[CoversClass(ConfigProvider::class)]
#[Group('unit')]
class ConfigProviderTest extends TestCase
{
/**
Expand All @@ -29,7 +34,8 @@ class ConfigProviderTest extends TestCase
Adapter\AdapterInterface::class => Adapter\Adapter::class,
],
'factories' => [
Adapter\Adapter::class => Container\AdapterInterfaceFactory::class,
Adapter\Adapter::class => Container\AdapterInterfaceFactory::class,
Sql\TableIdentifierFactory::class => Container\TableIdentifierFactoryFactory::class,
],
],
];
Expand Down
Loading