feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -24,7 +24,7 @@ final readonly class DatabasePlatformInitializer
return match($driver) {
'mysql', 'mysqli' => new MySQLPlatform(),
'pgsql', 'postgres', 'postgresql' => throw new \RuntimeException('PostgreSQL platform not yet implemented'),
'pgsql', 'postgres', 'postgresql' => new PostgreSQLPlatform(),
'sqlite' => throw new \RuntimeException('SQLite platform not yet implemented'),
default => throw new \RuntimeException("Unsupported database driver: {$driver}")
};

View File

@@ -0,0 +1,375 @@
<?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;
/**
* PostgreSQL platform implementation with native features
*
* Supports PostgreSQL-specific features:
* - SERIAL/BIGSERIAL for auto-increment
* - JSONB for optimized JSON storage
* - Native UUID generation
* - Advanced indexing (GiST, GIN, BRIN)
* - RETURNING clause
* - Partial and concurrent indexes
*/
final readonly class PostgreSQLPlatform implements DatabasePlatform
{
private array $supportedFeatures;
public function __construct()
{
$this->supportedFeatures = [
DatabaseFeature::AUTO_INCREMENT->value => true, // via SERIAL
DatabaseFeature::FOREIGN_KEYS->value => true,
DatabaseFeature::TRANSACTIONS->value => true,
DatabaseFeature::SAVEPOINTS->value => true,
DatabaseFeature::JSON_COLUMNS->value => true, // JSONB
DatabaseFeature::FULLTEXT_SEARCH->value => true, // via tsvector
DatabaseFeature::SPATIAL_INDEXES->value => true, // PostGIS
DatabaseFeature::PARTITIONING->value => true, // Native partitioning
DatabaseFeature::VIEWS->value => true,
DatabaseFeature::STORED_PROCEDURES->value => true,
DatabaseFeature::TRIGGERS->value => true,
DatabaseFeature::UUID_GENERATION->value => true, // uuid-ossp extension
DatabaseFeature::ULID_GENERATION->value => false, // App-level
DatabaseFeature::CONCURRENT_INDEXES->value => true, // CREATE INDEX CONCURRENTLY
DatabaseFeature::PARTIAL_INDEXES->value => true, // WHERE clause
DatabaseFeature::EXPRESSION_INDEXES->value => true,
DatabaseFeature::RECURSIVE_CTE->value => true,
DatabaseFeature::WINDOW_FUNCTIONS->value => true,
DatabaseFeature::UPSERT->value => true, // ON CONFLICT
DatabaseFeature::RETURNING_CLAUSE->value => true, // RETURNING *
];
}
public function getName(): string
{
return 'PostgreSQL';
}
public function supportsFeature(string $feature): bool
{
return $this->supportedFeatures[$feature] ?? false;
}
public function getAutoIncrementSQL(): string
{
// PostgreSQL uses SERIAL type instead of AUTO_INCREMENT
return 'GENERATED BY DEFAULT AS IDENTITY';
}
public function getColumnTypeSQL(string $type, array $options = []): string
{
$columnType = ColumnType::tryFrom($type);
if (! $columnType) {
throw new \InvalidArgumentException("Unknown column type: {$type}");
}
return match($columnType) {
// Integer types
ColumnType::TINY_INTEGER => 'SMALLINT', // PostgreSQL: no TINYINT
ColumnType::SMALL_INTEGER => 'SMALLINT',
ColumnType::INTEGER => 'INTEGER',
ColumnType::BIG_INTEGER => 'BIGINT',
// Decimal types
ColumnType::DECIMAL => $this->buildDecimalType('NUMERIC', $options),
ColumnType::FLOAT => 'REAL',
ColumnType::DOUBLE => 'DOUBLE PRECISION',
ColumnType::BOOLEAN => 'BOOLEAN',
// String types
ColumnType::CHAR => $this->buildStringType('CHAR', $options),
ColumnType::VARCHAR => $this->buildStringType('VARCHAR', $options),
ColumnType::TEXT => 'TEXT',
ColumnType::MEDIUM_TEXT => 'TEXT', // PostgreSQL: no MEDIUMTEXT
ColumnType::LONG_TEXT => 'TEXT', // PostgreSQL: no LONGTEXT
// Binary types
ColumnType::BINARY => $this->buildBinaryType('BYTEA', $options),
ColumnType::VARBINARY => 'BYTEA',
ColumnType::BLOB => 'BYTEA',
ColumnType::MEDIUM_BLOB => 'BYTEA',
ColumnType::LONG_BLOB => 'BYTEA',
// Date/Time types
ColumnType::DATE => 'DATE',
ColumnType::TIME => 'TIME',
ColumnType::DATETIME => 'TIMESTAMP', // PostgreSQL: TIMESTAMP instead of DATETIME
ColumnType::TIMESTAMP => 'TIMESTAMP',
ColumnType::YEAR => 'SMALLINT', // PostgreSQL: no YEAR type
// Special types
ColumnType::JSON => 'JSONB', // JSONB for better performance
ColumnType::UUID => 'UUID', // Native UUID type
ColumnType::ULID => 'BYTEA', // Store as binary
};
}
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 constraint
if ($column->isPrimaryKey()) {
$indexes[] = IndexDefinition::primary('PRIMARY', [$column->name]);
}
// Auto-create unique constraint for columns with unique option
$colOptions = $column->getOptions();
if ($colOptions->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 (constraints in PostgreSQL)
$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) . ')';
// PostgreSQL-specific table options
// Note: PostgreSQL doesn't support ENGINE or CHARSET table options like MySQL
// Encoding is set at database level, not table level
return implode(' ', $sql);
}
public function getDropTableSQL(string $table, bool $ifExists = false): string
{
$sql = 'DROP TABLE';
if ($ifExists) {
$sql .= ' IF EXISTS';
}
$sql .= ' ' . $this->quoteIdentifier($table);
// PostgreSQL: CASCADE option to drop dependent objects
// $sql .= ' CASCADE'; // Optional, use with caution
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);
}
$concurrent = $options['concurrent'] ?? false;
$unique = $indexType === IndexType::UNIQUE;
$sql = 'CREATE';
if ($unique) {
$sql .= ' UNIQUE';
}
$sql .= ' INDEX';
if ($concurrent) {
$sql .= ' CONCURRENTLY';
}
$sql .= ' ' . $this->quoteIdentifier($indexName);
$sql .= ' ON ' . $this->quoteIdentifier($table);
// Index method (BTREE, HASH, GIN, GIST, BRIN)
$method = $options['method'] ?? 'BTREE';
$sql .= ' USING ' . $method;
$sql .= ' (' . $this->quoteColumnList($columns) . ')';
// WHERE clause for partial indexes
if (isset($options['where'])) {
$sql .= ' WHERE ' . $options['where'];
}
return $sql;
}
public function getTableExistsSQL(string $table): string
{
return "SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = " . $this->escapeString($table) . "
)";
}
public function getListTablesSQL(): string
{
return "SELECT tablename FROM pg_catalog.pg_tables
WHERE schemaname = 'public'
ORDER BY tablename";
}
public function quoteIdentifier(string $identifier): string
{
// PostgreSQL uses double quotes for identifiers
return '"' . str_replace('"', '""', $identifier) . '"';
}
public function getCurrentTimestampSQL(): string
{
return 'CURRENT_TIMESTAMP';
}
public function getBinaryUuidSQL(): string
{
// PostgreSQL has native UUID generation via uuid-ossp extension
return 'uuid_generate_v4()';
}
private function buildColumnDefinition(ColumnDefinition $column): string
{
$parts = [];
// Column name and type
$parts[] = $this->quoteIdentifier($column->name);
// Handle auto-increment with SERIAL types
if ($column->autoIncrement) {
$serialType = match($column->type) {
ColumnType::SMALL_INTEGER => 'SMALLSERIAL',
ColumnType::INTEGER => 'SERIAL',
ColumnType::BIG_INTEGER => 'BIGSERIAL',
default => throw new \InvalidArgumentException('Auto-increment only supported for integer types')
};
$parts[] = $serialType;
} else {
$parts[] = $this->getColumnTypeSQL($column->type->value, [
'length' => $column->length,
'precision' => $column->precision,
'scale' => $column->scale,
]);
}
// Nullability
$parts[] = $column->nullable ? 'NULL' : 'NOT NULL';
// Default value
if ($column->hasDefault() && !$column->autoIncrement) {
if (is_string($column->default) && !in_array($column->default, ['CURRENT_TIMESTAMP'])) {
$parts[] = 'DEFAULT ' . $this->escapeString($column->default);
} else {
$parts[] = "DEFAULT {$column->default}";
}
}
// PostgreSQL doesn't support column comments in CREATE TABLE directly
// Use COMMENT ON COLUMN separately if needed
return implode(' ', $parts);
}
private function buildIndexDefinition(IndexDefinition $index): string
{
$parts = [];
// Constraint type
if ($index->type === IndexType::PRIMARY) {
$parts[] = 'PRIMARY KEY';
} elseif ($index->type === IndexType::UNIQUE) {
$parts[] = 'CONSTRAINT ' . $this->quoteIdentifier($index->name);
$parts[] = 'UNIQUE';
} else {
// Regular indexes are created separately in PostgreSQL
return '';
}
// Column list
$parts[] = '(' . $this->quoteColumnList($index->columns) . ')';
return implode(' ', $parts);
}
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']})";
}
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
{
// PostgreSQL BYTEA doesn't need length specification
return $baseType;
}
private function quoteColumnList(array $columns): string
{
return implode(', ', array_map([$this, 'quoteIdentifier'], $columns));
}
private function escapeString(string $value): string
{
// PostgreSQL uses single quotes and doubles them for escaping
return "'" . str_replace("'", "''", $value) . "'";
}
}

View File

@@ -7,15 +7,18 @@ namespace App\Framework\Database\Platform;
use App\Framework\Database\ConnectionInterface;
/**
* Factory for creating SchemaBuilder instances with appropriate platform
* Factory for creating SchemaBuilder instances with automatic platform detection
*
* Auto-detects database platform from PDO driver:
* - mysql/mariadb → MySQLPlatform
* - pgsql → PostgreSQLPlatform
* - sqlite → (future) SQLitePlatform
*/
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();
$platform = self::detectPlatform($connection);
return new SchemaBuilder($connection, $platform);
}
@@ -24,4 +27,20 @@ final readonly class SchemaBuilderFactory
{
return new SchemaBuilder($connection, $platform);
}
/**
* Detect database platform from connection driver
*/
private static function detectPlatform(ConnectionInterface $connection): DatabasePlatform
{
$pdo = $connection->getPdo();
$driver = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
return match ($driver) {
'mysql' => new MySQLPlatform(),
'pgsql' => new PostgreSQLPlatform(),
// 'sqlite' => new SQLitePlatform(), // Future implementation
default => throw new \RuntimeException("Unsupported database driver: {$driver}")
};
}
}