docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform;
use App\Framework\Database\Platform\ValueObjects\ColumnDefinition;
use App\Framework\Database\Platform\ValueObjects\TableOptions;
/**
* Database platform abstraction for SQL dialect differences
*/
interface DatabasePlatform
{
/**
* Get the SQL for auto-increment primary key
*/
public function getAutoIncrementSQL(): string;
/**
* Get the SQL data type for a given column type
*/
public function getColumnTypeSQL(string $type, array $options = []): string;
/**
* Get the SQL to create a table
*
* @param string $table Table name
* @param ColumnDefinition[] $columns Array of column definitions
* @param TableOptions|array $options Table options or legacy array options
*/
public function getCreateTableSQL(string $table, array $columns, array|TableOptions $options = []): string;
/**
* Get the SQL to drop a table
*/
public function getDropTableSQL(string $table, bool $ifExists = false): string;
/**
* Get the SQL to add an index
*/
public function getCreateIndexSQL(string $table, string $indexName, array $columns, array $options = []): string;
/**
* Get the SQL to check if a table exists
*/
public function getTableExistsSQL(string $table): string;
/**
* Get the SQL to list all tables
*/
public function getListTablesSQL(): string;
/**
* Quote an identifier (table or column name)
*/
public function quoteIdentifier(string $identifier): string;
/**
* Get current timestamp expression
*/
public function getCurrentTimestampSQL(): string;
/**
* Get the name of the platform
*/
public function getName(): string;
/**
* Check if the platform supports a feature
*/
public function supportsFeature(string $feature): bool;
/**
* Get SQL for UUID/ULID storage
*/
public function getBinaryUuidSQL(): string;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform;
use App\Framework\Config\Environment;
use App\Framework\DI\Initializer;
/**
* Initializes the database platform based on environment configuration
*/
final readonly class DatabasePlatformInitializer
{
public function __construct(
private Environment $environment
) {
}
#[Initializer]
public function __invoke(): DatabasePlatform
{
$driver = $this->environment->get('DB_DRIVER', 'mysql');
return match($driver) {
'mysql', 'mysqli' => new MySQLPlatform(),
'pgsql', 'postgres', 'postgresql' => throw new \RuntimeException('PostgreSQL platform not yet implemented'),
'sqlite' => throw new \RuntimeException('SQLite platform not yet implemented'),
default => throw new \RuntimeException("Unsupported database driver: {$driver}")
};
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform\Enums;
/**
* Database column types abstraction
*/
enum ColumnType: string
{
// Numeric types
case TINY_INTEGER = 'tinyint';
case SMALL_INTEGER = 'smallint';
case INTEGER = 'integer';
case BIG_INTEGER = 'bigint';
case DECIMAL = 'decimal';
case FLOAT = 'float';
case DOUBLE = 'double';
case BOOLEAN = 'boolean';
// String types
case CHAR = 'char';
case VARCHAR = 'varchar';
case TEXT = 'text';
case MEDIUM_TEXT = 'mediumtext';
case LONG_TEXT = 'longtext';
// Binary types
case BINARY = 'binary';
case VARBINARY = 'varbinary';
case BLOB = 'blob';
case MEDIUM_BLOB = 'mediumblob';
case LONG_BLOB = 'longblob';
// Date/Time types
case DATE = 'date';
case TIME = 'time';
case DATETIME = 'datetime';
case TIMESTAMP = 'timestamp';
case YEAR = 'year';
// JSON type
case JSON = 'json';
// UUID types
case UUID = 'uuid';
case ULID = 'ulid';
public function requiresLength(): bool
{
return match($this) {
self::CHAR, self::VARCHAR, self::BINARY, self::VARBINARY => true,
default => false
};
}
public function requiresPrecision(): bool
{
return match($this) {
self::DECIMAL, self::FLOAT, self::DOUBLE => true,
default => false
};
}
public function isNumeric(): bool
{
return match($this) {
self::TINY_INTEGER, self::SMALL_INTEGER, self::INTEGER, self::BIG_INTEGER,
self::DECIMAL, self::FLOAT, self::DOUBLE, self::BOOLEAN => true,
default => false
};
}
public function isTextual(): bool
{
return match($this) {
self::CHAR, self::VARCHAR, self::TEXT, self::MEDIUM_TEXT, self::LONG_TEXT, self::JSON => true,
default => false
};
}
public function isBinary(): bool
{
return match($this) {
self::BINARY, self::VARBINARY, self::BLOB, self::MEDIUM_BLOB, self::LONG_BLOB,
self::UUID, self::ULID => true,
default => false
};
}
public function isTemporal(): bool
{
return match($this) {
self::DATE, self::TIME, self::DATETIME, self::TIMESTAMP, self::YEAR => true,
default => false
};
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform\Enums;
/**
* Database features that platforms may or may not support
*/
enum DatabaseFeature: string
{
case AUTO_INCREMENT = 'auto_increment';
case FOREIGN_KEYS = 'foreign_keys';
case TRANSACTIONS = 'transactions';
case SAVEPOINTS = 'savepoints';
case JSON_COLUMNS = 'json_columns';
case FULLTEXT_SEARCH = 'fulltext_search';
case SPATIAL_INDEXES = 'spatial_indexes';
case PARTITIONING = 'partitioning';
case VIEWS = 'views';
case STORED_PROCEDURES = 'stored_procedures';
case TRIGGERS = 'triggers';
case UUID_GENERATION = 'uuid_generation';
case ULID_GENERATION = 'ulid_generation';
case CONCURRENT_INDEXES = 'concurrent_indexes';
case PARTIAL_INDEXES = 'partial_indexes';
case EXPRESSION_INDEXES = 'expression_indexes';
case RECURSIVE_CTE = 'recursive_cte';
case WINDOW_FUNCTIONS = 'window_functions';
case UPSERT = 'upsert';
case RETURNING_CLAUSE = 'returning_clause';
public function getDescription(): string
{
return match($this) {
self::AUTO_INCREMENT => 'Automatic incremental primary keys',
self::FOREIGN_KEYS => 'Foreign key constraints',
self::TRANSACTIONS => 'ACID transaction support',
self::SAVEPOINTS => 'Transaction savepoint support',
self::JSON_COLUMNS => 'Native JSON column type',
self::FULLTEXT_SEARCH => 'Full-text search indexes',
self::SPATIAL_INDEXES => 'Spatial/geometric indexes',
self::PARTITIONING => 'Table partitioning',
self::VIEWS => 'Database views',
self::STORED_PROCEDURES => 'Stored procedures and functions',
self::TRIGGERS => 'Database triggers',
self::UUID_GENERATION => 'Built-in UUID generation',
self::ULID_GENERATION => 'Built-in ULID generation',
self::CONCURRENT_INDEXES => 'Non-blocking index creation',
self::PARTIAL_INDEXES => 'Indexes with WHERE conditions',
self::EXPRESSION_INDEXES => 'Indexes on expressions',
self::RECURSIVE_CTE => 'Recursive common table expressions',
self::WINDOW_FUNCTIONS => 'Window/analytic functions',
self::UPSERT => 'INSERT ON DUPLICATE KEY UPDATE',
self::RETURNING_CLAUSE => 'RETURNING clause support'
};
}
public function isAdvancedFeature(): bool
{
return match($this) {
self::PARTITIONING, self::RECURSIVE_CTE, self::WINDOW_FUNCTIONS,
self::EXPRESSION_INDEXES, self::CONCURRENT_INDEXES => true,
default => false
};
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform\Enums;
/**
* Database index types
*/
enum IndexType: string
{
case PRIMARY = 'primary';
case UNIQUE = 'unique';
case INDEX = 'index';
case FULLTEXT = 'fulltext';
case SPATIAL = 'spatial';
public function isUnique(): bool
{
return match($this) {
self::PRIMARY, self::UNIQUE => true,
default => false
};
}
public function requiresSpecialEngine(): bool
{
return match($this) {
self::FULLTEXT, self::SPATIAL => true,
default => false
};
}
public function getKeyword(): string
{
return match($this) {
self::PRIMARY => 'PRIMARY KEY',
self::UNIQUE => 'UNIQUE KEY',
self::INDEX => 'KEY',
self::FULLTEXT => 'FULLTEXT KEY',
self::SPATIAL => 'SPATIAL KEY'
};
}
}

View File

@@ -0,0 +1,371 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform;
use App\Framework\Database\Platform\Enums\ColumnType;
use App\Framework\Database\Platform\Enums\DatabaseFeature;
use App\Framework\Database\Platform\Enums\IndexType;
use App\Framework\Database\Platform\ValueObjects\ColumnDefinition;
use App\Framework\Database\Platform\ValueObjects\IndexDefinition;
use App\Framework\Database\Platform\ValueObjects\TableOptions;
/**
* MySQL/MariaDB platform implementation
*/
final readonly class MySQLPlatform implements DatabasePlatform
{
private array $supportedFeatures;
public function __construct()
{
$this->supportedFeatures = [
DatabaseFeature::AUTO_INCREMENT->value => true,
DatabaseFeature::FOREIGN_KEYS->value => true,
DatabaseFeature::TRANSACTIONS->value => true,
DatabaseFeature::SAVEPOINTS->value => true,
DatabaseFeature::JSON_COLUMNS->value => true,
DatabaseFeature::FULLTEXT_SEARCH->value => true,
DatabaseFeature::SPATIAL_INDEXES->value => true,
DatabaseFeature::PARTITIONING->value => true,
DatabaseFeature::VIEWS->value => true,
DatabaseFeature::STORED_PROCEDURES->value => true,
DatabaseFeature::TRIGGERS->value => true,
DatabaseFeature::UUID_GENERATION->value => false,
DatabaseFeature::ULID_GENERATION->value => false,
DatabaseFeature::CONCURRENT_INDEXES->value => false,
DatabaseFeature::PARTIAL_INDEXES->value => false,
DatabaseFeature::EXPRESSION_INDEXES->value => true,
DatabaseFeature::RECURSIVE_CTE->value => true,
DatabaseFeature::WINDOW_FUNCTIONS->value => true,
DatabaseFeature::UPSERT->value => true,
DatabaseFeature::RETURNING_CLAUSE->value => false,
];
}
public function getName(): string
{
return 'MySQL';
}
public function supportsFeature(string $feature): bool
{
return $this->supportedFeatures[$feature] ?? false;
}
public function getAutoIncrementSQL(): string
{
return 'AUTO_INCREMENT';
}
public function getColumnTypeSQL(string $type, array $options = []): string
{
$columnType = ColumnType::tryFrom($type);
if (! $columnType) {
throw new \InvalidArgumentException("Unknown column type: {$type}");
}
return match($columnType) {
ColumnType::TINY_INTEGER => $this->buildNumericType('TINYINT', $options),
ColumnType::SMALL_INTEGER => $this->buildNumericType('SMALLINT', $options),
ColumnType::INTEGER => $this->buildNumericType('INT', $options),
ColumnType::BIG_INTEGER => $this->buildNumericType('BIGINT', $options),
ColumnType::DECIMAL => $this->buildDecimalType('DECIMAL', $options),
ColumnType::FLOAT => $this->buildDecimalType('FLOAT', $options),
ColumnType::DOUBLE => $this->buildDecimalType('DOUBLE', $options),
ColumnType::BOOLEAN => 'BOOLEAN',
ColumnType::CHAR => $this->buildStringType('CHAR', $options),
ColumnType::VARCHAR => $this->buildStringType('VARCHAR', $options),
ColumnType::TEXT => 'TEXT',
ColumnType::MEDIUM_TEXT => 'MEDIUMTEXT',
ColumnType::LONG_TEXT => 'LONGTEXT',
ColumnType::BINARY => $this->buildBinaryType('BINARY', $options),
ColumnType::VARBINARY => $this->buildBinaryType('VARBINARY', $options),
ColumnType::BLOB => 'BLOB',
ColumnType::MEDIUM_BLOB => 'MEDIUMBLOB',
ColumnType::LONG_BLOB => 'LONGBLOB',
ColumnType::DATE => 'DATE',
ColumnType::TIME => 'TIME',
ColumnType::DATETIME => 'DATETIME',
ColumnType::TIMESTAMP => 'TIMESTAMP',
ColumnType::YEAR => 'YEAR',
ColumnType::JSON => 'JSON',
ColumnType::UUID => 'BINARY(16)',
ColumnType::ULID => 'BINARY(16)',
};
}
public function getCreateTableSQL(string $table, array $columns, array|TableOptions $options = []): string
{
$tableOptions = $options instanceof TableOptions ? $options : TableOptions::default();
$sql = [];
// Table creation start
if ($tableOptions->temporary) {
$sql[] = 'CREATE TEMPORARY TABLE';
} else {
$sql[] = 'CREATE TABLE';
}
if ($tableOptions->ifNotExists) {
$sql[] = 'IF NOT EXISTS';
}
$sql[] = $this->quoteIdentifier($table);
// Column definitions
$columnDefinitions = [];
$indexes = [];
foreach ($columns as $column) {
if ($column instanceof ColumnDefinition) {
$columnDefinitions[] = $this->buildColumnDefinition($column);
// Auto-create primary key index
if ($column->isPrimaryKey()) {
$indexes[] = IndexDefinition::primary('PRIMARY', [$column->name]);
}
// Auto-create unique index for columns with unique option
$options = $column->getOptions();
if ($options->unique) {
$indexName = 'UNQ_' . $table . '_' . $column->name;
$indexes[] = IndexDefinition::unique($indexName, [$column->name], $table);
}
} else {
throw new \InvalidArgumentException('All columns must be ColumnDefinition instances');
}
}
// Index definitions
$indexDefinitions = [];
foreach ($indexes as $index) {
if ($index instanceof IndexDefinition) {
$indexDefinitions[] = $this->buildIndexDefinition($index);
}
}
// Combine column and index definitions
$allDefinitions = array_merge($columnDefinitions, $indexDefinitions);
$sql[] = '(' . implode(', ', $allDefinitions) . ')';
// Table options
$tableOptionsClauses = [];
if ($tableOptions->engine) {
$tableOptionsClauses[] = "ENGINE = {$tableOptions->engine}";
}
if ($tableOptions->charset) {
$tableOptionsClauses[] = "DEFAULT CHARACTER SET = {$tableOptions->charset}";
}
if ($tableOptions->collation) {
$tableOptionsClauses[] = "COLLATE = {$tableOptions->collation}";
}
if ($tableOptions->comment) {
$tableOptionsClauses[] = "COMMENT = " . $this->escapeString($tableOptions->comment);
}
if ($tableOptions->autoIncrementStart !== null) {
$tableOptionsClauses[] = "AUTO_INCREMENT = {$tableOptions->autoIncrementStart}";
}
if (! empty($tableOptionsClauses)) {
$sql[] = implode(' ', $tableOptionsClauses);
}
return implode(' ', $sql);
}
public function getDropTableSQL(string $table, bool $ifExists = false): string
{
$sql = 'DROP TABLE';
if ($ifExists) {
$sql .= ' IF EXISTS';
}
$sql .= ' ' . $this->quoteIdentifier($table);
return $sql;
}
public function getCreateIndexSQL(string $table, string $indexName, array $columns, array $options = []): string
{
$indexType = $options['type'] ?? IndexType::INDEX;
if (is_string($indexType)) {
$indexType = IndexType::from($indexType);
}
$index = new IndexDefinition($indexName, $indexType, $columns, $table, $options);
return "CREATE {$indexType->getKeyword()} {$this->quoteIdentifier($indexName)} ON {$this->quoteIdentifier($table)} ({$this->quoteColumnList($columns)})";
}
public function getTableExistsSQL(string $table): string
{
return "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = " . $this->escapeString($table);
}
public function getListTablesSQL(): string
{
return "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name";
}
public function quoteIdentifier(string $identifier): string
{
return '`' . str_replace('`', '``', $identifier) . '`';
}
public function getCurrentTimestampSQL(): string
{
return 'CURRENT_TIMESTAMP';
}
public function getBinaryUuidSQL(): string
{
// MySQL doesn't have built-in UUID generation, use application-level UUID generation
return 'UNHEX(REPLACE(UUID(), \'-\', \'\'))';
}
private function buildColumnDefinition(ColumnDefinition $column): string
{
$parts = [];
// Column name and type
$parts[] = $this->quoteIdentifier($column->name);
$parts[] = $this->getColumnTypeSQL($column->type->value, [
'length' => $column->length,
'precision' => $column->precision,
'scale' => $column->scale,
'unsigned' => $column->unsigned,
]);
// Character set and collation for text columns
if ($column->type->isTextual() && $column->type !== ColumnType::JSON) {
if ($column->charset !== 'utf8mb4') {
$parts[] = "CHARACTER SET {$column->charset}";
}
if ($column->collation !== 'utf8mb4_unicode_ci') {
$parts[] = "COLLATE {$column->collation}";
}
}
// Nullability
$parts[] = $column->nullable ? 'NULL' : 'NOT NULL';
// Auto increment
if ($column->autoIncrement) {
$parts[] = 'AUTO_INCREMENT';
}
// Default value
if ($column->hasDefault()) {
if (is_string($column->default) && ! in_array($column->default, ['CURRENT_TIMESTAMP', 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'])) {
$parts[] = 'DEFAULT ' . $this->escapeString($column->default);
} else {
$parts[] = "DEFAULT {$column->default}";
}
}
// Comment
if ($column->comment) {
$parts[] = 'COMMENT ' . $this->escapeString($column->comment);
}
return implode(' ', $parts);
}
private function buildIndexDefinition(IndexDefinition $index): string
{
$parts = [];
// Index type keyword
$parts[] = $index->type->getKeyword();
// Index name (except for PRIMARY KEY)
if ($index->type !== IndexType::PRIMARY) {
$parts[] = $this->quoteIdentifier($index->name);
}
// Column list
$parts[] = '(' . $this->quoteColumnList($index->columns) . ')';
return implode(' ', $parts);
}
private function buildNumericType(string $baseType, array $options): string
{
$type = $baseType;
if (! empty($options['length'])) {
$type .= "({$options['length']})";
}
if (! empty($options['unsigned'])) {
$type .= ' UNSIGNED';
}
return $type;
}
private function buildDecimalType(string $baseType, array $options): string
{
$type = $baseType;
if (! empty($options['precision']) && ! empty($options['scale'])) {
$type .= "({$options['precision']},{$options['scale']})";
} elseif (! empty($options['precision'])) {
$type .= "({$options['precision']})";
}
if (! empty($options['unsigned'])) {
$type .= ' UNSIGNED';
}
return $type;
}
private function buildStringType(string $baseType, array $options): string
{
$type = $baseType;
if (! empty($options['length'])) {
$type .= "({$options['length']})";
} else {
$type .= '(255)'; // Default length
}
return $type;
}
private function buildBinaryType(string $baseType, array $options): string
{
$type = $baseType;
if (! empty($options['length'])) {
$type .= "({$options['length']})";
}
return $type;
}
private function quoteColumnList(array $columns): string
{
return implode(', ', array_map([$this, 'quoteIdentifier'], $columns));
}
private function escapeString(string $value): string
{
return "'" . str_replace("'", "''", $value) . "'";
}
}

View File

@@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Platform\ValueObjects\ColumnDefinition;
use App\Framework\Database\Platform\ValueObjects\TableOptions;
use App\Framework\Database\ValueObjects\SqlQuery;
/**
* Fluent schema builder for database-independent table creation
*/
final readonly class SchemaBuilder
{
public function __construct(
private ConnectionInterface $connection,
private DatabasePlatform $platform
) {
}
/**
* Create a new table
*
* @param string $tableName
* @param ColumnDefinition[] $columns
* @param TableOptions $options
*/
public function createTable(string $tableName, array $columns, ?TableOptions $options = null): void
{
$options = $options ?? TableOptions::default();
$sql = $this->platform->getCreateTableSQL($tableName, $columns, $options);
$this->connection->query(SqlQuery::create($sql));
}
/**
* Drop a table
*/
public function dropTable(string $tableName, bool $ifExists = false): void
{
$sql = $this->platform->getDropTableSQL($tableName, $ifExists);
$this->connection->query(SqlQuery::create($sql));
}
/**
* Create an auto-increment primary key column
*/
public function id(string $name = 'id'): ColumnDefinition
{
return ColumnDefinition::id($name);
}
/**
* Create a string/varchar column
*/
public function string(string $name, int $length = 255): ColumnDefinition
{
return ColumnDefinition::string($name, $length);
}
/**
* Create a text column
*/
public function text(string $name): ColumnDefinition
{
return ColumnDefinition::text($name);
}
/**
* Create an integer column
*/
public function integer(string $name): ColumnDefinition
{
return ColumnDefinition::integer($name);
}
/**
* Create a big integer column
*/
public function bigInteger(string $name): ColumnDefinition
{
return ColumnDefinition::bigInteger($name);
}
/**
* Create a decimal column
*/
public function decimal(string $name, int $precision = 8, int $scale = 2): ColumnDefinition
{
return ColumnDefinition::decimal($name, $precision, $scale);
}
/**
* Create a float column
*/
public function float(string $name): ColumnDefinition
{
return ColumnDefinition::float($name);
}
/**
* Create a boolean column
*/
public function boolean(string $name): ColumnDefinition
{
return ColumnDefinition::boolean($name);
}
/**
* Create a date column
*/
public function date(string $name): ColumnDefinition
{
return ColumnDefinition::date($name);
}
/**
* Create a datetime column
*/
public function datetime(string $name): ColumnDefinition
{
return ColumnDefinition::datetime($name);
}
/**
* Create a timestamp column
*/
public function timestamp(string $name): ColumnDefinition
{
return ColumnDefinition::timestamp($name);
}
/**
* Create a binary column
*/
public function binary(string $name, int $length): ColumnDefinition
{
return ColumnDefinition::binary($name, $length);
}
/**
* Create a JSON column
*/
public function json(string $name): ColumnDefinition
{
return ColumnDefinition::json($name);
}
/**
* Create a time column
*/
public function time(string $name): ColumnDefinition
{
return ColumnDefinition::time($name);
}
/**
* Create a year column
*/
public function year(string $name): ColumnDefinition
{
return ColumnDefinition::year($name);
}
/**
* Create timestamps columns (created_at, updated_at)
*
* @return ColumnDefinition[]
*/
public function timestamps(): array
{
return [
ColumnDefinition::timestamp('created_at', false),
ColumnDefinition::timestamp('updated_at', false),
];
}
/**
* Create soft delete column (deleted_at)
*/
public function softDeletes(): ColumnDefinition
{
return ColumnDefinition::timestamp('deleted_at', true);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform;
use App\Framework\Database\ConnectionInterface;
/**
* Factory for creating SchemaBuilder instances with appropriate platform
*/
final readonly class SchemaBuilderFactory
{
public static function create(ConnectionInterface $connection): SchemaBuilder
{
// For now, we'll use MySQL platform
// In a future version, this could detect the database type or accept it as parameter
$platform = new MySQLPlatform();
return new SchemaBuilder($connection, $platform);
}
public static function createForPlatform(ConnectionInterface $connection, DatabasePlatform $platform): SchemaBuilder
{
return new SchemaBuilder($connection, $platform);
}
}

View File

@@ -0,0 +1,315 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform\ValueObjects;
use App\Framework\Database\Platform\Enums\ColumnType;
/**
* Represents a database column definition
*/
final readonly class ColumnDefinition
{
public function __construct(
public string $name,
public ColumnType $type,
public ?int $length = null,
public ?int $precision = null,
public ?int $scale = null,
public bool $nullable = true,
public mixed $default = null,
public bool $autoIncrement = false,
public bool $unsigned = false,
public string $charset = 'utf8mb4',
public string $collation = 'utf8mb4_unicode_ci',
public ?string $comment = null,
public ?ColumnOptions $options = null
) {
if ($type->requiresLength() && $length === null) {
throw new \InvalidArgumentException("Column type {$type->value} requires a length");
}
if ($type->requiresPrecision() && $precision === null) {
throw new \InvalidArgumentException("Column type {$type->value} requires precision");
}
if ($autoIncrement && ! $type->isNumeric()) {
throw new \InvalidArgumentException("Auto-increment is only valid for numeric column types");
}
if ($unsigned && ! $type->isNumeric()) {
throw new \InvalidArgumentException("Unsigned attribute is only valid for numeric column types");
}
}
public function getOptions(): ColumnOptions
{
return $this->options ?? ColumnOptions::default();
}
public static function id(string $name = 'id'): self
{
return new self(
name: $name,
type: ColumnType::BIG_INTEGER,
nullable: false,
autoIncrement: true,
unsigned: true
);
}
public static function string(string $name, int $length = 255, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::VARCHAR,
length: $length,
nullable: $nullable
);
}
public static function text(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::TEXT,
nullable: $nullable
);
}
public static function integer(string $name, bool $nullable = true, bool $unsigned = false): self
{
return new self(
name: $name,
type: ColumnType::INTEGER,
nullable: $nullable,
unsigned: $unsigned
);
}
public static function bigInteger(string $name, bool $nullable = true, bool $unsigned = false): self
{
return new self(
name: $name,
type: ColumnType::BIG_INTEGER,
nullable: $nullable,
unsigned: $unsigned
);
}
public static function decimal(string $name, int $precision = 8, int $scale = 2, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::DECIMAL,
precision: $precision,
scale: $scale,
nullable: $nullable
);
}
public static function boolean(string $name, bool $nullable = true, ?bool $default = null): self
{
return new self(
name: $name,
type: ColumnType::BOOLEAN,
nullable: $nullable,
default: $default
);
}
public static function timestamp(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::TIMESTAMP,
nullable: $nullable
);
}
public static function timestamps(): array
{
return [
new self(
name: 'created_at',
type: ColumnType::TIMESTAMP,
nullable: false,
default: 'CURRENT_TIMESTAMP'
),
new self(
name: 'updated_at',
type: ColumnType::TIMESTAMP,
nullable: false,
default: 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
),
];
}
public static function json(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::JSON,
nullable: $nullable
);
}
public static function uuid(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::UUID,
nullable: $nullable
);
}
public static function binary(string $name, int $length, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::BINARY,
length: $length,
nullable: $nullable
);
}
public static function date(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::DATE,
nullable: $nullable
);
}
public static function time(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::TIME,
nullable: $nullable
);
}
public static function datetime(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::DATETIME,
nullable: $nullable
);
}
public static function year(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::YEAR,
nullable: $nullable
);
}
public static function float(string $name, bool $nullable = true): self
{
return new self(
name: $name,
type: ColumnType::FLOAT,
nullable: $nullable
);
}
public function withDefault(mixed $value): self
{
return new self(
name: $this->name,
type: $this->type,
length: $this->length,
precision: $this->precision,
scale: $this->scale,
nullable: $this->nullable,
default: $value,
autoIncrement: $this->autoIncrement,
unsigned: $this->unsigned,
charset: $this->charset,
collation: $this->collation,
comment: $this->comment,
options: $this->options
);
}
public function withComment(string $comment): self
{
return new self(
name: $this->name,
type: $this->type,
length: $this->length,
precision: $this->precision,
scale: $this->scale,
nullable: $this->nullable,
default: $this->default,
autoIncrement: $this->autoIncrement,
unsigned: $this->unsigned,
charset: $this->charset,
collation: $this->collation,
comment: $comment,
options: $this->options
);
}
public function notNull(): self
{
return $this->notNullable();
}
public function notNullable(): self
{
return new self(
name: $this->name,
type: $this->type,
length: $this->length,
precision: $this->precision,
scale: $this->scale,
nullable: false,
default: $this->default,
autoIncrement: $this->autoIncrement,
unsigned: $this->unsigned,
charset: $this->charset,
collation: $this->collation,
comment: $this->comment,
options: $this->options
);
}
public function unique(): self
{
$currentOptions = $this->getOptions();
$newOptions = $currentOptions->withUnique(true);
return new self(
name: $this->name,
type: $this->type,
length: $this->length,
precision: $this->precision,
scale: $this->scale,
nullable: $this->nullable,
default: $this->default,
autoIncrement: $this->autoIncrement,
unsigned: $this->unsigned,
charset: $this->charset,
collation: $this->collation,
comment: $this->comment,
options: $newOptions
);
}
public function hasDefault(): bool
{
return $this->default !== null;
}
public function isPrimaryKey(): bool
{
return $this->autoIncrement && $this->type->isNumeric();
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform\ValueObjects;
/**
* Value object for column options like unique, index, etc.
*/
final readonly class ColumnOptions
{
public function __construct(
public bool $unique = false,
public bool $index = false,
public ?string $after = null,
public ?string $before = null,
public bool $first = false,
public array $customOptions = []
) {
}
public static function default(): self
{
return new self();
}
public static function unique(): self
{
return new self(unique: true);
}
public static function indexed(): self
{
return new self(index: true);
}
public function withUnique(bool $unique = true): self
{
return new self(
unique: $unique,
index: $this->index,
after: $this->after,
before: $this->before,
first: $this->first,
customOptions: $this->customOptions
);
}
public function withIndex(bool $index = true): self
{
return new self(
unique: $this->unique,
index: $index,
after: $this->after,
before: $this->before,
first: $this->first,
customOptions: $this->customOptions
);
}
public function after(string $columnName): self
{
return new self(
unique: $this->unique,
index: $this->index,
after: $columnName,
before: null,
first: false,
customOptions: $this->customOptions
);
}
public function before(string $columnName): self
{
return new self(
unique: $this->unique,
index: $this->index,
after: null,
before: $columnName,
first: false,
customOptions: $this->customOptions
);
}
public function first(): self
{
return new self(
unique: $this->unique,
index: $this->index,
after: null,
before: null,
first: true,
customOptions: $this->customOptions
);
}
public function withCustomOption(string $key, mixed $value): self
{
$newCustomOptions = $this->customOptions;
$newCustomOptions[$key] = $value;
return new self(
unique: $this->unique,
index: $this->index,
after: $this->after,
before: $this->before,
first: $this->first,
customOptions: $newCustomOptions
);
}
public function hasCustomOption(string $key): bool
{
return array_key_exists($key, $this->customOptions);
}
public function getCustomOption(string $key, mixed $default = null): mixed
{
return $this->customOptions[$key] ?? $default;
}
/**
* Convert to legacy array format for backwards compatibility
*/
public function toArray(): array
{
$array = [
'unique' => $this->unique,
'index' => $this->index,
];
if ($this->after) {
$array['after'] = $this->after;
}
if ($this->before) {
$array['before'] = $this->before;
}
if ($this->first) {
$array['first'] = $this->first;
}
return array_merge($array, $this->customOptions);
}
/**
* Create from legacy array format
*/
public static function fromArray(array $options): self
{
$customOptions = $options;
unset($customOptions['unique'], $customOptions['index'], $customOptions['after'], $customOptions['before'], $customOptions['first']);
return new self(
unique: $options['unique'] ?? false,
index: $options['index'] ?? false,
after: $options['after'] ?? null,
before: $options['before'] ?? null,
first: $options['first'] ?? false,
customOptions: $customOptions
);
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform\ValueObjects;
use App\Framework\Database\Platform\Enums\IndexType;
/**
* Represents a database index definition
*/
final readonly class IndexDefinition
{
public function __construct(
public string $name,
public IndexType $type,
public array $columns,
public ?string $tableName = null,
public array $options = []
) {
if (empty($columns)) {
throw new \InvalidArgumentException('Index must have at least one column');
}
foreach ($columns as $column) {
if (! is_string($column) || trim($column) === '') {
throw new \InvalidArgumentException('All index columns must be non-empty strings');
}
}
}
public static function primary(string $name = 'PRIMARY', array $columns = ['id']): self
{
return new self($name, IndexType::PRIMARY, $columns);
}
public static function unique(string $name, array $columns): self
{
return new self($name, IndexType::UNIQUE, $columns);
}
public static function index(string $name, array $columns): self
{
return new self($name, IndexType::INDEX, $columns);
}
public static function fulltext(string $name, array $columns): self
{
return new self($name, IndexType::FULLTEXT, $columns);
}
public static function spatial(string $name, array $columns): self
{
return new self($name, IndexType::SPATIAL, $columns);
}
public function withTable(string $tableName): self
{
return new self(
name: $this->name,
type: $this->type,
columns: $this->columns,
tableName: $tableName,
options: $this->options
);
}
public function withOptions(array $options): self
{
return new self(
name: $this->name,
type: $this->type,
columns: $this->columns,
tableName: $this->tableName,
options: array_merge($this->options, $options)
);
}
public function getFullName(): string
{
if ($this->tableName) {
return "{$this->tableName}_{$this->name}";
}
return $this->name;
}
public function isComposite(): bool
{
return count($this->columns) > 1;
}
public function getFirstColumn(): string
{
return $this->columns[0];
}
public function getColumnsString(): string
{
return implode(', ', $this->columns);
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Platform\ValueObjects;
/**
* Represents table creation options
*/
final readonly class TableOptions
{
public function __construct(
public string $engine = 'InnoDB',
public string $charset = 'utf8mb4',
public string $collation = 'utf8mb4_unicode_ci',
public ?string $comment = null,
public ?int $autoIncrementStart = null,
public bool $temporary = false,
public bool $ifNotExists = true,
public array $partitioning = [],
public array $customOptions = []
) {
}
public static function default(): self
{
return new self();
}
public static function temporary(): self
{
return new self(temporary: true);
}
public static function myISAM(): self
{
return new self(engine: 'MyISAM');
}
public static function memory(): self
{
return new self(engine: 'MEMORY');
}
public function withComment(string $comment): self
{
return new self(
engine: $this->engine,
charset: $this->charset,
collation: $this->collation,
comment: $comment,
autoIncrementStart: $this->autoIncrementStart,
temporary: $this->temporary,
ifNotExists: $this->ifNotExists,
partitioning: $this->partitioning,
customOptions: $this->customOptions
);
}
public function withCharset(string $charset, ?string $collation = null): self
{
return new self(
engine: $this->engine,
charset: $charset,
collation: $collation ?? $this->collation,
comment: $this->comment,
autoIncrementStart: $this->autoIncrementStart,
temporary: $this->temporary,
ifNotExists: $this->ifNotExists,
partitioning: $this->partitioning,
customOptions: $this->customOptions
);
}
public function withCollation(string $collation): self
{
return new self(
engine: $this->engine,
charset: $this->charset,
collation: $collation,
comment: $this->comment,
autoIncrementStart: $this->autoIncrementStart,
temporary: $this->temporary,
ifNotExists: $this->ifNotExists,
partitioning: $this->partitioning,
customOptions: $this->customOptions
);
}
public function withEngine(string $engine): self
{
return new self(
engine: $engine,
charset: $this->charset,
collation: $this->collation,
comment: $this->comment,
autoIncrementStart: $this->autoIncrementStart,
temporary: $this->temporary,
ifNotExists: $this->ifNotExists,
partitioning: $this->partitioning,
customOptions: $this->customOptions
);
}
public function withIfNotExists(bool $ifNotExists = true): self
{
return new self(
engine: $this->engine,
charset: $this->charset,
collation: $this->collation,
comment: $this->comment,
autoIncrementStart: $this->autoIncrementStart,
temporary: $this->temporary,
ifNotExists: $ifNotExists,
partitioning: $this->partitioning,
customOptions: $this->customOptions
);
}
public function withAutoIncrementStart(int $start): self
{
return new self(
engine: $this->engine,
charset: $this->charset,
collation: $this->collation,
comment: $this->comment,
autoIncrementStart: $start,
temporary: $this->temporary,
ifNotExists: $this->ifNotExists,
partitioning: $this->partitioning,
customOptions: $this->customOptions
);
}
public function withoutIfNotExists(): self
{
return new self(
engine: $this->engine,
charset: $this->charset,
collation: $this->collation,
comment: $this->comment,
autoIncrementStart: $this->autoIncrementStart,
temporary: $this->temporary,
ifNotExists: false,
partitioning: $this->partitioning,
customOptions: $this->customOptions
);
}
public function withCustomOption(string $key, mixed $value): self
{
return new self(
engine: $this->engine,
charset: $this->charset,
collation: $this->collation,
comment: $this->comment,
autoIncrementStart: $this->autoIncrementStart,
temporary: $this->temporary,
ifNotExists: $this->ifNotExists,
partitioning: $this->partitioning,
customOptions: array_merge($this->customOptions, [$key => $value])
);
}
public function isUtf8(): bool
{
return str_starts_with($this->charset, 'utf8');
}
public function supportsFulltext(): bool
{
return in_array($this->engine, ['InnoDB', 'MyISAM']);
}
public function supportsTransactions(): bool
{
return $this->engine === 'InnoDB';
}
public function supportsRowLocking(): bool
{
return $this->engine === 'InnoDB';
}
}