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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"files": [
"src/internal/filesystem/functions.php",
"src/internal/functions.php"
"src/package/functions.php"
]
},
"autoload-dev": {
Expand Down
4 changes: 3 additions & 1 deletion src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace dbschemix\core;

use dbschemix\core\exception\ConfigurationException;
use dbschemix\core\internal\template\Factory;
use dbschemix\core\template\FactoryInterface;

/**
* @api
Expand All @@ -17,7 +19,7 @@
*/
public function __construct(
public string $table = 'migration',
public template\FactoryInterface $templFactory = new template\Factory(),
public FactoryInterface $templFactory = new Factory(),
) {
if (preg_match('/^\w+$/', $table) !== 1) {
throw new ConfigurationException(
Expand Down
1 change: 1 addition & 0 deletions src/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace dbschemix\core;

/**
* @api
* @infection-ignore-all IncrementInteger
*/
final readonly class Context
Expand Down
23 changes: 8 additions & 15 deletions src/command/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
use dbschemix\core\Context;

/**
* @psalm-internal dbschemix
* SQL default command
*
* @api
*/
final readonly class Command implements CommandInterface
{
Expand All @@ -37,20 +39,11 @@ public function fetchApplied(Options $options = new Options()): array
$limit = 'LIMIT ' . $options->limit;
}

/** @var non-empty-string $query */
$query = str_replace(
[
':table',
'[WHERE]',
'[LIMIT]',
],
[
$this->config->table,
$where,
$limit,
],
'SELECT name, version FROM :table [WHERE] ORDER BY atime DESC, name DESC [LIMIT]',
);
$query = 'SELECT name, version FROM '
. $this->config->table
. ($where !== '' ? ' ' . $where : '')
. ' ORDER BY atime DESC, name DESC'
. ($limit !== '' ? ' ' . $limit : '');

return $this->connection->fetchRecord($query, $params);
}
Expand Down
24 changes: 24 additions & 0 deletions src/command/CommandInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@
use Throwable;
use dbschemix\core\Context;

/**
* Command port: the contract a database driver implementation fulfills to
* run core migration workflow operations.
*
* Audience: driver implementors (the in-tree adapter
* `internal\command\Command` plus downstream packages such as
* `dbschemix/pdo`, `dbschemix/pgsql`, `dbschemix/clickhouse`). End consumers
* interact with `Migrator`, whose exception contract is fully typed.
*
* Exception policy: each method below is declared `@throws Throwable`
* intentionally. A driver implementation may surface whatever its
* underlying client throws (\PDOException, mysqli_sql_exception, ClickHouse
* client exceptions, ...) plus library-typed exceptions such as
* `PrepareException`. The core workflow catches these at its boundary
* (Workflow::run, Workflow::getAppliedMigrations) and wraps them into
* typed `MigratorException` subtypes — `ActionException` for migration
* failures, `InitializationException` for system-state reads — so the
* typed exception contract is restored before reaching the consumer of
* `Migrator`. Driver implementors are NOT required to pre-wrap their
* underlying exceptions; the workflow's `catch (Throwable)` is the
* wrapping point by design.
*
* @api
*/
interface CommandInterface
{
/**
Expand Down
3 changes: 3 additions & 0 deletions src/command/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use dbschemix\core\InputOptions;

/**
* @api
*/
final readonly class Options
{
/**
Expand Down
3 changes: 3 additions & 0 deletions src/connection/ConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace dbschemix\core\connection;

/**
* @api
*/
interface ConnectionInterface extends StatementInterface
{
public function beginTransaction(): TransactionInterface;
Expand Down
3 changes: 3 additions & 0 deletions src/connection/DriverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use dbschemix\core\exception\ConnectionException;
use dbschemix\core\Config;

/**
* @api
*/
interface DriverInterface
{
/**
Expand Down
3 changes: 3 additions & 0 deletions src/connection/StatementInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace dbschemix\core\connection;

/**
* @api
*/
interface StatementInterface
{
/**
Expand Down
3 changes: 3 additions & 0 deletions src/connection/TransactionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace dbschemix\core\connection;

/**
* @api
*/
interface TransactionInterface extends StatementInterface
{
public function isActive(): bool;
Expand Down
50 changes: 50 additions & 0 deletions src/event/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,69 @@

namespace dbschemix\core\event;

/**
* Library-wide event identifiers and the stable enum-to-payload mapping.
*
* Each case below documents the concrete {@see EventInterface} implementation
* a subscriber will receive when the event fires. Subscribers that need the
* structured payload (Context, Throwable, dbName, action) may safely
* `instanceof` onto the dispatched class — the mapping is part of the @api
* contract.
*
* @api
*/
enum Event: string
{
/**
* Reading the migration-tracking table failed during initialization.
*
* Dispatched payload: {@see ExceptionEvent} (dbName, exception).
*/
case InitializationError = 'initialization-error-event';

/**
* The driver could not open a connection or build a CommandInterface.
*
* Dispatched payload: {@see ExceptionEvent} (dbName, exception).
*/
case ConnectionError = 'connection-error-event';

/**
* Configuration is invalid (requested database is not in the list,
* table name is malformed, required arguments missing, ...).
*
* Dispatched payload: {@see ExceptionEvent} (dbName, exception).
*/
case ConfigurationError = 'configuration-error-event';

/**
* A filesystem operation failed (missing migration directory, unwritable
* path, unreadable file, ...).
*
* Dispatched payload: {@see ExceptionEvent} (dbName, exception).
*/
case FilesystemError = 'filesystem-error-event';

/**
* Filesystem soft event: directory exists but contains no migration files.
* Informational; not an error. The carried exception holds the notice
* message for logging convenience.
*
* Dispatched payload: {@see ExceptionEvent} (dbName, exception).
*/
case FilesystemNotice = 'filesystem-notice-event';

/**
* A single migration file has been applied successfully.
*
* Dispatched payload: {@see MigrateSuccessEvent} (action, context).
*/
case MigrateSuccess = 'migrate-success-event';

/**
* A single migration file failed to apply.
*
* Dispatched payload: {@see MigrateErrorEvent} (action, context, exception).
*/
case MigrateError = 'migrate-error-event';
}
3 changes: 3 additions & 0 deletions src/event/EventAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace dbschemix\core\event;

/**
* @psalm-internal dbschemix\core
*/
enum EventAction
{
case up;
Expand Down
36 changes: 32 additions & 4 deletions src/event/EventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace dbschemix\core\event;

use Closure;
use Throwable;

/**
Expand All @@ -12,7 +13,7 @@
final readonly class EventDispatcher
{
/**
* @var array<string, list<callable(Event $name, EventInterface $event):void>>
* @var array<string, list<Closure(Event $name, EventInterface $event):void>>
*/
private array $eventHandlers;

Expand All @@ -23,21 +24,48 @@ public function __construct(array $eventSubscribers)
{
$subscriptions = [];
foreach ($eventSubscribers as $subscriber) {
foreach ($subscriber->subscriptions() as $name => $callback) {
$subscriptions[$name][] = $callback;
foreach ($subscriber->subscriptions() as $subscription) {
$subscriptions[$subscription->event->value][] = $subscription->callback;
}
}

$this->eventHandlers = $subscriptions;
}

/**
* Notifies subscribers of an event.
*
* Subscriber callbacks MUST NOT throw. If one does, the exception is captured
* and reported through trigger_error(E_USER_WARNING); the loop continues so
* remaining subscribers still receive the event, and the migration is not
* aborted by a subscriber bug.
*
* This isolation is intentional: trigger() is called from inside the migration's
* own try/catch flow (see Workflow::run()), and an exception propagating out
* of a subscriber would be misattributed as a migration failure — corrupting the
* success/error event contract and potentially shadowing the original migration
* error.
*
* Hosts that want fail-fast on subscriber failures may install a strict error
* handler (set_error_handler) that converts E_USER_WARNING into an exception;
* that is an explicit opt-in by the host.
*/
public function trigger(Event $name, EventInterface $event): void
{
if (array_key_exists($name->value, $this->eventHandlers)) {
foreach ($this->eventHandlers[$name->value] as $subscriberCallback) {
try {
$subscriberCallback($name, $event);
} catch (Throwable) {
} catch (Throwable $exception) {
trigger_error(
sprintf(
'dbschemix\core: subscriber callback for event "%s" failed (%s): %s',
$name->value,
$exception::class,
$exception->getMessage(),
),
E_USER_WARNING,
);
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/event/EventInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@

namespace dbschemix\core\event;

/**
* Common surface across all dispatched events.
*
* Provides generic-logging-friendly accessors (getName, getMessage) suitable
* for subscribers that only need a human-readable line. Structured payload
* data (Context, Throwable, dbName, action) lives on the concrete event
* class — {@see MigrateSuccessEvent}, {@see MigrateErrorEvent},
* {@see ExceptionEvent} — as public readonly properties.
*
* Subscribers that need structured data should type-narrow via `instanceof`
* onto the concrete class. The Event enum case → concrete event class
* mapping is part of the @api contract and is documented per case in
* {@see Event}.
*
* @api
*/
interface EventInterface
{
/**
Expand Down
12 changes: 0 additions & 12 deletions src/event/EventPublisherInterface.php

This file was deleted.

13 changes: 12 additions & 1 deletion src/event/EventSubscriberInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@

namespace dbschemix\core\event;

/**
* @api
*/
interface EventSubscriberInterface
{
/**
* @return array<string, callable(Event $name, EventInterface $event):void>
* Returns the list of (event, callback) subscriptions this subscriber wants.
*
* Subscriber callbacks MUST NOT throw. If a callback throws, the library
* reports the failure via trigger_error(E_USER_WARNING) and continues:
* other subscribers still receive the event, and the migration is not
* aborted. Subscribers that need to react to their own failures must
* wrap their body in their own try/catch.
*
* @return list<Subscription>
*/
public function subscriptions(): array;
}
3 changes: 3 additions & 0 deletions src/event/ExceptionEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Override;
use Throwable;

/**
* @api
*/
final readonly class ExceptionEvent implements EventInterface
{
/**
Expand Down
5 changes: 4 additions & 1 deletion src/event/MigrateErrorEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use Throwable;
use dbschemix\core\Context;

/**
* @api
*/
final readonly class MigrateErrorEvent implements EventInterface
{
/**
Expand All @@ -30,7 +33,7 @@ public function getName(): string
public function getMessage(): string
{
return sprintf(
"[%s] %s: %s, vers: %d\r\n%s",
"[%s] %s: %s, vers: %d\n%s",
$this->context->dbName,
$this->action,
$this->context->filename,
Expand Down
3 changes: 3 additions & 0 deletions src/event/MigrateSuccessEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Override;
use dbschemix\core\Context;

/**
* @api
*/
final readonly class MigrateSuccessEvent implements EventInterface
{
/**
Expand Down
Loading
Loading