diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/SetupController.php b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/SetupController.php index 3ee9bb6f98..bc05e2cce9 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/SetupController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/SetupController.php @@ -33,6 +33,18 @@ final class SetupController extends AbstractController { + /** + * Run pre-upgrade checks for the installed phpMyFAQ version provided in the request body. + * + * Validates the request content as the installed version, ensures maintenance mode is enabled, + * verifies the installed version meets the minimum upgradable version, and checks hard requirements. + * + * @param Request $request The HTTP request whose body must contain the installed phpMyFAQ version string. + * @return JsonResponse A JSON response containing a `message` and an HTTP status code: + * - 200: Installation check successful. + * - 400: No version given or pre-upgrade requirement failure (exception message). + * - 409: Maintenance mode not enabled or installed version below minimum required (advice message). + */ #[Route(path: 'setup/check', name: 'api.private.setup.check', methods: ['POST'])] public function check(Request $request): JsonResponse { @@ -71,6 +83,17 @@ public function check(Request $request): JsonResponse return $this->json(['message' => 'Installation check successful'], Response::HTTP_OK); } + /** + * Create a configuration backup for the provided installed version. + * + * Validates the request body for an installed version, determines the configuration + * directory based on that version, and attempts to create a backup file. + * Returns a JSON response with HTTP 200 and `backupFile` on success, + * HTTP 400 if no version is provided, or HTTP 502 if the backup fails. + * + * @param Request $request HTTP request whose body contains the installed version string. + * @return JsonResponse JSON containing a message and, on success, a `backupFile` path. + */ #[Route(path: 'setup/backup', name: 'api.private.setup.backup', methods: ['POST'])] public function backup(Request $request): JsonResponse { @@ -99,6 +122,14 @@ public function backup(Request $request): JsonResponse return $this->json(['message' => 'Backup successful', 'backupFile' => $pathToBackup], Response::HTTP_OK); } + /** + * Performs the database upgrade for a given installed phpMyFAQ version and updates maintenance mode. + * + * Attempts to apply database updates for the installed version provided in the request body. On success, disables maintenance mode and returns a success message; on failure returns an error message describing the problem. + * + * @param Request $request HTTP request whose body must contain the installed phpMyFAQ version. + * @return JsonResponse JSON with `success` on success, or `error` with an explanatory message on failure. + */ #[Route(path: 'setup/update-database', name: 'api.private.setup.update-database', methods: ['POST'])] public function updateDatabase(Request $request): JsonResponse { @@ -126,4 +157,4 @@ public function updateDatabase(Request $request): JsonResponse ], Response::HTTP_BAD_GATEWAY); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/AbstractMigration.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/AbstractMigration.php index eaef0c849d..e05712e915 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/AbstractMigration.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/AbstractMigration.php @@ -28,6 +28,14 @@ protected string $tablePrefix; protected string $dbType; + /** + * Initialize the migration with a Configuration instance and capture environment-specific database settings. + * + * The constructor stores the provided Configuration for later access and sets the internal table prefix and + * database type based on the current Database environment. + * + * @param Configuration $configuration The configuration service used to read setup settings. + */ public function __construct( protected Configuration $configuration, ) { @@ -36,9 +44,9 @@ public function __construct( } /** - * Default implementation returns an empty array (no dependencies). + * Get migration dependencies; override to declare required migration class names. * - * @return string[] + * @return string[] List of migration class names this migration depends on. Empty array if there are no dependencies. */ public function getDependencies(): array { @@ -46,7 +54,9 @@ public function getDependencies(): array } /** - * Default implementation - migrations are not reversible unless overridden. + * Indicates whether the migration is reversible. + * + * @return bool `true` if the migration is reversible, `false` otherwise. */ public function isReversible(): bool { @@ -62,7 +72,10 @@ public function down(OperationRecorder $recorder): void } /** - * Returns the table name with a prefix. + * Resolve a table name using the configured table prefix. + * + * @param string $name The base table name (without prefix). + * @return string The fully qualified table name including the configured prefix. */ protected function table(string $name): string { @@ -70,9 +83,10 @@ protected function table(string $name): string } /** - * Checks if the current database type matches any of the given types. + * Determines whether the current database type matches any of the provided type names. * - * @param string|string[] $types Database type(s) to check + * @param string|string[] $types Database type name or list of names to compare against (e.g. 'mysqli', 'pdo_mysql'). + * @return bool `true` if the current database type is one of the provided types, `false` otherwise. */ protected function isDbType(string|array $types): bool { @@ -81,15 +95,19 @@ protected function isDbType(string|array $types): bool } /** - * Returns true if running MySQL/MariaDB. - */ + * Determine whether the active database type is MySQL or MariaDB. + * + * @return bool `true` if the current database type is MySQL/MariaDB, `false` otherwise. + */ protected function isMySql(): bool { return $this->isDbType(['mysqli', 'pdo_mysql']); } /** - * Returns true if running PostgreSQL. + * Determine whether the active database is PostgreSQL. + * + * @return bool `true` if the current database type is PostgreSQL (`pgsql` or `pdo_pgsql`), `false` otherwise. */ protected function isPostgreSql(): bool { @@ -97,7 +115,9 @@ protected function isPostgreSql(): bool } /** - * Returns true if running SQLite. + * Determine whether the current database type is SQLite. + * + * @return bool `true` if the active DB type is `sqlite3` or `pdo_sqlite`, `false` otherwise. */ protected function isSqlite(): bool { @@ -105,17 +125,24 @@ protected function isSqlite(): bool } /** - * Returns true if running SQL Server. - */ + * Determine whether the current database type is Microsoft SQL Server. + * + * @return bool `true` if the active database type is SQL Server (`sqlsrv` or `pdo_sqlsrv`), `false` otherwise. + */ protected function isSqlServer(): bool { return $this->isDbType(['sqlsrv', 'pdo_sqlsrv']); } /** - * Helper to add a column to a table. - * Returns the appropriate SQL for the current database type. - */ + * Builds an ALTER TABLE statement to add a column adapted to the active database dialect. + * + * @param string $table Unprefixed table name; the configured table prefix will be applied. + * @param string $column Column name to add. + * @param string $type SQL type definition for the new column (e.g. `VARCHAR(255)`). + * @param string|null $default Optional SQL expression for the DEFAULT clause (include quotes/literals as required). + * @return string The ALTER TABLE SQL statement that adds the specified column for the current DB type. + */ protected function addColumn(string $table, string $column, string $type, ?string $default = null): string { $tableName = $this->table($table); @@ -131,7 +158,11 @@ protected function addColumn(string $table, string $column, string $type, ?strin } /** - * Helper to drop a column from a table. + * Builds an SQL statement to drop a column from a prefixed table. + * + * @param string $table Unprefixed table name. + * @param string $column Column name to drop. + * @return string The SQL statement that drops the specified column from the prefixed table. */ protected function dropColumn(string $table, string $column): string { @@ -140,10 +171,12 @@ protected function dropColumn(string $table, string $column): string } /** - * Helper to drop multiple columns from a table (MySQL syntax). - * - * @param string[] $columns - */ + * Build an ALTER TABLE statement that drops multiple columns using MySQL-style syntax. + * + * @param string $table Unprefixed table name (will be prefixed by the migration's table prefix). + * @param string[] $columns Names of columns to drop. + * @return string The generated ALTER TABLE ... DROP COLUMN ... SQL statement. + */ protected function dropColumns(string $table, array $columns): string { $tableName = $this->table($table); @@ -152,7 +185,12 @@ protected function dropColumns(string $table, array $columns): string } /** - * Helper to create an index. + * Builds a CREATE INDEX SQL statement appropriate for the active database. + * + * @param string $table The logical table name (will be prefixed using the migration's table prefix). + * @param string $indexName The name of the index to create. + * @param string|array $columns Column name or list of column names to include in the index; an array is joined with ", ". + * @return string The SQL statement that creates the index if it does not already exist for the current DB. */ protected function createIndex(string $table, string $indexName, string|array $columns): string { @@ -173,7 +211,10 @@ protected function createIndex(string $table, string $indexName, string|array $c } /** - * Helper to drop a table. + * Build a DROP TABLE statement for the given table name using the configured prefix. + * + * @param string $table Unprefixed table name. + * @return string SQL DROP TABLE statement targeting the prefixed table. */ protected function dropTable(string $table): string { @@ -181,15 +222,26 @@ protected function dropTable(string $table): string } /** - * Helper to drop a table if it exists. - */ + * Builds a DROP TABLE IF EXISTS SQL statement for the given table name. + * + * @param string $table The table name without prefix. + * @return string The SQL statement to drop the prefixed table if it exists. + */ protected function dropTableIfExists(string $table): string { return sprintf('DROP TABLE IF EXISTS %s', $this->table($table)); } /** - * Helper for UPDATE queries with a language code fix pattern. + * Builds an UPDATE SQL statement that replaces a language code value in a specified column. + * + * The table name is passed through the migration's table-prefix resolver before being used. + * + * @param string $table The (unprefixed) table name to update. + * @param string $column The column containing the language code. + * @param string $oldCode The language code value to replace. + * @param string $newCode The language code value to set. + * @return string The constructed UPDATE SQL statement. */ protected function updateLanguageCode(string $table, string $column, string $oldCode, string $newCode): string { @@ -204,7 +256,9 @@ protected function updateLanguageCode(string $table, string $column, string $old } /** - * Returns the INTEGER type appropriate for the database. + * Get the SQL integer column type for the current database. + * + * @return string The SQL type name: 'INT' for MySQL/MariaDB, 'INTEGER' for other databases. */ protected function integerType(): string { @@ -215,7 +269,9 @@ protected function integerType(): string } /** - * Returns the TEXT type appropriate for the database. + * Determine the SQL column type to use for text fields for the current database. + * + * @return string The SQL type name for text columns: 'NVARCHAR(MAX)' for SQL Server variants, otherwise 'TEXT'. */ protected function textType(): string { @@ -226,7 +282,10 @@ protected function textType(): string } /** - * Returns the VARCHAR type appropriate for the database. + * Selects the VARCHAR column type declaration appropriate for the active database. + * + * @param int $length The character length for the VARCHAR/NVARCHAR column. + * @return string The SQL type declaration, e.g. `NVARCHAR(255)` for SQL Server or `VARCHAR(255)` for other databases. */ protected function varcharType(int $length): string { @@ -237,8 +296,13 @@ protected function varcharType(int $length): string } /** - * Returns the TIMESTAMP/DATETIME type with default appropriate for the database. - */ + * Selects the appropriate TIMESTAMP/DATETIME column type for the active database. + * + * When $withDefault is true, the returned string includes a database-specific default/current-timestamp clause. + * + * @param bool $withDefault If true, append the database-specific DEFAULT/current-timestamp clause. + * @return string The SQL column type (optionally including NOT NULL and DEFAULT clause) suitable for the current DB driver. + */ protected function timestampType(bool $withDefault = true): string { $type = match ($this->dbType) { @@ -259,8 +323,10 @@ protected function timestampType(bool $withDefault = true): string } /** - * Returns the BOOLEAN/TINYINT type appropriate for the database. - */ + * Determine the SQL column type used to represent boolean values for the current database. + * + * @return string `TINYINT(1)` for MySQL, `TINYINT` for SQL Server, `INTEGER` for other databases. + */ protected function booleanType(): string { return match ($this->dbType) { @@ -271,7 +337,10 @@ protected function booleanType(): string } /** - * Returns the auto-increment column definition appropriate for the database. + * Produce an auto-increment column definition suited to the active database type. + * + * @param string $columnName Name of the column; defaults to "id". + * @return string Column definition fragment including type and auto-increment syntax. */ protected function autoIncrementColumn(string $columnName = 'id'): string { @@ -285,7 +354,10 @@ protected function autoIncrementColumn(string $columnName = 'id'): string } /** - * Gets a configuration value safely. + * Retrieve the configuration value for the specified key. + * + * @param string $key The configuration key to retrieve. + * @return mixed The value associated with the configuration key. */ protected function getConfig(string $key): mixed { @@ -293,8 +365,12 @@ protected function getConfig(string $key): mixed } /** - * Calculates a checksum for migration integrity. - */ + * Compute a checksum representing this migration's identity. + * + * The checksum is derived from the migration's version, description, and class name. + * + * @return string The SHA-256 hash of a JSON object containing the migration's version, description, and class name. + */ public function getChecksum(): string { $data = [ @@ -304,4 +380,4 @@ public function getChecksum(): string ]; return hash('sha256', json_encode($data)); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationExecutor.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationExecutor.php index 7f7f961a07..76995ab508 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationExecutor.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationExecutor.php @@ -30,6 +30,15 @@ class MigrationExecutor /** @var MigrationResult[] */ private array $results = []; + /** + * Create a MigrationExecutor with its required dependencies. + * + * The provided Configuration and MigrationTracker are used for migration execution + * and persistent tracking. The Filesystem is optional and used for file-related + * operations during migrations when present. + * + * @param ?Filesystem $filesystem Optional filesystem used for file and directory operations. + */ public function __construct( private readonly Configuration $configuration, private readonly MigrationTracker $tracker, @@ -47,7 +56,9 @@ public function setDryRun(bool $dryRun): self } /** - * Returns whether dry-run mode is enabled. + * Indicates whether migrations will be simulated without applying changes. + * + * @return bool `true` if dry-run mode is enabled, `false` otherwise. */ public function isDryRun(): bool { @@ -55,7 +66,15 @@ public function isDryRun(): bool } /** - * Executes a single migration. + * Execute a single migration and collect per-operation results. + * + * Runs the migration's up() to gather operations, then simulates them when in dry-run mode + * or executes them otherwise. On success (and when not in dry-run), the migration is recorded + * with the tracker. The produced MigrationResult is stored internally and returned. + * + * @param MigrationInterface $migration The migration instance to execute. + * @return MigrationResult The result containing per-operation outcomes, total execution time (ms), + * dry-run flag, and any error message if execution failed. */ public function executeMigration(MigrationInterface $migration): MigrationResult { @@ -107,10 +126,12 @@ public function executeMigration(MigrationInterface $migration): MigrationResult } /** - * Executes multiple migrations in order. + * Executes each migration in the given order, collecting and returning their results. + * + * Execution stops after the first failed migration unless dry-run mode is enabled. * - * @param MigrationInterface[] $migrations - * @return MigrationResult[] + * @param MigrationInterface[] $migrations Migrations to execute, in desired order. + * @return MigrationResult[] Array of results for the migrations that were processed. */ public function executeMigrations(array $migrations): array { @@ -130,10 +151,12 @@ public function executeMigrations(array $migrations): array } /** - * Collects operations from migrations without executing them. + * Collects operations produced by each migration's up() method without executing them. * - * @param MigrationInterface[] $migrations - * @return array>}> + * @param MigrationInterface[] $migrations Array of migrations keyed by version. + * @return array>}> Array keyed by migration version. Each entry contains: + * - `migration`: the MigrationInterface instance + * - `operations`: an indexed array of operation representations produced by the migration */ public function collectOperations(array $migrations): array { @@ -153,9 +176,9 @@ public function collectOperations(array $migrations): array } /** - * Returns all execution results. + * Get accumulated migration execution results. * - * @return MigrationResult[] + * @return MigrationResult[] The collected MigrationResult objects in execution order. */ public function getResults(): array { @@ -163,7 +186,9 @@ public function getResults(): array } /** - * Clears stored results. + * Clear all accumulated migration results. + * + * @return $this The current executor instance for method chaining. */ public function clearResults(): self { @@ -172,10 +197,21 @@ public function clearResults(): self } /** - * Generates a dry-run report for the given migrations. + * Generate a dry-run report summarizing operations produced by each migration. + * + * The report contains two top-level keys: + * - `migrations` (array): keyed by migration version; each entry includes: + * - `description` (string): migration description + * - `operationCount` (int): total operations for the migration + * - `operationsByType` (array): counts per operation type + * - `operations` (array): the recorded operations + * - `summary` (array): aggregated totals with keys: + * - `migrationCount` (int) + * - `totalOperations` (int) + * - `operationsByType` (array) aggregated across all migrations * - * @param MigrationInterface[] $migrations - * @return array + * @param MigrationInterface[] $migrations Migrations keyed by version to include in the report. + * @return array The aggregated dry-run report with `migrations` and `summary` keys. */ public function generateDryRunReport(array $migrations): array { @@ -215,9 +251,17 @@ public function generateDryRunReport(array $migrations): array } /** - * Formats the dry-run report as a human-readable string. + * Produce a human-readable text report for a dry-run migration report. * - * @param array $report + * The output contains a per-migration section (version, description and grouped + * operations: SQL, configuration changes, file operations, permission changes) + * followed by a summary with migration and operation counts and a breakdown by + * operation type. + * + * @param array $report Dry-run report produced by generateDryRunReport(), expected to contain: + * - 'migrations': array keyed by version with ['description' => string, 'operations' => array] + * - 'summary': array with 'migrationCount', 'totalOperations' and optional 'operationsByType' + * @return string Formatted multi-line human-readable report. */ public function formatDryRunReport(array $report): string { @@ -295,6 +339,17 @@ public function formatDryRunReport(array $report): string return $output; } + /** + * Normalize whitespace in a SQL query and shorten it to a maximum length. + * + * The query's consecutive whitespace characters are collapsed to single spaces and surrounding + * whitespace is trimmed. If the result exceeds `$maxLength`, it is truncated and an + * ellipsis (`...`) is appended so the returned string length does not exceed `$maxLength`. + * + * @param string $query The SQL query to normalize and truncate. + * @param int $maxLength The maximum allowed length of the returned string (including the ellipsis). Defaults to 100. + * @return string The normalized, possibly truncated query. + */ private function truncateQuery(string $query, int $maxLength = 100): string { $query = preg_replace('/\s+/', ' ', trim($query)); @@ -303,4 +358,4 @@ private function truncateQuery(string $query, int $maxLength = 100): string } return $query; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationInterface.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationInterface.php index e885b9d847..27b6f09019 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationInterface.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationInterface.php @@ -24,37 +24,51 @@ interface MigrationInterface { /** - * Returns the version string this migration upgrades to. - * Format: semantic version (e.g., "3.2.0-alpha", "4.0.0-alpha.2") - */ + * The target semantic version this migration upgrades to. + * + * The version must follow semantic versioning and may include pre-release or build metadata + * (for example: "3.2.0-alpha", "4.0.0-alpha.2"). + * + * @return string The target semantic version string. + */ public function getVersion(): string; /** - * Returns an array of version strings that must be applied before this migration. - * - * @return string[] - */ + * List version strings this migration depends on. + * + * @return string[] An array of version strings that must be applied before this migration. + */ public function getDependencies(): array; /** - * Returns a human-readable description of what this migration does. - */ + * Provide a human-readable description of the migration's purpose or effects. + * + * @return string A short, human-readable description of what the migration does. + */ public function getDescription(): string; /** - * Applies the migration, recording all operations to the recorder. - * Operations are NOT executed immediately - they are collected for review or execution. - */ + * Record the operations required to apply this migration into the provided recorder. + * + * The recorder collects operations; they are not executed by this method and may be reviewed or applied later. + * + * @param OperationRecorder $recorder The recorder that will collect migration operations. + */ public function up(OperationRecorder $recorder): void; /** - * Reverses the migration, recording all rollback operations to the recorder. - * Operations are NOT executed immediately - they are collected for review or execution. - */ + * Perform the reverse migration by recording rollback operations to the provided recorder. + * + * The recorder collects operations for review or later execution; operations are not executed immediately. + * + * @param OperationRecorder $recorder Recorder that will collect the rollback operations. + */ public function down(OperationRecorder $recorder): void; /** - * Returns true if this migration can be safely reversed. - */ + * Indicates whether the migration can be safely reversed. + * + * @return bool `true` if the migration can be reversed, `false` otherwise. + */ public function isReversible(): bool; -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationRegistry.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationRegistry.php index 81b9f7737b..167b298b03 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationRegistry.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationRegistry.php @@ -29,6 +29,14 @@ class MigrationRegistry /** @var array|null */ private ?array $migrations = null; + /** + * Create a MigrationRegistry and register the default migration mappings. + * + * Stores the provided Configuration for use when instantiating migration classes + * and populates the registry with the built-in migrations. + * + * @param Configuration $configuration Configuration used to instantiate migration classes. + */ public function __construct( private readonly Configuration $configuration, ) { @@ -36,10 +44,14 @@ public function __construct( } /** - * Registers a migration class. - * - * @param class-string $className - */ + * Register a migration class for the given version. + * + * Also clears the cached instantiated migrations so the new registration is picked up. + * + * @param string $version The version identifier for the migration. + * @param class-string $className Fully-qualified migration class name. + * @return $this The current MigrationRegistry instance for fluent chaining. + */ public function register(string $version, string $className): self { $this->migrationClasses[$version] = $className; @@ -73,9 +85,12 @@ private function registerDefaultMigrations(): void } /** - * Returns all registered migrations, sorted by version. + * Get all registered migrations ordered by version. + * + * Builds and caches MigrationInterface instances (constructed with the registry's Configuration) + * for each registered migration class that exists, then returns them keyed by version. * - * @return array + * @return array Associative array mapping version strings to MigrationInterface instances, ordered by version. */ public function getMigrations(): array { @@ -98,7 +113,10 @@ public function getMigrations(): array } /** - * Returns a specific migration by version. + * Retrieve the migration instance for a given version. + * + * @param string $version The migration version identifier. + * @return MigrationInterface|null The migration instance for the version, or `null` if no migration is registered for it. */ public function getMigration(string $version): ?MigrationInterface { @@ -107,9 +125,9 @@ public function getMigration(string $version): ?MigrationInterface } /** - * Returns all versions in order. + * List registered migration version strings in ascending version order. * - * @return string[] + * @return string[] Array of migration version strings ordered from lowest to highest. */ public function getVersions(): array { @@ -117,10 +135,11 @@ public function getVersions(): array } /** - * Returns migrations that need to be applied to get from $currentVersion to the latest. - * - * @return MigrationInterface[] - */ + * Get migrations with versions newer than the provided current version. + * + * @param string $currentVersion The current installed version to compare from. + * @return MigrationInterface[] Migrations whose version is greater than `$currentVersion`, keyed by version string. + */ public function getPendingMigrations(string $currentVersion): array { $pending = []; @@ -135,11 +154,11 @@ public function getPendingMigrations(string $currentVersion): array } /** - * Returns migrations that need to be applied based on what's already tracked. - * - * @param string[] $appliedVersions - * @return MigrationInterface[] - */ + * Determine migrations that are not present in the provided applied versions. + * + * @param string[] $appliedVersions List of migration version strings that have already been applied. + * @return MigrationInterface[] Migration instances keyed by version for migrations not listed in `$appliedVersions`. + */ public function getUnappliedMigrations(array $appliedVersions): array { $unapplied = []; @@ -154,7 +173,9 @@ public function getUnappliedMigrations(array $appliedVersions): array } /** - * Returns the latest migration version. + * Get the most recent registered migration version. + * + * @return string|null The latest migration version string, or `null` if no migrations are registered. */ public function getLatestVersion(): ?string { @@ -163,10 +184,13 @@ public function getLatestVersion(): ?string } /** - * Checks if a migration version exists. + * Determine whether a migration is registered for the given version. + * + * @param string $version The migration version string to check. + * @return bool `true` if a migration for the version is registered, `false` otherwise. */ public function hasMigration(string $version): bool { return isset($this->migrationClasses[$version]); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationResult.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationResult.php index 3bf06f6319..ec2f809e53 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationResult.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationResult.php @@ -31,60 +31,120 @@ class MigrationResult private float $executionTimeMs = 0; private bool $dryRun = false; + /** + * Create a MigrationResult for a specific migration version and description. + * + * @param string $version Migration identifier (typically a version string or timestamp). + * @param string $description Human-readable description of the migration. + */ public function __construct( private readonly string $version, private readonly string $description, ) { } + /** + * Get the migration's version identifier. + * + * @return string The migration version. + */ public function getVersion(): string { return $this->version; } + /** + * Gets the migration description. + * + * @return string The migration description. + */ public function getDescription(): string { return $this->description; } + /** + * Indicates whether the migration completed successfully. + * + * @return bool `true` if the migration and all recorded operations succeeded, `false` otherwise. + */ public function isSuccess(): bool { return $this->success; } + /** + * Set the overall success flag for this migration result. + * + * @param bool $success `true` if the migration is successful, `false` otherwise. + * @return self The current instance for method chaining. + */ public function setSuccess(bool $success): self { $this->success = $success; return $this; } + /** + * Get the aggregate error message for the migration, if one was recorded. + * + * @return string|null The aggregate error message if present, `null` otherwise. + */ public function getErrorMessage(): ?string { return $this->errorMessage; } + /** + * Set the aggregate error message for this migration result. + * + * @param string|null $errorMessage The aggregate error message, or `null` to clear it. + * @return self Fluent instance for method chaining. + */ public function setErrorMessage(?string $errorMessage): self { $this->errorMessage = $errorMessage; return $this; } + /** + * Get the total execution time of the migration in milliseconds. + * + * @return float The execution time in milliseconds. + */ public function getExecutionTimeMs(): float { return $this->executionTimeMs; } + /** + * Set the total execution time of the migration in milliseconds. + * + * @param float $executionTimeMs Total execution time in milliseconds. + * @return self The current instance for method chaining. + */ public function setExecutionTimeMs(float $executionTimeMs): self { $this->executionTimeMs = $executionTimeMs; return $this; } + /** + * Indicates whether the migration was executed as a dry run. + * + * @return bool `true` if this result represents a dry run, `false` otherwise. + */ public function isDryRun(): bool { return $this->dryRun; } + /** + * Set whether this migration result represents a dry run. + * + * @param bool $dryRun True if the migration was a dry run, false otherwise. + * @return self The current MigrationResult instance. + */ public function setDryRun(bool $dryRun): self { $this->dryRun = $dryRun; @@ -92,7 +152,15 @@ public function setDryRun(bool $dryRun): self } /** - * Adds an operation result. + * Record the result of a single migration operation. + * + * If the operation failed ($success is false), the overall migration success flag + * is set to false. + * + * @param OperationInterface $operation The operation that was executed. + * @param bool $success `true` if the operation succeeded, `false` otherwise. + * @param string|null $error Optional error message for a failed operation. + * @return self The current MigrationResult instance (fluent interface). */ public function addOperationResult(OperationInterface $operation, bool $success, ?string $error = null): self { @@ -108,17 +176,22 @@ public function addOperationResult(OperationInterface $operation, bool $success, } /** - * Returns all operation results. - * - * @return array - */ + * Retrieve recorded per-operation results for this migration. + * + * @return array An indexed array where each entry contains: + * - `operation`: the OperationInterface instance + * - `success`: `true` if the operation succeeded, `false` otherwise + * - `error`: an error message or `null` if none + */ public function getOperationResults(): array { return $this->operationResults; } /** - * Returns the count of successful operations. + * Count successful operations recorded for this migration. + * + * @return int The number of operation results with success = true. */ public function getSuccessCount(): int { @@ -126,15 +199,19 @@ public function getSuccessCount(): int } /** - * Returns the count of failed operations. - */ + * Counts failed operation results. + * + * @return int The number of recorded operations whose `success` flag is false. + */ public function getFailureCount(): int { return count(array_filter($this->operationResults, static fn($r) => !$r['success'])); } /** - * Returns the total operation count. + * Total number of operations recorded for this migration. + * + * @return int The number of recorded operation results. */ public function getOperationCount(): int { @@ -142,9 +219,29 @@ public function getOperationCount(): int } /** - * Converts the result to an array for serialization. + * Produce a serializable associative array representing this migration result. + * + * The array contains top-level metadata (version, description), execution state + * (success, dryRun, errorMessage, executionTimeMs), aggregate counts + * (operationCount, successCount, failureCount), and an `operations` list with + * per-operation entries. * - * @return array + * @return array { + * @type string $version Migration version. + * @type string $description Migration description. + * @type bool $success Overall success flag. + * @type bool $dryRun Whether the migration was a dry run. + * @type string|null $errorMessage Aggregate error message or null. + * @type float $executionTimeMs Total execution time in milliseconds. + * @type int $operationCount Total number of operations recorded. + * @type int $successCount Number of successful operations. + * @type int $failureCount Number of failed operations. + * @type array[] $operations List of per-operation arrays with keys: + * - `type` (string) + * - `description` (string) + * - `success` (bool) + * - `error` (string|null) + * } */ public function toArray(): array { @@ -166,4 +263,4 @@ public function toArray(): array ], $this->operationResults), ]; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationTracker.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationTracker.php index f685e0546f..ef6913da60 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationTracker.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/MigrationTracker.php @@ -26,13 +26,24 @@ class MigrationTracker { private const TABLE_NAME = 'faqmigrations'; + /** + * Create a MigrationTracker bound to the provided configuration. + * + * @param Configuration $configuration Configuration instance used to access the database connection, table prefix, and related settings. + */ public function __construct( private readonly Configuration $configuration, ) { } /** - * Creates the migrations tracking table if it doesn't exist. + * Ensure the migrations tracking table exists for the configured database. + * + * Creates the migrations tracking table using SQL appropriate for the current database type + * (MySQL, PostgreSQL, SQLite, or SQL Server) and the configured table prefix, and executes + * the CREATE statement on the configured database connection. + * + * @throws \RuntimeException If the current database type is not supported. */ public function ensureTableExists(): void { @@ -85,8 +96,11 @@ public function ensureTableExists(): void } /** - * Checks if a migration has been applied. - */ + * Determine whether a migration version is recorded as applied. + * + * @param string $version The migration version identifier to check. + * @return bool `true` if the version is recorded as applied, `false` otherwise. + */ public function isApplied(string $version): bool { $tableName = Database::getTablePrefix() . self::TABLE_NAME; @@ -103,7 +117,15 @@ public function isApplied(string $version): bool } /** - * Records a migration as applied. + * Insert a record marking a migration version as applied. + * + * Stores the migration version with its execution time (in milliseconds) and optional checksum and description + * in the migrations tracking table. + * + * @param string $version The migration version identifier. + * @param int $executionTimeMs Execution time of the migration in milliseconds. + * @param string|null $checksum Optional checksum for the migration, or null if not provided. + * @param string|null $description Optional human-readable description for the migration, or null if not provided. */ public function recordMigration( string $version, @@ -127,7 +149,9 @@ public function recordMigration( } /** - * Removes a migration record (for rollback). + * Removes the recorded migration with the given version from the migrations table. + * + * @param string $version The migration version identifier to remove. */ public function removeMigration(string $version): void { @@ -142,10 +166,15 @@ public function removeMigration(string $version): void } /** - * Returns all applied migrations. - * - * @return array - */ + * Retrieve all applied migrations with their metadata. + * + * @return array Array of associative arrays containing: + * - `version`: migration version string + * - `applied_at`: timestamp when the migration was applied + * - `execution_time_ms`: execution time in milliseconds + * - `checksum`: optional checksum of the migration, or null + * - `description`: optional migration description, or null + */ public function getAppliedMigrations(): array { $tableName = Database::getTablePrefix() . self::TABLE_NAME; @@ -171,9 +200,9 @@ public function getAppliedMigrations(): array } /** - * Returns the list of applied versions. + * Get the versions of all applied migrations. * - * @return string[] + * @return string[] List of applied migration version strings. */ public function getAppliedVersions(): array { @@ -181,7 +210,9 @@ public function getAppliedVersions(): array } /** - * Returns the last applied migration version. + * Get the most recently applied migration version. + * + * @return string|null The version of the last applied migration, or `null` if no migrations have been recorded. */ public function getLastAppliedVersion(): ?string { @@ -195,8 +226,11 @@ public function getLastAppliedVersion(): ?string } /** - * Checks if the tracking table exists. - */ + * Determines whether the migrations tracking table exists for the current database. + * + * @return bool `true` if the tracking table exists, `false` otherwise. + * @throws \RuntimeException If the current database type is not supported. + */ public function tableExists(): bool { $tableName = Database::getTablePrefix() . self::TABLE_NAME; @@ -213,4 +247,4 @@ public function tableExists(): bool $result = $this->configuration->getDb()->query($query); return $this->configuration->getDb()->numRows($result) > 0; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigAddOperation.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigAddOperation.php index 9d1286cdb9..65e3d75f78 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigAddOperation.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigAddOperation.php @@ -23,6 +23,12 @@ readonly class ConfigAddOperation implements OperationInterface { + /** + * Create a new ConfigAddOperation for adding a configuration entry. + * + * @param string $key The configuration key to add. + * @param mixed $value The value to assign to the configuration key. + */ public function __construct( private Configuration $configuration, private string $key, @@ -30,33 +36,69 @@ public function __construct( ) { } + /** + * Get the operation type identifier. + * + * @return string The operation type identifier 'config_add'. + */ public function getType(): string { return 'config_add'; } + /** + * Build a human-readable description of the configuration addition. + * + * @return string The formatted description in the form "Add configuration: {key} = {formatted_value}". + */ public function getDescription(): string { $displayValue = $this->formatValue($this->value); return sprintf('Add configuration: %s = %s', $this->key, $displayValue); } + /** + * Get the configuration key targeted by this operation. + * + * @return string The configuration key being added. + */ public function getKey(): string { return $this->key; } + /** + * Retrieve the value associated with the configuration key. + * + * @return mixed The configuration value stored for the key. + */ public function getValue(): mixed { return $this->value; } + /** + * Apply the configuration-add operation to the injected Configuration. + * + * Adds the configured key and value to the configuration store. + * + * @return bool `true` if the configuration was added; currently always `true`. + */ public function execute(): bool { $this->configuration->add($this->key, $this->value); return true; } + /** + * Get a serializable associative array representing this operation. + * + * @return array{type:string,description:string,key:string,value:mixed} Associative array with keys: + * - `type`: operation type identifier, + * - `description`: human-readable description, + * - `key`: configuration key to add, + * - `value`: configuration value to set. + */ public function toArray(): array { return [ @@ -67,6 +109,14 @@ public function toArray(): array ]; } + /** + * Format a value into a human-readable string for operation descriptions. + * + * @param mixed $value The value to format. + * @return string A string representation suitable for descriptions: + * `true`/`false` for booleans, single-quoted strings (truncated to 47 characters plus "..." if longer than 50), + * `null` for null, or the result of casting to string for other types. + */ private function formatValue(mixed $value): string { if (is_bool($value)) { @@ -83,4 +133,4 @@ private function formatValue(mixed $value): string } return (string) $value; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigDeleteOperation.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigDeleteOperation.php index aae6ab4556..1a2d6bd900 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigDeleteOperation.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigDeleteOperation.php @@ -23,33 +23,67 @@ readonly class ConfigDeleteOperation implements OperationInterface { + /** + * Create a ConfigDeleteOperation that will delete the given configuration key. + * + * @param Configuration $configuration The Configuration instance used to perform the deletion. + * @param string $key The configuration key to delete. + */ public function __construct( private Configuration $configuration, private string $key, ) { } + /** + * Operation type identifier for this operation. + * + * @return string The operation type identifier: 'config_delete'. + */ public function getType(): string { return 'config_delete'; } + /** + * Provide a human-readable description of the configuration deletion operation. + * + * @return string A string describing the configuration key to delete. + */ public function getDescription(): string { return sprintf('Delete configuration: %s', $this->key); } + /** + * Retrieve the configuration key targeted by this operation. + * + * @return string The configuration key targeted for deletion. + */ public function getKey(): string { return $this->key; } + /** + * Delete the configuration key represented by this operation. + * + * @return bool true after the delete call is invoked. + */ public function execute(): bool { $this->configuration->delete($this->key); return true; } + /** + * Serialize the operation into an associative array for storage or transport. + * + * The returned array contains the operation `type`, a human-readable `description`, + * and the configuration `key` targeted by the operation. + * + * @return array{type:string,description:string,key:string} The serialized representation of the operation. + */ public function toArray(): array { return [ @@ -58,4 +92,4 @@ public function toArray(): array 'key' => $this->key, ]; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigRenameOperation.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigRenameOperation.php index d801ef879a..e59e11d0ea 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigRenameOperation.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigRenameOperation.php @@ -23,6 +23,13 @@ readonly class ConfigRenameOperation implements OperationInterface { + /** + * Create a config rename operation that will rename a configuration entry from one key to another. + * + * @param Configuration $configuration The configuration service used to perform the rename. + * @param string $oldKey The existing configuration key to rename. + * @param string $newKey The target configuration key name. + */ public function __construct( private Configuration $configuration, private string $oldKey, @@ -30,32 +37,67 @@ public function __construct( ) { } + /** + * Get the operation type identifier. + * + * @return string The operation type identifier 'config_rename'. + */ public function getType(): string { return 'config_rename'; } + /** + * Provides a human-readable description of the rename operation. + * + * @return string A description in the format "Rename configuration: {oldKey} -> {newKey}". + */ public function getDescription(): string { return sprintf('Rename configuration: %s -> %s', $this->oldKey, $this->newKey); } + /** + * Original configuration key to be renamed. + * + * @return string The stored old configuration key name. + */ public function getOldKey(): string { return $this->oldKey; } + /** + * Returns the target configuration key name for the rename operation. + * + * @return string The new configuration key. + */ public function getNewKey(): string { return $this->newKey; } + /** + * Execute the operation by renaming the stored configuration key from the old key to the new key. + * + * @return bool `true` if the operation completed (this method always returns `true`). + */ public function execute(): bool { $this->configuration->rename($this->oldKey, $this->newKey); return true; } + /** + * Serialize the operation to an associative array for migration reporting. + * + * @return array{ + * type: string, + * description: string, + * oldKey: string, + * newKey: string + * } Array containing operation metadata and the source and destination configuration keys. + */ public function toArray(): array { return [ @@ -65,4 +107,4 @@ public function toArray(): array 'newKey' => $this->newKey, ]; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigUpdateOperation.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigUpdateOperation.php index 0eaf17e369..5d17b1fdf2 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigUpdateOperation.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/ConfigUpdateOperation.php @@ -23,6 +23,13 @@ readonly class ConfigUpdateOperation implements OperationInterface { + /** + * Create a new ConfigUpdateOperation. + * + * @param Configuration $configuration The configuration object to update. + * @param string $key The configuration key to set. + * @param mixed $value The new value for the configuration key. + */ public function __construct( private Configuration $configuration, private string $key, @@ -30,33 +37,69 @@ public function __construct( ) { } + /** + * Identify the operation as a configuration update. + * + * @return string The operation type identifier 'config_update'. + */ public function getType(): string { return 'config_update'; } + /** + * Produce a human-readable description of the configuration update. + * + * @return string A description in the form "Update configuration: {key} = {displayValue}" where `{displayValue}` is a formatted representation of the value. + */ public function getDescription(): string { $displayValue = $this->formatValue($this->value); return sprintf('Update configuration: %s = %s', $this->key, $displayValue); } + /** + * Get the configuration key targeted by this operation. + * + * @return string The configuration key. + */ public function getKey(): string { return $this->key; } + /** + * Retrieve the configured value for this operation. + * + * @return mixed The value associated with the configuration key. + */ public function getValue(): mixed { return $this->value; } + /** + * Apply the stored configuration change to the injected Configuration instance. + * + * @return bool `true` indicating the configuration update was invoked. + */ public function execute(): bool { $this->configuration->update([$this->key => $this->value]); return true; } + /** + * Build a serializable representation of this operation. + * + * The returned array contains: + * - 'type': operation type string + * - 'description': human-readable description + * - 'key': configuration key being updated + * - 'value': configured value + * + * @return array Associative array with keys 'type', 'description', 'key', and 'value'. + */ public function toArray(): array { return [ @@ -67,6 +110,16 @@ public function toArray(): array ]; } + /** + * Format a value for human-readable display in migration descriptions. + * + * Booleans become `true` or `false`. Strings are wrapped in single quotes and + * truncated to 47 characters plus "..." if longer than 50 characters. + * `null` becomes `null`. Other values are cast to string. + * + * @param mixed $value The value to format. + * @return string The formatted string representation. + */ private function formatValue(mixed $value): string { if (is_bool($value)) { @@ -83,4 +136,4 @@ private function formatValue(mixed $value): string } return (string) $value; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/DirectoryCopyOperation.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/DirectoryCopyOperation.php index dce257fcf6..510d121ac5 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/DirectoryCopyOperation.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/DirectoryCopyOperation.php @@ -23,6 +23,14 @@ readonly class DirectoryCopyOperation implements OperationInterface { + /** + * Create a DirectoryCopyOperation configured to copy a directory from the given source to the given destination. + * + * @param Filesystem $filesystem The filesystem used to perform the recursive copy. + * @param string $source The source directory path to copy. + * @param string $destination The destination directory path. + * @param bool $onlyIfExists Whether to skip execution when the source directory does not exist (default true). + */ public function __construct( private Filesystem $filesystem, private string $source, @@ -31,11 +39,21 @@ public function __construct( ) { } + /** + * Return the operation's type identifier. + * + * @return string The operation type identifier 'directory_copy'. + */ public function getType(): string { return 'directory_copy'; } + /** + * Build a human-friendly description of this directory copy operation. + * + * @return string A description in the form "Copy directory: {source} -> {destination}" where source and destination paths are shortened for display. + */ public function getDescription(): string { $sourceShort = $this->shortenPath($this->source); @@ -43,16 +61,33 @@ public function getDescription(): string return sprintf('Copy directory: %s -> %s', $sourceShort, $destShort); } + /** + * Get the configured source directory path for the operation. + * + * @return string The source directory path. + */ public function getSource(): string { return $this->source; } + /** + * Get the configured destination path for the directory copy. + * + * @return string The destination filesystem path. + */ public function getDestination(): string { return $this->destination; } + /** + * Executes the directory copy operation. + * + * If configured to run only when the source exists, the method returns `true` without performing any action when the source is not a directory. Otherwise it attempts a recursive copy from source to destination. + * + * @return bool `true` on success or when skipped due to a missing source while onlyIfExists is true, `false` on failure. + */ public function execute(): bool { if ($this->onlyIfExists && !is_dir($this->source)) { @@ -67,6 +102,18 @@ public function execute(): bool } } + /** + * Serialize the operation into an associative array representation. + * + * The array contains keys describing the operation and its configuration: + * - `type`: operation type identifier + * - `description`: human-friendly description + * - `source`: source directory path + * - `destination`: destination directory path + * - `onlyIfExists`: whether the operation should be skipped when the source does not exist + * + * @return array{type:string,description:string,source:string,destination:string,onlyIfExists:bool} + */ public function toArray(): array { return [ @@ -78,6 +125,12 @@ public function toArray(): array ]; } + /** + * Shortens a filesystem path for display by removing the PMF root directory prefix if defined. + * + * @param string $path The original filesystem path. + * @return string The possibly shortened path with the PMF root prefix removed when applicable. + */ private function shortenPath(string $path): string { // Remove common prefixes to shorten the path for display @@ -86,4 +139,4 @@ private function shortenPath(string $path): string } return $path; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/FileCopyOperation.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/FileCopyOperation.php index 817944860f..5157a1e90f 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/FileCopyOperation.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/FileCopyOperation.php @@ -23,6 +23,13 @@ readonly class FileCopyOperation implements OperationInterface { + /** + * Create a file copy operation with a source, destination, and optional existence check. + * + * @param string $source Path of the source file to copy. + * @param string $destination Path where the file should be copied to. + * @param bool $onlyIfExists If true, skip the copy when the source does not exist (default true). + */ public function __construct( private Filesystem $filesystem, private string $source, @@ -31,11 +38,21 @@ public function __construct( ) { } + /** + * Get the operation type identifier. + * + * @return string The operation type identifier 'file_copy'. + */ public function getType(): string { return 'file_copy'; } + /** + * Provide a human-readable description of the file copy operation. + * + * @return string A description in the form "Copy file: -> " where each path is shortened relative to PMF_ROOT_DIR when applicable. + */ public function getDescription(): string { $sourceShort = $this->shortenPath($this->source); @@ -43,16 +60,33 @@ public function getDescription(): string return sprintf('Copy file: %s -> %s', $sourceShort, $destShort); } + /** + * Get the source file path for the copy operation. + * + * @return string The source file path. + */ public function getSource(): string { return $this->source; } + /** + * Get the destination path for the file copy operation. + * + * @return string The destination path where the file should be copied. + */ public function getDestination(): string { return $this->destination; } + /** + * Executes the file copy operation from the configured source to destination. + * + * If `onlyIfExists` is true and the source file does not exist, the operation is skipped. + * + * @return bool `true` if the file was copied or the operation was skipped due to a missing source, `false` if the copy failed. + */ public function execute(): bool { if ($this->onlyIfExists && !file_exists($this->source)) { @@ -67,6 +101,16 @@ public function execute(): bool } } + /** + * Convert the operation into an associative array of its public properties and metadata. + * + * @return array{type:string,description:string,source:string,destination:string,onlyIfExists:bool} Associative array containing: + * - type: operation type identifier + * - description: human-readable description + * - source: source file path + * - destination: destination file path + * - onlyIfExists: whether the operation should be skipped when the source is missing + */ public function toArray(): array { return [ @@ -78,6 +122,12 @@ public function toArray(): array ]; } + /** + * Shortens a filesystem path for display by removing the PMF_ROOT_DIR prefix when defined. + * + * @param string $path The original filesystem path. + * @return string The path with PMF_ROOT_DIR removed if it was defined and present, otherwise the original path. + */ private function shortenPath(string $path): string { // Remove common prefixes to shorten the path for display @@ -86,4 +136,4 @@ private function shortenPath(string $path): string } return $path; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/OperationInterface.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/OperationInterface.php index e991795c82..30955c8223 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/OperationInterface.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/OperationInterface.php @@ -22,13 +22,17 @@ interface OperationInterface { /** - * Returns the type of operation (e.g., 'sql', 'config_add', 'file_copy'). - */ + * Get the operation type. + * + * @return string The operation type (e.g., 'sql', 'config_add', 'file_copy'). + */ public function getType(): string; /** - * Returns a human-readable description of the operation. - */ + * Get a human-readable description of the operation. + * + * @return string A concise human-readable description of the operation. + */ public function getDescription(): string; /** @@ -39,9 +43,9 @@ public function getDescription(): string; public function execute(): bool; /** - * Returns an array representation of the operation for dry-run output. - * - * @return array - */ + * Provide an associative array representation of the operation suitable for dry-run output. + * + * @return array Associative array describing the operation (string keys to mixed values). + */ public function toArray(): array; -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/OperationRecorder.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/OperationRecorder.php index 9b9d668580..35b9be5606 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/OperationRecorder.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/OperationRecorder.php @@ -28,6 +28,12 @@ class OperationRecorder /** @var OperationInterface[] */ private array $operations = []; + /** + * Create a new OperationRecorder with the given configuration and optional filesystem. + * + * @param Configuration $configuration Application configuration used by recorded operations. + * @param Filesystem|null $filesystem Optional filesystem used for file and directory operations; if null, a default filesystem rooted at PMF_ROOT_DIR will be used. + */ public function __construct( private readonly Configuration $configuration, private readonly ?Filesystem $filesystem = null, @@ -35,7 +41,11 @@ public function __construct( } /** - * Records a SQL query operation. + * Record a SQL query operation for later review or execution. + * + * @param string $query The SQL statement to record. + * @param string $description Optional human-readable description of the query. + * @return self The recorder instance for method chaining. */ public function addSql(string $query, string $description = ''): self { @@ -44,9 +54,12 @@ public function addSql(string $query, string $description = ''): self } /** - * Records a SQL query operation with database type condition. + * Records a SQL operation only when the current database type matches one of the provided types. * - * @param string|string[] $dbTypes One or more database types (mysqli, pgsql, sqlite3, sqlsrv) + * @param string $query The SQL query to record. + * @param string|string[] $dbTypes One or more database types to match (e.g., 'mysqli', 'pgsql', 'sqlite3', 'sqlsrv'). + * @param string $description Optional description for the recorded operation. + * @return self The recorder instance for chaining. */ public function addSqlForDbType(string $query, string|array $dbTypes, string $description = ''): self { @@ -58,7 +71,11 @@ public function addSqlForDbType(string $query, string|array $dbTypes, string $de } /** - * Records a configuration add operation. + * Record a configuration addition operation for the given key and value. + * + * @param string $key The configuration key to add. + * @param mixed $value The value to set for the configuration key. + * @return self The current OperationRecorder instance for chaining. */ public function addConfig(string $key, mixed $value): self { @@ -67,7 +84,10 @@ public function addConfig(string $key, mixed $value): self } /** - * Records a configuration delete operation. + * Record a configuration delete operation. + * + * @param string $key The configuration key to delete. + * @return self The recorder instance for method chaining. */ public function deleteConfig(string $key): self { @@ -76,7 +96,11 @@ public function deleteConfig(string $key): self } /** - * Records a configuration rename operation. + * Record a configuration key rename operation. + * + * @param string $oldKey The existing configuration key to rename. + * @param string $newKey The new configuration key name. + * @return self The recorder instance for method chaining. */ public function renameConfig(string $oldKey, string $newKey): self { @@ -85,7 +109,13 @@ public function renameConfig(string $oldKey, string $newKey): self } /** - * Records a configuration update operation. + * Record an operation that updates a configuration entry. + * + * Appends an operation to update the configuration identified by `$key` to the provided `$value`. + * + * @param string $key The configuration key to update. + * @param mixed $value The new value for the configuration key. + * @return self The current OperationRecorder instance for method chaining. */ public function updateConfig(string $key, mixed $value): self { @@ -94,8 +124,13 @@ public function updateConfig(string $key, mixed $value): self } /** - * Records a file copy operation. - */ + * Record a file copy operation for later execution or review. + * + * @param string $source The source file path. + * @param string $destination The destination file path. + * @param bool $onlyIfExists If `true`, the operation should be skipped when the source file does not exist. + * @return self The current OperationRecorder instance for method chaining. + */ public function copyFile(string $source, string $destination, bool $onlyIfExists = true): self { $filesystem = $this->filesystem ?? new Filesystem(PMF_ROOT_DIR); @@ -104,8 +139,13 @@ public function copyFile(string $source, string $destination, bool $onlyIfExists } /** - * Records a directory copy operation. - */ + * Record a directory copy operation for later execution or review. + * + * @param string $source Path to the source directory. + * @param string $destination Path to the destination directory. + * @param bool $onlyIfExists If `true`, the operation should be applied only when the source directory exists. + * @return self The current OperationRecorder instance for chaining. + */ public function copyDirectory(string $source, string $destination, bool $onlyIfExists = true): self { $filesystem = $this->filesystem ?? new Filesystem(PMF_ROOT_DIR); @@ -114,7 +154,12 @@ public function copyDirectory(string $source, string $destination, bool $onlyIfE } /** - * Records a permission grant operation. + * Record a permission grant operation to be executed or reviewed later. + * + * @param string $name The internal name of the permission. + * @param string $description A human-readable description of the permission. + * @param int $userId The ID of the user who will receive the permission (defaults to 1). + * @return self The current OperationRecorder instance for method chaining. */ public function grantPermission(string $name, string $description, int $userId = 1): self { @@ -123,7 +168,10 @@ public function grantPermission(string $name, string $description, int $userId = } /** - * Records a custom operation. + * Record a custom migration operation. + * + * @param OperationInterface $operation The operation to record. + * @return self The current OperationRecorder instance. */ public function addOperation(OperationInterface $operation): self { @@ -132,9 +180,9 @@ public function addOperation(OperationInterface $operation): self } /** - * Returns all recorded operations. + * Get all recorded operations. * - * @return OperationInterface[] + * @return OperationInterface[] An array of recorded operations. */ public function getOperations(): array { @@ -142,19 +190,20 @@ public function getOperations(): array } /** - * Returns operations filtered by type. + * Get recorded operations that match the given type. * - * @return OperationInterface[] + * @param string $type The operation type to filter by. + * @return OperationInterface[] The operations whose type equals the provided type. */ public function getOperationsByType(string $type): array { return array_filter($this->operations, static fn(OperationInterface $op) => $op->getType() === $type); } - /** - * Returns all SQL queries. + / ** + * Collects SQL query strings from recorded SQL operations. * - * @return string[] + * @return string[] Array of SQL query strings; non-SQL entries contribute an empty string. */ public function getSqlQueries(): array { @@ -164,9 +213,9 @@ public function getSqlQueries(): array } /** - * Returns the count of operations by type. + * Compute counts of recorded operations grouped by their type. * - * @return array + * @return array Map of operation type to the number of recorded operations. */ public function getOperationCounts(): array { @@ -179,9 +228,9 @@ public function getOperationCounts(): array } /** - * Returns all operations as an array for dry-run output. + * Provide an array representation of all recorded operations suitable for dry-run output. * - * @return array> + * @return array> An indexed array where each element is an associative array representation of a recorded operation. */ public function toArray(): array { @@ -189,7 +238,9 @@ public function toArray(): array } /** - * Clears all recorded operations. + * Remove all recorded operations from the recorder. + * + * @return self The same recorder instance for method chaining. */ public function clear(): self { @@ -198,10 +249,12 @@ public function clear(): self } /** - * Returns the total count of operations. + * Get the total number of recorded operations. + * + * @return int The number of recorded operations. */ public function count(): int { return count($this->operations); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/PermissionGrantOperation.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/PermissionGrantOperation.php index 63a86417df..796cb49711 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/PermissionGrantOperation.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/PermissionGrantOperation.php @@ -24,6 +24,13 @@ readonly class PermissionGrantOperation implements OperationInterface { + /** + * Initialize the operation with configuration, the permission's name and description, and the target user ID. + * + * @param string $permissionName The machine name/key of the permission to create. + * @param string $permissionDescription Human-readable description of the permission. + * @param int $userId ID of the user who will receive the granted permission. + */ public function __construct( private Configuration $configuration, private string $permissionName, @@ -32,31 +39,61 @@ public function __construct( ) { } + /** + * Get the operation type identifier for this migration. + * + * @return string The operation type identifier 'permission_grant'. + */ public function getType(): string { return 'permission_grant'; } + /** + * Get a human-readable description of the permission being added. + * + * @return string The description formatted as "Add permission: {name} ({description})". + */ public function getDescription(): string { return sprintf('Add permission: %s (%s)', $this->permissionName, $this->permissionDescription); } + /** + * Get the permission name configured for this operation. + * + * @return string The configured permission name. + */ public function getPermissionName(): string { return $this->permissionName; } + /** + * Retrieve the human-readable description of the permission. + * + * @return string The permission description. + */ public function getPermissionDescription(): string { return $this->permissionDescription; } + /** + * Get the target user id for the permission grant. + * + * @return int The user id to which the new permission will be granted. + */ public function getUserId(): int { return $this->userId; } + /** + * Creates a new permission and assigns it to the configured user. + * + * @return bool `true` if the permission was added and granted to the user, `false` otherwise. + */ public function execute(): bool { try { @@ -73,6 +110,17 @@ public function execute(): bool } } + /** + * Serialize the operation into an associative array. + * + * @return array{ + * type: string, + * description: string, + * permissionName: string, + * permissionDescription: string, + * userId: int + * } An associative array with the operation's type, description, permission name and description, and target user ID. + */ public function toArray(): array { return [ @@ -83,4 +131,4 @@ public function toArray(): array 'userId' => $this->userId, ]; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/SqlOperation.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/SqlOperation.php index 4806eb44d8..eb12c36056 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/SqlOperation.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Operations/SqlOperation.php @@ -24,6 +24,12 @@ readonly class SqlOperation implements OperationInterface { + / ** + * Create a SQL migration operation with a query and optional description. + * + * @param string $query The SQL statement to execute during the migration. + * @param string $description Optional human-readable description; when empty a description will be derived from the query. + * / public function __construct( private Configuration $configuration, private string $query, @@ -31,11 +37,26 @@ public function __construct( ) { } + /** + * Provide the operation type identifier. + * + * @return string The operation type "sql". + */ public function getType(): string { return 'sql'; } + /** + * Provide a human-readable description for this SQL operation. + * + * Returns the explicitly set description if present; otherwise derives a short + * description from the SQL query (for example, "Create table " or + * "Insert into "). If the query pattern is not recognized, returns + * "Execute SQL query". + * + * @return string The operation description. + */ public function getDescription(): string { if ($this->description !== '') { @@ -76,11 +97,23 @@ public function getDescription(): string return 'Execute SQL query'; } + /** + * Get the stored SQL query. + * + * @return string The raw SQL query. + */ public function getQuery(): string { return $this->query; } + /** + * Execute the stored SQL query using the configured database connection. + * + * Runs the operation's SQL query and indicates whether execution succeeded. + * + * @return bool `true` if the query executed successfully, `false` otherwise. + */ public function execute(): bool { try { @@ -91,6 +124,14 @@ public function execute(): bool } } + /** + * Serialize the operation into an associative array representation. + * + * @return array{type:string,description:string,query:string} Associative array with keys: + * - `type`: operation type identifier (e.g., "sql"), + * - `description`: human-readable description of the operation, + * - `query`: the raw SQL query to be executed. + */ public function toArray(): array { return [ @@ -99,4 +140,4 @@ public function toArray(): array 'query' => $this->query, ]; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/AlterTableBuilder.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/AlterTableBuilder.php index 1a8ce1f69b..af14b58f45 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/AlterTableBuilder.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/AlterTableBuilder.php @@ -29,13 +29,25 @@ class AlterTableBuilder /** @var array */ private array $alterations = []; + /** + * Initialize the builder with an optional SQL dialect. + * + * If no dialect is provided, a default dialect is created via DialectFactory::create() + * and stored for later SQL generation. + * + * @param DialectInterface|null $dialect The dialect to use for SQL generation, or null to use the default. + */ public function __construct(?DialectInterface $dialect = null) { $this->dialect = $dialect ?? DialectFactory::create(); } /** - * Sets the table name. + * Set the target table for subsequent ALTER operations. + * + * @param string $name The table name without prefix. + * @param bool $withPrefix If true, prepend the configured table prefix to $name. + * @return self The current builder instance. */ public function table(string $name, bool $withPrefix = true): self { @@ -44,7 +56,13 @@ public function table(string $name, bool $withPrefix = true): self } /** - * Adds a new INTEGER column. + * Adds an INTEGER column definition to the builder for the current table. + * + * @param string $name The column name. + * @param bool $nullable Whether the column allows NULL. + * @param int|null $default Optional integer default value for the column. + * @param string|null $after Optional existing column name after which the new column should be placed. + * @return self The builder instance for method chaining. */ public function addInteger(string $name, bool $nullable = true, ?int $default = null, ?string $after = null): self { @@ -59,7 +77,14 @@ public function addInteger(string $name, bool $nullable = true, ?int $default = } /** - * Adds a new VARCHAR column. + * Add a VARCHAR column definition to the pending alterations for the current table. + * + * @param string $name Column name. + * @param int $length Length of the VARCHAR. + * @param bool $nullable Whether the column allows NULL. + * @param string|null $default Default value for the column (provided as an unquoted string; it will be quoted internally) or null for no default. + * @param string|null $after Optional column name to position the new column AFTER, or null to append. + * @return self The builder instance for method chaining. */ public function addVarchar( string $name, @@ -73,7 +98,12 @@ public function addVarchar( } /** - * Adds a new TEXT column. + * Add a TEXT column to the target table. + * + * @param string $name The column name. + * @param bool $nullable Whether the column allows NULL values. + * @param string|null $after Optional column name to position the new column after; ignored if the dialect does not support column positioning. + * @return self The builder instance for method chaining. */ public function addText(string $name, bool $nullable = true, ?string $after = null): self { @@ -81,7 +111,13 @@ public function addText(string $name, bool $nullable = true, ?string $after = nu } /** - * Adds a new BOOLEAN column. + * Add a BOOLEAN column to the current table definition. + * + * @param string $name The column name to add. + * @param bool $nullable Whether the column allows NULL. + * @param bool|null $default The default value for the column; when provided it will be converted to '1' or '0'. + * @param string|null $after Optional column name to position the new column after (may be ignored by some dialects). + * @return self The builder instance for method chaining. */ public function addBoolean(string $name, bool $nullable = true, ?bool $default = null, ?string $after = null): self { @@ -90,7 +126,13 @@ public function addBoolean(string $name, bool $nullable = true, ?bool $default = } /** - * Adds a new TIMESTAMP column. + * Adds a TIMESTAMP column definition to the builder for the configured table. + * + * @param string $name The column name. + * @param bool $nullable Whether the column allows NULL values. + * @param bool $defaultCurrent If true, sets the column default to the current timestamp. + * @param string|null $after Optional column name after which the new column should be placed. + * @return self The builder instance for method chaining. */ public function addTimestamp( string $name, @@ -103,7 +145,11 @@ public function addTimestamp( } /** - * Modifies an existing column type. + * Queue a modification that changes the specified column's type. + * + * @param string $name The name of the column to modify. + * @param string $type The new SQL column type definition to apply. + * @return self The current builder instance for method chaining. */ public function modifyColumn(string $name, string $type): self { @@ -119,7 +165,10 @@ public function modifyColumn(string $name, string $type): self } /** - * Drops a column. + * Schedule removal of a column from the target table. + * + * @param string $name The name of the column to drop. + * @return $this The builder instance for method chaining. */ public function dropColumn(string $name): self { @@ -135,10 +184,9 @@ public function dropColumn(string $name): self } /** - * Builds the ALTER TABLE statement(s). - * Returns an array of statements because some databases require separate statements for each alteration. + * Build ALTER TABLE statements for all queued alterations. * - * @return string[] + * @return string[] An array of SQL ALTER statements, one statement per recorded alteration. */ public function build(): array { @@ -178,7 +226,11 @@ public function build(): array } /** - * Builds a single ALTER TABLE statement combining all alterations (MySQL only). + * Build a single MySQL-style ALTER TABLE statement that combines all queued alterations. + * + * Processes ADD, MODIFY and DROP alterations in the order they were added. ADD parts include nullability and DEFAULT clauses, and will include an AFTER clause when the configured dialect supports column positioning. + * + * @return string The combined ALTER TABLE SQL statement for the configured table. */ public function buildCombined(): string { @@ -216,6 +268,17 @@ public function buildCombined(): string return "ALTER TABLE {$this->tableName} " . implode(', ', $parts); } + /** + * Append a column alteration entry to the builder's pending alterations. + * + * @param string $action The alteration action: 'ADD', 'MODIFY', or 'DROP'. + * @param string $name The column name to be altered. + * @param string $type The column SQL type or modification specification (e.g., "VARCHAR(255)"). + * @param bool $nullable True if the column should allow NULL, false for NOT NULL. + * @param string|null $default The default value as an SQL literal (e.g., "'text'", "1") or null for none. + * @param string|null $after Optional column name to position the new column after, or null to omit positioning. + * @return self The current AlterTableBuilder instance for chaining. + */ private function addColumn( string $action, string $name, @@ -234,4 +297,4 @@ private function addColumn( ]; return $this; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/MysqlDialect.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/MysqlDialect.php index 2155de6c96..299b1e0832 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/MysqlDialect.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/MysqlDialect.php @@ -23,93 +23,199 @@ class MysqlDialect implements DialectInterface { + /** + * Provide the driver type identifier for this dialect. + * + * @return string The driver type identifier — `'mysqli'`. + */ public function getType(): string { return 'mysqli'; } + /** + * Get the SQL type for integer columns. + * + * @return string The SQL type 'INT'. + */ public function integer(): string { return 'INT'; } + /** + * Provides the SQL type name for a big integer column. + * + * @return string The SQL type `BIGINT`. + */ public function bigInteger(): string { return 'BIGINT'; } + /** + * Provides the SQL type for a small integer column. + * + * @return string The SQL type `SMALLINT`. + */ public function smallInteger(): string { return 'SMALLINT'; } + /** + * Builds a VARCHAR type declaration with the specified length. + * + * @param int $length The maximum character length for the VARCHAR column. + * @return string The VARCHAR type declaration (e.g. `VARCHAR(255)`). + */ public function varchar(int $length): string { return "VARCHAR($length)"; } + /** + * Provide the SQL type for a variable-length text column. + * + * @return string The SQL type 'TEXT'. + */ public function text(): string { return 'TEXT'; } + /** + * SQL type used to represent boolean values in MySQL. + * + * @return string The SQL type 'TINYINT(1)' used to store boolean values. + */ public function boolean(): string { return 'TINYINT(1)'; } + /** + * Get the SQL type name for timestamp columns. + * + * @return string The SQL type 'TIMESTAMP'. + */ public function timestamp(): string { return 'TIMESTAMP'; } + /** + * Get the SQL type for date columns. + * + * @return string The SQL type 'DATE'. + */ public function date(): string { return 'DATE'; } + /** + * Create a CHAR type definition for the given length. + * + * @param int $length The character length for the CHAR column. + * @return string The SQL type definition `CHAR($length)`. + */ public function char(int $length): string { return "CHAR($length)"; } + /** + * SQL expression for the current timestamp. + * + * @return string The SQL expression `CURRENT_TIMESTAMP`. + */ public function currentTimestamp(): string { return 'CURRENT_TIMESTAMP'; } + /** + * SQL expression for retrieving the current date. + * + * @return string The SQL expression `CURDATE()` which yields the current date. + */ public function currentDate(): string { return 'CURDATE()'; } + /** + * Builds a column definition fragment for an auto-incrementing integer column. + * + * @param string $columnName The name of the column. + * @return string The SQL fragment for the column definition (e.g. "`id` INT NOT NULL AUTO_INCREMENT"). + */ public function autoIncrement(string $columnName): string { return "$columnName INT NOT NULL AUTO_INCREMENT"; } + /** + * Builds the prefix of a CREATE TABLE statement for the specified table. + * + * @param string $tableName The table name to create. + * @param bool $ifNotExists If true, includes 'IF NOT EXISTS' after 'CREATE TABLE'. + * @return string The SQL prefix: 'CREATE TABLE {IF NOT EXISTS }{tableName}'. + */ public function createTablePrefix(string $tableName, bool $ifNotExists = false): string { $exists = $ifNotExists ? 'IF NOT EXISTS ' : ''; return "CREATE TABLE {$exists}{$tableName}"; } + /** + * Provides the default table options fragment used when creating MySQL/MariaDB tables. + * + * @return string The SQL fragment specifying DEFAULT CHARACTER SET utf8mb4, COLLATE utf8mb4_unicode_ci, and ENGINE = InnoDB. + */ public function createTableSuffix(): string { return 'DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'; } + /** + * Builds an ALTER TABLE statement that adds a column to a table. + * + * @param string $tableName The target table name. + * @param string $columnName The name of the column to add. + * @param string $type The SQL type definition for the new column (e.g., 'INT NOT NULL'). + * @param string|null $after Column name after which the new column should be placed; if null the column is appended. + * @return string The generated ALTER TABLE ... ADD COLUMN SQL statement. + */ public function addColumn(string $tableName, string $columnName, string $type, ?string $after = null): string { $afterClause = $after !== null ? " AFTER $after" : ''; return "ALTER TABLE $tableName ADD COLUMN $columnName $type$afterClause"; } + /** + * Builds an ALTER TABLE statement to change a column's type. + * + * @param string $tableName The name of the table containing the column. + * @param string $columnName The name of the column to modify. + * @param string $newType The new column type and any modifiers (e.g., "INT NOT NULL"). + * @return string The ALTER TABLE SQL statement that modifies the column type. + */ public function modifyColumn(string $tableName, string $columnName, string $newType): string { return "ALTER TABLE $tableName MODIFY $columnName $newType"; } + /** + * Builds a CREATE INDEX statement for a MySQL table. + * + * @param string $indexName Name of the index. + * @param string $tableName Name of the table to create the index on. + * @param string[] $columns List of column names to include in the index, in order. + * @param bool $ifNotExists Ignored for MySQL (parameter retained for API compatibility). + * @return string The SQL `CREATE INDEX` statement for the given table and columns. + */ public function createIndex(string $indexName, string $tableName, array $columns, bool $ifNotExists = false): string { $columnList = implode(', ', $columns); @@ -117,18 +223,34 @@ public function createIndex(string $indexName, string $tableName, array $columns return "CREATE INDEX $indexName ON $tableName ($columnList)"; } + /** + * Build a DROP INDEX statement for the given index on a table. + * + * @returns string The SQL statement that drops the specified index from the specified table (e.g. "DROP INDEX index_name ON table_name"). + */ public function dropIndex(string $indexName, string $tableName): string { return "DROP INDEX $indexName ON $tableName"; } + /** + * Indicates whether this SQL dialect supports specifying column position (for example using `AFTER`) in ALTER statements. + * + * @return bool `true` if the dialect supports column positioning, `false` otherwise. + */ public function supportsColumnPositioning(): bool { return true; } + /** + * Escape backticks in an SQL identifier and wrap it in backticks for safe quoting. + * + * @param string $identifier The identifier to quote (e.g., table or column name). + * @return string The identifier with internal backticks doubled and surrounded by backticks (e.g. `escaped``name`). + */ public function quoteIdentifier(string $identifier): string { return '`' . str_replace('`', '``', $identifier) . '`'; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/PostgresDialect.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/PostgresDialect.php index 06dd5ae031..0c7dcf35bc 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/PostgresDialect.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/PostgresDialect.php @@ -23,93 +23,199 @@ class PostgresDialect implements DialectInterface { + /** + * Identify the SQL dialect as PostgreSQL. + * + * @return string The dialect identifier 'pgsql'. + */ public function getType(): string { return 'pgsql'; } + /** + * Provide the PostgreSQL integer column type. + * + * @return string The SQL type name 'INTEGER'. + */ public function integer(): string { return 'INTEGER'; } + /** + * Get the PostgreSQL column type used for big integers. + * + * @return string The PostgreSQL type name for big integers (`BIGINT`). + */ public function bigInteger(): string { return 'BIGINT'; } + /** + * Provide the PostgreSQL SQL type name for a small integer. + * + * @return string The SQL type `SMALLINT`. + */ public function smallInteger(): string { return 'SMALLINT'; } + /** + * Produces a VARCHAR column type declaration with the specified maximum length. + * + * @param int $length The maximum number of characters for the VARCHAR column. + * @return string The VARCHAR type declaration formatted as `VARCHAR($length)`. + */ public function varchar(int $length): string { return "VARCHAR($length)"; } + /** + * Returns the PostgreSQL column type for variable-length text. + * + * @return string The SQL type declaration `TEXT`. + */ public function text(): string { return 'TEXT'; } + /** + * Provide the SQL type used to represent boolean values in this dialect. + * + * @return string The SQL type name used for boolean values (e.g., "SMALLINT"). + */ public function boolean(): string { return 'SMALLINT'; } + /** + * Provide the PostgreSQL column type for timestamp values. + * + * @return string The SQL type name 'TIMESTAMP'. + */ public function timestamp(): string { return 'TIMESTAMP'; } + /** + * Get the SQL DATE type name used by the PostgreSQL dialect. + * + * @return string The SQL type name 'DATE'. + */ public function date(): string { return 'DATE'; } + / ** + * Get a fixed-length CHAR type declaration for the specified length. + * + * @param int $length The number of characters for the CHAR type. + * @return string The SQL type declaration formatted as "CHAR()". + */ public function char(int $length): string { return "CHAR($length)"; } + /** + * Return the SQL expression that yields the current timestamp in PostgreSQL. + * + * @return string The SQL expression CURRENT_TIMESTAMP. + */ public function currentTimestamp(): string { return 'CURRENT_TIMESTAMP'; } + /** + * SQL expression for the current date in PostgreSQL. + * + * @return string The SQL expression `CURRENT_DATE`. + */ public function currentDate(): string { return 'CURRENT_DATE'; } + /** + * Produce a PostgreSQL auto-increment column definition for the given column name. + * + * @param string $columnName The column name to define as auto-increment. + * @return string The SQL fragment for the column definition using `SERIAL NOT NULL`. + */ public function autoIncrement(string $columnName): string { return "$columnName SERIAL NOT NULL"; } + /** + * Builds the prefix for a CREATE TABLE statement for the given table. + * + * @param string $tableName The name of the table to create (unquoted). + * @param bool $ifNotExists When true, includes the `IF NOT EXISTS` clause. + * @return string The CREATE TABLE statement prefix (e.g. `CREATE TABLE IF NOT EXISTS my_table` or `CREATE TABLE my_table`). + */ public function createTablePrefix(string $tableName, bool $ifNotExists = false): string { $exists = $ifNotExists ? 'IF NOT EXISTS ' : ''; return "CREATE TABLE {$exists}{$tableName}"; } + /** + * Provide the suffix appended to CREATE TABLE statements. + * + * @return string An empty string (no suffix) for CREATE TABLE statements. + */ public function createTableSuffix(): string { return ''; } + /** + * Generate an ALTER TABLE statement to add a column to a PostgreSQL table. + * + * @param string $tableName The name of the table to alter. + * @param string $columnName The name of the column to add. + * @param string $type The SQL type declaration for the new column. + * @param string|null $after Ignored for PostgreSQL (present for interface compatibility). + * @return string The SQL statement, e.g. "ALTER TABLE ADD COLUMN ". + */ public function addColumn(string $tableName, string $columnName, string $type, ?string $after = null): string { // PostgreSQL doesn't support AFTER clause return "ALTER TABLE $tableName ADD COLUMN $columnName $type"; } + /** + * Builds an ALTER TABLE statement to change a column's data type. + * + * @param string $tableName The name of the table containing the column. + * @param string $columnName The name of the column to modify. + * @param string $newType The new SQL type declaration for the column (e.g. "VARCHAR(255)"). + * @return string The SQL ALTER TABLE statement that changes the column's type. + */ public function modifyColumn(string $tableName, string $columnName, string $newType): string { return "ALTER TABLE $tableName ALTER COLUMN $columnName TYPE $newType"; } + /**** + * Builds a PostgreSQL CREATE INDEX statement for the given table and columns. + * + * @param string $indexName The name of the index to create. + * @param string $tableName The table on which to create the index. + * @param string[] $columns An ordered list of column names to include in the index. + * @param bool $ifNotExists When true, includes the IF NOT EXISTS clause to avoid error if the index already exists. + * @return string The SQL CREATE INDEX statement. + */ public function createIndex(string $indexName, string $tableName, array $columns, bool $ifNotExists = false): string { $columnList = implode(', ', $columns); @@ -117,18 +223,36 @@ public function createIndex(string $indexName, string $tableName, array $columns return "CREATE INDEX {$exists}$indexName ON $tableName ($columnList)"; } + /** + * Build a PostgreSQL DROP INDEX statement for the given index. + * + * @param string $indexName The name of the index to drop. + * @param string $tableName The table name (ignored for PostgreSQL; present for interface compatibility). + * @return string The DROP INDEX SQL statement. + */ public function dropIndex(string $indexName, string $tableName): string { return "DROP INDEX $indexName"; } + /** + * Indicates whether the dialect supports column positioning (for example an `AFTER` clause) in DDL statements. + * + * @return bool `true` if column positioning is supported, `false` otherwise. + */ public function supportsColumnPositioning(): bool { return false; } + /** + * Quote an SQL identifier for use in PostgreSQL statements. + * + * @param string $identifier The identifier to quote (e.g., table or column name). + * @return string The identifier wrapped in double quotes with any embedded double quotes escaped by doubling. + */ public function quoteIdentifier(string $identifier): string { return '"' . str_replace('"', '""', $identifier) . '"'; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/SqlServerDialect.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/SqlServerDialect.php index 187ab0df25..c73b866c76 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/SqlServerDialect.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/SqlServerDialect.php @@ -23,71 +23,146 @@ class SqlServerDialect implements DialectInterface { + /** + * Provide the dialect identifier for SQL Server. + * + * @return string The dialect identifier 'sqlsrv'. + */ public function getType(): string { return 'sqlsrv'; } + /** + * SQL Server column type for a standard integer. + * + * @return string The SQL Server type `INT`. + */ public function integer(): string { return 'INT'; } + /** + * Get the SQL Server column type for big integers. + * + * @return string The SQL Server data type for 64-bit integers: 'BIGINT'. + */ public function bigInteger(): string { return 'BIGINT'; } + /** + * SQL Server column type for a small integer. + * + * @return string The SQL Server type `SMALLINT`. + */ public function smallInteger(): string { return 'SMALLINT'; } + /** + * Generates a Unicode variable-length character type declaration for SQL Server using the specified length. + * + * @param int $length The maximum number of characters. + * @return string The NVARCHAR type declaration (e.g. "NVARCHAR(255)"). + */ public function varchar(int $length): string { return "NVARCHAR($length)"; } + /** + * Provides the SQL Server column type for large Unicode text columns. + * + * @return string The SQL Server column type for large Unicode text (NVARCHAR(MAX)). + */ public function text(): string { return 'NVARCHAR(MAX)'; } + /** + * SQL Server column type used to store boolean values. + * + * @return string The SQL Server type used to represent booleans (e.g. 'TINYINT'). + */ public function boolean(): string { return 'TINYINT'; } + /** + * SQL Server column type for timestamp values. + * + * @return string The SQL Server type name for timestamp columns (`DATETIME`). + */ public function timestamp(): string { return 'DATETIME'; } + /** + * Provide the SQL Server column type for date values. + * + * @return string The SQL Server DATE type. + */ public function date(): string { return 'DATE'; } + /** + * Get the SQL Server fixed-length Unicode character type for a given length. + * + * @param int $length The number of characters for the NCHAR type. + * @return string The SQL Server type declaration `NCHAR(length)`. + */ public function char(int $length): string { return "NCHAR($length)"; } + /** + * Provide the SQL Server expression that yields the current date and time. + * + * @return string The SQL expression `GETDATE()` which returns the current date and time in SQL Server. + */ public function currentTimestamp(): string { return 'GETDATE()'; } + /** + * SQL Server expression that yields the current date and time. + * + * @return string The SQL expression `GETDATE()` which evaluates to the current date and time. + */ public function currentDate(): string { return 'GETDATE()'; } + /** + * Generate a SQL Server column definition for an auto-incrementing integer column. + * + * @param string $columnName The name of the column. + * @return string The SQL fragment defining the column as `INT IDENTITY(1,1) NOT NULL`. + */ public function autoIncrement(string $columnName): string { return "$columnName INT IDENTITY(1,1) NOT NULL"; } + / ** + * Generate the SQL prefix used to create a table in SQL Server. + * + * @param string $tableName The name of the table to create. + * @param bool $ifNotExists If true, prepend a conditional existence check using `sysobjects` so the table is created only when it does not already exist. + * @return string The SQL statement prefix for creating the specified table; when `$ifNotExists` is true the prefix includes an `IF NOT EXISTS` check. + */ public function createTablePrefix(string $tableName, bool $ifNotExists = false): string { if ($ifNotExists) { @@ -99,22 +174,53 @@ public function createTablePrefix(string $tableName, bool $ifNotExists = false): return "CREATE TABLE $tableName"; } + /** + * SQL Server-specific suffix appended after a CREATE TABLE statement. + * + * @return string An empty string indicating no suffix is appended. + */ public function createTableSuffix(): string { return ''; } + /** + * Generate an ALTER TABLE statement to add a column to a SQL Server table. + * + * @param string $tableName The target table name. + * @param string $columnName The name of the column to add. + * @param string $type The column type definition (for example, 'INT' or 'NVARCHAR(255)'). + * @param string|null $after Ignored — provided for interface compatibility; SQL Server does not support column positioning. + * @return string The SQL Server `ALTER TABLE ... ADD ...` statement for adding the column. + */ public function addColumn(string $tableName, string $columnName, string $type, ?string $after = null): string { // SQL Server doesn't support AFTER clause, and uses different syntax return "ALTER TABLE $tableName ADD $columnName $type"; } + /** + * Generate SQL to alter a column's type in a table. + * + * @param string $tableName The name of the table containing the column. + * @param string $columnName The name of the column to modify. + * @param string $newType The new SQL type and modifiers for the column (e.g., `NVARCHAR(255) NOT NULL`). + * @return string The ALTER TABLE statement that changes the column's type to the specified definition. + */ public function modifyColumn(string $tableName, string $columnName, string $newType): string { return "ALTER TABLE $tableName ALTER COLUMN $columnName $newType"; } + /** + * Builds a SQL statement to create an index on a table. + * + * @param string $indexName The name of the index to create. + * @param string $tableName The table on which to create the index. + * @param string[] $columns The list of column names to include in the index, in order. + * @param bool $ifNotExists When true, include a conditional existence check so the index is only created if it does not already exist. + * @return string The SQL statement that creates the index; if `$ifNotExists` is true the statement includes a preceding `IF NOT EXISTS` check. + */ public function createIndex(string $indexName, string $tableName, array $columns, bool $ifNotExists = false): string { $columnList = implode(', ', $columns); @@ -127,18 +233,36 @@ public function createIndex(string $indexName, string $tableName, array $columns return "CREATE INDEX $indexName ON $tableName ($columnList)"; } + /** + * Generates a SQL Server statement to drop an index from a table. + * + * @param string $indexName The name of the index to drop. + * @param string $tableName The table containing the index. + * @return string The SQL statement that drops the specified index on the given table. + */ public function dropIndex(string $indexName, string $tableName): string { return "DROP INDEX $indexName ON $tableName"; } + /** + * Indicates whether this dialect supports specifying column position when adding or modifying columns. + * + * @return bool `true` if column positioning is supported, `false` otherwise. + */ public function supportsColumnPositioning(): bool { return false; } + / ** + * Quote an SQL Server identifier with square brackets, escaping any closing brackets. + * + * @param string $identifier The identifier to quote; any `]` characters will be escaped by doubling them. + * @return string The identifier wrapped in `[` and `]` with internal `]` characters replaced by `]]`. + */ public function quoteIdentifier(string $identifier): string { return '[' . str_replace(']', ']]', $identifier) . ']'; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/SqliteDialect.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/SqliteDialect.php index 58fb6aea66..f6557f69cd 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/SqliteDialect.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/Dialect/SqliteDialect.php @@ -23,88 +23,192 @@ class SqliteDialect implements DialectInterface { + /** + * Get the dialect type identifier. + * + * @return string The dialect type identifier "sqlite3". + */ public function getType(): string { return 'sqlite3'; } + /** + * Provide the SQLite column type for integers. + * + * @return string The SQL type `INTEGER`. + */ public function integer(): string { return 'INTEGER'; } + /** + * Maps a migration "bigInteger" column to SQLite's integer type. + * + * @return string The SQL type name `INTEGER`. + */ public function bigInteger(): string { return 'INTEGER'; } + /** + * Provide the SQL column type used for a small integer in SQLite. + * + * @return string The SQL type `INTEGER`. + */ public function smallInteger(): string { return 'INTEGER'; } + /** + * Create a VARCHAR type declaration with the given maximum length. + * + * @param int $length The maximum character length for the column. + * @return string The SQL type declaration, e.g. "VARCHAR(255)". + */ public function varchar(int $length): string { return "VARCHAR($length)"; } + /** + * SQLite column type used for storing text. + * + * @return string The SQLite type name "TEXT". + */ public function text(): string { return 'TEXT'; } + /** + * Maps a boolean column to SQLite's storage type for boolean values. + * + * @return string The SQLite type name 'INTEGER' used to store boolean values (0 or 1). + */ public function boolean(): string { return 'INTEGER'; } + /** + * Provide the SQL type name for timestamp columns in SQLite. + * + * @return string The SQL type name used for timestamp columns (`DATETIME`). + */ public function timestamp(): string { return 'DATETIME'; } + /** + * Provide the SQL column type for date values in SQLite. + * + * @return string The SQL type name 'DATE'. + */ public function date(): string { return 'DATE'; } + /** + * Produce a SQLite CHAR type declaration for the given length. + * + * @param int $length The number of characters for the CHAR type. + * @return string The SQL type declaration, e.g. "CHAR(10)". + */ public function char(int $length): string { return "CHAR($length)"; } + /** + * SQLite expression that yields the current timestamp. + * + * @return string The SQL expression `CURRENT_TIMESTAMP`. + */ public function currentTimestamp(): string { return 'CURRENT_TIMESTAMP'; } + /** + * SQLite expression that yields the current date in YYYY-MM-DD format. + * + * @return string The SQL expression "(date('now'))" which evaluates to the current UTC date in YYYY-MM-DD format. + */ public function currentDate(): string { return "(date('now'))"; } + /** + * Build a SQLite column definition that makes the given column an autoincrementing primary key. + * + * @param string $columnName The name of the column to define as an autoincrementing primary key. + * @return string The column definition, e.g. "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT". + */ public function autoIncrement(string $columnName): string { return "$columnName INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT"; } + /** + * Build the SQL prefix for creating a table in SQLite. + * + * @param string $tableName The name of the table. + * @param bool $ifNotExists If true, include the `IF NOT EXISTS` clause. + * @return string The `CREATE TABLE` statement prefix for the specified table, optionally containing `IF NOT EXISTS`. + */ public function createTablePrefix(string $tableName, bool $ifNotExists = false): string { $exists = $ifNotExists ? 'IF NOT EXISTS ' : ''; return "CREATE TABLE {$exists}{$tableName}"; } + /** + * Provides the SQL suffix appended to CREATE TABLE statements for this dialect. + * + * SQLite does not require a table suffix. + * + * @return string An empty string (no suffix). + */ public function createTableSuffix(): string { return ''; } + /** + * Generate an ALTER TABLE statement to add a new column to a SQLite table. + * + * The optional `$after` parameter is ignored because SQLite does not support column positioning. + * + * @param string $tableName The name of the table to alter. + * @param string $columnName The name of the column to add. + * @param string $type The column type and any constraints (e.g., "INTEGER NOT NULL"). + * @param string|null $after Ignored for SQLite; kept for API compatibility. + * @return string The SQL statement to add the column (e.g., "ALTER TABLE
ADD COLUMN "). + */ public function addColumn(string $tableName, string $columnName, string $type, ?string $after = null): string { // SQLite doesn't support AFTER clause return "ALTER TABLE $tableName ADD COLUMN $columnName $type"; } + /** + * Indicate that a column's type cannot be modified directly in SQLite. + * + * This method always throws a RuntimeException because SQLite does not support altering column types in place; + * callers must rebuild the table to effect column changes. + * + * @param string $tableName The name of the table containing the column. + * @param string $columnName The name of the column to modify. + * @param string $newType The desired new column type definition. + * @throws \RuntimeException Always thrown to signal that column modification is not supported in SQLite. + */ public function modifyColumn(string $tableName, string $columnName, string $newType): string { // SQLite doesn't support ALTER COLUMN directly @@ -112,6 +216,15 @@ public function modifyColumn(string $tableName, string $columnName, string $newT throw new \RuntimeException('SQLite does not support modifying columns. Use table rebuild pattern.'); } + /** + * Builds a CREATE INDEX SQL statement for the specified table and columns. + * + * @param string $indexName The name of the index to create. + * @param string $tableName The table on which to create the index. + * @param string[] $columns Ordered list of column names or identifiers to include in the index. + * @param bool $ifNotExists When true, includes `IF NOT EXISTS` to avoid an error if the index already exists. + * @return string The SQL CREATE INDEX statement. If `$ifNotExists` is true, the statement includes `IF NOT EXISTS`. + */ public function createIndex(string $indexName, string $tableName, array $columns, bool $ifNotExists = false): string { $columnList = implode(', ', $columns); @@ -119,18 +232,36 @@ public function createIndex(string $indexName, string $tableName, array $columns return "CREATE INDEX {$exists}$indexName ON $tableName ($columnList)"; } + /** + * Generate SQL to drop an index if it exists. + * + * @param string $indexName The name of the index to drop. + * @param string $tableName The table name (unused; present for interface compatibility). + * @return string The DROP INDEX statement with IF EXISTS for the specified index. + */ public function dropIndex(string $indexName, string $tableName): string { return "DROP INDEX IF EXISTS $indexName"; } + /** + * Indicates whether this SQL dialect supports specifying column position when altering a table. + * + * @return bool `true` if column positioning is supported, `false` otherwise. + */ public function supportsColumnPositioning(): bool { return false; } + /** + * Quote an SQL identifier for SQLite by wrapping it in double quotes and escaping any embedded double quotes. + * + * @param string $identifier The identifier to quote (e.g., table or column name). + * @return string The identifier wrapped in double quotes with internal double quotes doubled. + */ public function quoteIdentifier(string $identifier): string { return '"' . str_replace('"', '""', $identifier) . '"'; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/DialectFactory.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/DialectFactory.php index d3ec06cde0..e6128ef7a9 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/DialectFactory.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/DialectFactory.php @@ -28,7 +28,9 @@ class DialectFactory { /** - * Creates the appropriate dialect for the current database type. + * Instantiate a Dialect implementation appropriate for the application's current database type. + * + * @return DialectInterface The dialect instance corresponding to Database::getType(). */ public static function create(): DialectInterface { @@ -36,7 +38,11 @@ public static function create(): DialectInterface } /** - * Creates a dialect for the specified database type. + * Create a dialect instance corresponding to the given database type. + * + * @param string $dbType The database type identifier (accepted values: 'mysqli', 'pdo_mysql', 'pgsql', 'pdo_pgsql', 'sqlite3', 'pdo_sqlite', 'sqlsrv', 'pdo_sqlsrv'). + * @return DialectInterface The dialect implementation for the specified database type. + * @throws \InvalidArgumentException If the provided database type is not supported. */ public static function createForType(string $dbType): DialectInterface { @@ -48,4 +54,4 @@ public static function createForType(string $dbType): DialectInterface default => throw new \InvalidArgumentException("Unsupported database type: $dbType"), }; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/DialectInterface.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/DialectInterface.php index 46c1fb8302..8118c70d09 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/DialectInterface.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/DialectInterface.php @@ -22,92 +22,144 @@ interface DialectInterface { /** - * Returns the database type identifier (e.g., 'mysqli', 'pgsql'). - */ + * Get the database dialect identifier. + * + * @return string The database type identifier, e.g. "mysqli" or "pgsql". + */ public function getType(): string; /** - * Returns the INTEGER column type. - */ + * Get the SQL type used for an INTEGER column. + * + * @return string The SQL type definition for an INTEGER column (for example, "INT"). + */ public function integer(): string; /** - * Returns the BIGINT column type. - */ + * Get the SQL type definition for a BIGINT column. + * + * @return string The SQL column type used for big integers (e.g., "BIGINT"). + */ public function bigInteger(): string; /** - * Returns the SMALLINT column type. - */ + * Get the SQL type for a SMALLINT column. + * + * @return string The SQL type for a SMALLINT column. + */ public function smallInteger(): string; /** - * Returns the VARCHAR column type with length. - */ + * Produce the SQL type definition for a VARCHAR column of the specified length. + * + * @param int $length The maximum character length of the VARCHAR column. + * @return string The SQL type string for a VARCHAR column of the given length. + */ public function varchar(int $length): string; /** - * Returns the TEXT column type. - */ + * Get the SQL type for a TEXT column. + * + * @return string The SQL type string used for a TEXT column. + */ public function text(): string; /** - * Returns the BOOLEAN/TINYINT column type. - */ + * Get the SQL column type used to store boolean values. + * + * @return string The SQL type declaration for a boolean column (for example `BOOLEAN` or `TINYINT(1)`). + */ public function boolean(): string; /** - * Returns the TIMESTAMP/DATETIME column type. - */ + * Get the SQL column type used for timestamp/datetime values. + * + * @return string The SQL type for a TIMESTAMP or DATETIME column. + */ public function timestamp(): string; /** - * Returns the DATE column type. - */ + * Provide the SQL type used for DATE columns. + * + * @return string The SQL type definition for a DATE column. + */ public function date(): string; - /** - * Returns the CHAR column type with length. - */ + / ** + * Get the SQL declaration for a CHAR column of the specified length. + * + * @param int $length The number of characters for the CHAR column. + * @return string The SQL type declaration for a CHAR column with the given length. + */ public function char(int $length): string; /** - * Returns the current timestamp function/default. - */ + * Provide the SQL expression used to represent the current timestamp. + * + * @return string The SQL expression or default value to use for a column's current timestamp (e.g. `CURRENT_TIMESTAMP` or a dialect-specific function). + */ public function currentTimestamp(): string; /** - * Returns the current date function/default. - */ + * Get the SQL expression that yields the current date for this dialect. + * + * @return string The SQL expression or default value used to produce the current date (for example `CURRENT_DATE`). + */ public function currentDate(): string; /** - * Returns the auto-increment column definition. - */ + * Provide the SQL fragment that declares the specified column as auto-incrementing. + * + * @param string $columnName The column name to mark as auto-increment (unquoted identifier). + * @return string The SQL fragment used to declare the column as auto-increment. + */ public function autoIncrement(string $columnName): string; /** - * Returns the CREATE TABLE prefix with options. - */ + * Build the initial CREATE TABLE statement for the given table, optionally including an IF NOT EXISTS clause. + * + * @param string $tableName The table name to create (identifier will be quoted by the dialect implementation as needed). + * @param bool $ifNotExists When true, include an IF NOT EXISTS clause to avoid error if the table already exists. + * @return string The SQL prefix for a CREATE TABLE statement for the specified table. + */ public function createTablePrefix(string $tableName, bool $ifNotExists = false): string; /** - * Returns the CREATE TABLE suffix with engine/charset options. - */ + * Provide the CREATE TABLE suffix containing engine, charset, and other table-level options for the dialect. + * + * @return string The SQL fragment to append to a CREATE TABLE statement (e.g., engine, charset, collation settings), or an empty string if the dialect does not require a suffix. + */ public function createTableSuffix(): string; /** - * Returns the ALTER TABLE ADD COLUMN syntax. - */ + * Build an ALTER TABLE fragment to add a column to a table. + * + * @param string $tableName The name of the table to alter. + * @param string $columnName The name of the column to add. + * @param string $type The column type definition (including length, nullability, defaults, etc.). + * @param string|null $after Optional column name after which the new column should be placed; null to omit positioning. + * @return string The complete ALTER TABLE ADD COLUMN SQL statement for the given table and column. + */ public function addColumn(string $tableName, string $columnName, string $type, ?string $after = null): string; /** - * Returns the ALTER TABLE MODIFY/ALTER COLUMN syntax. - */ + * Generate an ALTER TABLE statement that changes the type/definition of a specified column. + * + * @param string $tableName The target table name. + * @param string $columnName The column to modify. + * @param string $newType The full SQL type/definition to apply to the column (including length, nullability, defaults, etc., as required by the dialect). + * @return string The SQL statement that alters the table to modify the specified column to the given type. + */ public function modifyColumn(string $tableName, string $columnName, string $newType): string; /** - * Returns the CREATE INDEX syntax. + * Builds a CREATE INDEX statement for the given table and columns. + * + * @param string $indexName The name of the index to create. + * @param string $tableName The table on which the index will be created. + * @param string[] $columns Ordered list of column identifiers to include in the index. + * @param bool $ifNotExists Whether to include an IF NOT EXISTS clause when supported. + * @return string The CREATE INDEX SQL statement. */ public function createIndex( string $indexName, @@ -117,17 +169,25 @@ public function createIndex( ): string; /** - * Returns the DROP INDEX syntax. - */ + * Builds a DROP INDEX statement for the specified index on a table. + * + * @param string $indexName The index name. + * @param string $tableName The table name. + * @return string The DROP INDEX SQL statement targeting the given table and index. + */ public function dropIndex(string $indexName, string $tableName): string; /** - * Returns whether AFTER clause is supported in ALTER TABLE ADD COLUMN. - */ + * Indicates whether the dialect supports specifying a column position with an AFTER clause when adding a column. + * + * @return bool `true` if the dialect supports an AFTER clause for column positioning, `false` otherwise. + */ public function supportsColumnPositioning(): bool; /** - * Quotes an identifier (table name, column name). - */ + * Quote a SQL identifier (such as a table or column name) for safe use in queries. + * + * @param string $identifier The identifier to quote; may include a schema or table prefix. + * @return string The quoted identifier. */ public function quoteIdentifier(string $identifier): string; -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/QueryBuilder.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/QueryBuilder.php index e8947a75d7..c5f2dd1ffd 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/QueryBuilder.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/QueryBuilder.php @@ -26,6 +26,11 @@ class QueryBuilder private DialectInterface $dialect; private string $tablePrefix; + /** + * Initialize the QueryBuilder with a SQL dialect and configured table prefix. + * + * @param DialectInterface|null $dialect Optional SQL dialect to use. If null, a default dialect is created. + */ public function __construct(?DialectInterface $dialect = null) { $this->dialect = $dialect ?? DialectFactory::create(); @@ -33,8 +38,12 @@ public function __construct(?DialectInterface $dialect = null) } /** - * Creates a new TableBuilder for CREATE TABLE statements. - */ + * Create a TableBuilder configured for the given table name. + * + * @param string $tableName The table name (without prefix). + * @param bool $withPrefix Whether to prepend the configured table prefix. + * @return TableBuilder The configured TableBuilder for building a CREATE TABLE statement. + */ public function createTable(string $tableName, bool $withPrefix = true): TableBuilder { $builder = new TableBuilder($this->dialect); @@ -42,7 +51,11 @@ public function createTable(string $tableName, bool $withPrefix = true): TableBu } /** - * Creates a new TableBuilder with IF NOT EXISTS. + * Create a TableBuilder configured for the given table with "IF NOT EXISTS" enabled. + * + * @param string $tableName The name of the table to build. + * @param bool $withPrefix Whether to apply the configured table prefix. + * @return TableBuilder The configured TableBuilder with IF NOT EXISTS set. */ public function createTableIfNotExists(string $tableName, bool $withPrefix = true): TableBuilder { @@ -51,7 +64,11 @@ public function createTableIfNotExists(string $tableName, bool $withPrefix = tru } /** - * Creates a new AlterTableBuilder for ALTER TABLE statements. + * Create an AlterTableBuilder configured for the specified table. + * + * @param string $tableName The target table name (provided without prefix). + * @param bool $withPrefix Whether to prepend the configured table prefix to $tableName. + * @return AlterTableBuilder The builder configured for the resolved table name. */ public function alterTable(string $tableName, bool $withPrefix = true): AlterTableBuilder { @@ -60,8 +77,12 @@ public function alterTable(string $tableName, bool $withPrefix = true): AlterTab } /** - * Creates a DROP TABLE statement. - */ + * Builds a DROP TABLE SQL statement for the specified table. + * + * @param string $tableName The table name (without prefix). + * @param bool $withPrefix If true, prepends the configured table prefix to `$tableName`. + * @return string The generated `DROP TABLE` statement targeting the resolved table name. + */ public function dropTable(string $tableName, bool $withPrefix = true): string { $fullName = $withPrefix ? $this->tablePrefix . $tableName : $tableName; @@ -69,8 +90,12 @@ public function dropTable(string $tableName, bool $withPrefix = true): string } /** - * Creates a DROP TABLE IF EXISTS statement. - */ + * Build a DROP TABLE IF EXISTS SQL statement for the specified table. + * + * @param string $tableName The name of the table (without prefix). + * @param bool $withPrefix Whether to prepend the configured table prefix to the table name. + * @return string The SQL statement "DROP TABLE IF EXISTS ". + */ public function dropTableIfExists(string $tableName, bool $withPrefix = true): string { $fullName = $withPrefix ? $this->tablePrefix . $tableName : $tableName; @@ -78,9 +103,13 @@ public function dropTableIfExists(string $tableName, bool $withPrefix = true): s } /** - * Creates a CREATE INDEX statement. + * Builds a CREATE INDEX SQL statement for the specified table and columns. * - * @param string|string[] $columns + * @param string $indexName The name of the index. + * @param string $tableName The table name; the configured table prefix is prepended when $withPrefix is true. + * @param string|array $columns A column name or an array of column names to include in the index. + * @param bool $withPrefix Whether to prepend the configured table prefix to $tableName. + * @return string The generated CREATE INDEX SQL statement. */ public function createIndex( string $indexName, @@ -93,9 +122,13 @@ public function createIndex( } /** - * Creates a CREATE INDEX IF NOT EXISTS statement. + * Builds a CREATE INDEX IF NOT EXISTS SQL statement for the specified table. * - * @param string|string[] $columns + * @param string $indexName The name of the index to create. + * @param string $tableName The table name to create the index on. + * @param string|string[] $columns Column name or array of column names to include in the index. + * @param bool $withPrefix Whether to prepend the configured table prefix to $tableName. + * @return string The generated CREATE INDEX IF NOT EXISTS SQL statement. */ public function createIndexIfNotExists( string $indexName, @@ -108,8 +141,13 @@ public function createIndexIfNotExists( } /** - * Creates a DROP INDEX statement. - */ + * Builds a DROP INDEX SQL statement for an index on a table. + * + * @param string $indexName The name of the index to drop. + * @param string $tableName The table name (without prefix). + * @param bool $withPrefix Whether to prepend the configured table prefix to $tableName. + * @return string The generated DROP INDEX SQL statement. + */ public function dropIndex(string $indexName, string $tableName, bool $withPrefix = true): string { $fullTableName = $withPrefix ? $this->tablePrefix . $tableName : $tableName; @@ -117,7 +155,9 @@ public function dropIndex(string $indexName, string $tableName, bool $withPrefix } /** - * Gets the current dialect. + * Provides the SQL dialect used to generate SQL statements. + * + * @return DialectInterface The dialect instance used for SQL generation. */ public function getDialect(): DialectInterface { @@ -125,7 +165,9 @@ public function getDialect(): DialectInterface } /** - * Gets the table prefix. + * Return the configured database table name prefix used by this QueryBuilder. + * + * @return string The table prefix (possibly an empty string) that is prepended to table names. */ public function getTablePrefix(): string { @@ -133,10 +175,13 @@ public function getTablePrefix(): string } /** - * Returns a prefixed table name. - */ + * Compute the table name using the configured table prefix. + * + * @param string $name The base table name without prefix. + * @return string The table name prefixed with the configured table prefix. + */ public function table(string $name): string { return $this->tablePrefix . $name; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/TableBuilder.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/TableBuilder.php index 5943d714b2..dac403599c 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/TableBuilder.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/QueryBuilder/TableBuilder.php @@ -36,13 +36,22 @@ class TableBuilder /** @var array */ private array $indexes = []; + /** + * Create a TableBuilder using the provided SQL dialect or a default dialect. + * + * @param DialectInterface|null $dialect The dialect implementation to use; if null, a default dialect is created. + */ public function __construct(?DialectInterface $dialect = null) { $this->dialect = $dialect ?? DialectFactory::create(); } /** - * Sets the table name. + * Set the table name for the builder, optionally applying the global table prefix. + * + * @param string $name The table name (without prefix). + * @param bool $withPrefix When true, prepend Database::getTablePrefix() to the name. + * @return self Fluent instance for method chaining. */ public function table(string $name, bool $withPrefix = true): self { @@ -51,7 +60,9 @@ public function table(string $name, bool $withPrefix = true): self } /** - * Adds IF NOT EXISTS clause. + * Enable the IF NOT EXISTS clause for the CREATE TABLE statement. + * + * @return $this The current TableBuilder instance for method chaining. */ public function ifNotExists(): self { @@ -60,8 +71,13 @@ public function ifNotExists(): self } /** - * Adds an INTEGER column. - */ + * Add an INTEGER column to the table definition. + * + * @param string $name Column name. + * @param bool $nullable Whether the column allows NULL. + * @param int|null $default Optional default value for the column. + * @return self The builder instance. + */ public function integer(string $name, bool $nullable = true, ?int $default = null): self { return $this->addColumn( @@ -86,7 +102,12 @@ public function bigInteger(string $name, bool $nullable = true, ?int $default = } /** - * Adds a SMALLINT column. + * Add a SMALLINT column to the table definition. + * + * @param string $name The column name. + * @param bool $nullable Whether the column allows NULL. + * @param int|null $default The default integer value for the column, or null for no default. + * @return self The current builder instance for method chaining. */ public function smallInteger(string $name, bool $nullable = true, ?int $default = null): self { @@ -99,7 +120,13 @@ public function smallInteger(string $name, bool $nullable = true, ?int $default } /** - * Adds a VARCHAR column. + * Add a VARCHAR column to the table definition. + * + * @param string $name Column name. + * @param int $length Maximum character length for the column. + * @param bool $nullable Whether the column allows NULL. + * @param string|null $default Default string value for the column (unquoted); `null` means no default. + * @return self The builder instance for method chaining. */ public function varchar(string $name, int $length, bool $nullable = true, ?string $default = null): self { @@ -108,7 +135,11 @@ public function varchar(string $name, int $length, bool $nullable = true, ?strin } /** - * Adds a TEXT column. + * Add a TEXT column to the table definition. + * + * @param string $name The column name. + * @param bool $nullable Whether the column allows NULL (default: true). + * @return self The current builder instance for method chaining. */ public function text(string $name, bool $nullable = true): self { @@ -116,8 +147,13 @@ public function text(string $name, bool $nullable = true): self } /** - * Adds a BOOLEAN/TINYINT column. - */ + * Adds a BOOLEAN/TINYINT column to the table definition. + * + * @param string $name The column name. + * @param bool $nullable Whether the column allows NULL (true) or is NOT NULL (false). + * @param bool|null $default Optional default value; when provided, `true` is converted to `'1'` and `false` to `'0'` for the column default. + * @return self The builder instance for chaining. + */ public function boolean(string $name, bool $nullable = true, ?bool $default = null): self { $defaultVal = $default !== null ? ($default ? '1' : '0') : null; @@ -125,7 +161,14 @@ public function boolean(string $name, bool $nullable = true, ?bool $default = nu } /** - * Adds a TIMESTAMP/DATETIME column. + * Add a TIMESTAMP/DATETIME column to the table definition. + * + * If $defaultCurrent is true, sets the column default to the dialect's current timestamp expression. + * + * @param string $name Column name. + * @param bool $nullable Whether the column allows NULL. + * @param bool $defaultCurrent Whether to use the dialect's current timestamp as the column default. + * @return self The builder instance. */ public function timestamp(string $name, bool $nullable = true, bool $defaultCurrent = false): self { @@ -134,7 +177,14 @@ public function timestamp(string $name, bool $nullable = true, bool $defaultCurr } /** - * Adds a DATE column. + * Add a DATE column to the table definition. + * + * If `$defaultCurrent` is true, the column's default value will be the current date as provided by the configured SQL dialect. + * + * @param string $name The column name. + * @param bool $nullable Whether the column allows NULL. + * @param bool $defaultCurrent If true, set the column default to the dialect-specific current date. + * @return self The TableBuilder instance for method chaining. */ public function date(string $name, bool $nullable = true, bool $defaultCurrent = false): self { @@ -143,7 +193,13 @@ public function date(string $name, bool $nullable = true, bool $defaultCurrent = } /** - * Adds a CHAR column. + * Adds a CHAR column definition to the table. + * + * @param string $name Column name. + * @param int $length Maximum character length for the CHAR column. + * @param bool $nullable Whether the column allows NULL. + * @param string|null $default Default string value for the column; if provided it will be used as the SQL default (wrapped in single quotes). + * @return self The builder instance for method chaining. */ public function char(string $name, int $length, bool $nullable = true, ?string $default = null): self { @@ -152,7 +208,10 @@ public function char(string $name, int $length, bool $nullable = true, ?string $ } /** - * Adds an auto-increment primary key column. + * Add an auto-incrementing primary key column to the table definition. + * + * @param string $name Name of the column; defaults to 'id'. + * @return self The current builder instance for method chaining. */ public function autoIncrement(string $name = 'id'): self { @@ -166,9 +225,10 @@ public function autoIncrement(string $name = 'id'): self } /** - * Sets the primary key column(s). + * Set the table primary key to the given column or columns. * - * @param string|string[] $columns + * @param string|string[] $columns Column name or array of column names for the primary key. + * @return self The builder instance for chaining. */ public function primaryKey(string|array $columns): self { @@ -177,9 +237,11 @@ public function primaryKey(string|array $columns): self } /** - * Adds an index. + * Register a non-unique index with the given name on the specified column(s). * - * @param string|string[] $columns + * @param string $name The index name. + * @param string|string[] $columns Column name or array of column names to include in the index. + * @return self The builder instance for chaining. */ public function index(string $name, string|array $columns): self { @@ -191,9 +253,11 @@ public function index(string $name, string|array $columns): self } /** - * Adds a unique index. + * Register a unique index with the given name on specified column(s). * - * @param string|string[] $columns + * @param string $name The index name. + * @param string|string[] $columns Column name or list of column names to include in the index. + * @return self The current builder instance. */ public function uniqueIndex(string $name, string|array $columns): self { @@ -205,7 +269,13 @@ public function uniqueIndex(string $name, string|array $columns): self } /** - * Builds the CREATE TABLE statement. + * Constructs the CREATE TABLE SQL statement for the configured table. + * + * Includes column definitions (respecting NULL/NOT NULL and DEFAULT), an optional PRIMARY KEY clause, + * and inline indexes. Delegates dialect-specific pieces such as auto-increment column rendering, + * table prefixing (including IF NOT EXISTS) and table suffix to the configured dialect. + * + * @return string The complete CREATE TABLE SQL statement. */ public function build(): string { @@ -254,9 +324,9 @@ public function build(): string } /** - * Returns separate CREATE INDEX statements. + * Build separate CREATE INDEX statements for all defined indexes. * - * @return string[] + * @return string[] Array of CREATE INDEX SQL statements, one entry per defined index. */ public function buildIndexStatements(): array { @@ -272,6 +342,16 @@ public function buildIndexStatements(): array return $statements; } + /** + * Store a column definition for later SQL generation. + * + * @param string $name The column name. + * @param string $type The SQL column type or type expression (e.g., "VARCHAR(255)"). + * @param bool $nullable Whether the column allows NULL. + * @param string|null $default The default value or SQL expression to use for the column, or null if none. + * @param string|null $extra Optional additional column attributes (e.g., "AUTO_INCREMENT"). + * @return self The builder instance. + */ private function addColumn( string $name, string $type, @@ -287,4 +367,4 @@ private function addColumn( ]; return $this; } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Alpha.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Alpha.php index c5622ec930..a78d4f0488 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Alpha.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Alpha.php @@ -24,16 +24,39 @@ readonly class Migration320Alpha extends AbstractMigration { + /** + * Migration version identifier. + * + * @return string The version identifier for this migration (e.g., "3.2.0-alpha"). + */ public function getVersion(): string { return '3.2.0-alpha'; } + /** + * Provide a short human-readable summary of the migration's purpose. + * + * @return string A concise description listing the migration's changes: Microsoft Entra ID support, two-factor authentication support, creation of a backup table, Google ReCAPTCHA v2 support, and removal of section tables. + */ public function getDescription(): string { return 'Microsoft Entra ID support, 2FA support, backup table, Google ReCAPTCHA v2, remove section tables'; } + /** + * Apply schema and configuration changes required by the 3.2.0-alpha migration. + * + * Performs database schema updates and configuration additions/remappings, including: + * - enable Microsoft Entra ID sign-in and 2FA support flag, + * - add OAuth-related columns to faquser and 2FA columns to faquserdata, + * - create the faqbackup table, + * - migrate faqdata to InnoDB on MySQL, + * - add new main.* and Google ReCAPTCHA v2 configuration entries, + * - drop obsolete section-related tables. + * + * @param OperationRecorder $recorder Recorder used to schedule SQL and configuration operations for the migration. + */ public function up(OperationRecorder $recorder): void { // Microsoft Entra ID support and 2FA-support @@ -115,4 +138,4 @@ public function up(OperationRecorder $recorder): void $recorder->addSql(sprintf('DROP TABLE %sfaqsection_group', $this->tablePrefix), 'Drop faqsection_group table'); $recorder->addSql(sprintf('DROP TABLE %sfaqsection_news', $this->tablePrefix), 'Drop faqsection_news table'); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Beta.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Beta.php index 7d948295a2..afa59389df 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Beta.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Beta.php @@ -24,21 +24,45 @@ readonly class Migration320Beta extends AbstractMigration { + /** + * Retrieve the migration version identifier. + * + * @return string The migration version "3.2.0-beta". + */ public function getVersion(): string { return '3.2.0-beta'; } + /** + * Migration versions required before applying this migration. + * + * @return string[] Array of migration version identifiers required as dependencies. + */ public function getDependencies(): array { return ['3.2.0-alpha']; } + /** + * Human-readable summary of the migration's purpose and changes. + * + * @return string A short description of the migration's intent and applied changes. + */ public function getDescription(): string { return 'SMTP TLS config, remove link verification, config value as TEXT column'; } + /** + * Apply migration changes for version 3.2.0-beta. + * + * Adds the mail.remoteSMTPDisableTLSPeerVerification config, removes main.enableLinkVerification, + * drops link verification columns from faqdata and faqdata_revisions, and converts + * faqconfig.config_value to a TEXT column using database-specific SQL. + * + * @param OperationRecorder $recorder Recorder used to record configuration changes and SQL statements. + */ public function up(OperationRecorder $recorder): void { $recorder->addConfig('mail.remoteSMTPDisableTLSPeerVerification', false); @@ -101,4 +125,4 @@ public function up(OperationRecorder $recorder): void ); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Beta2.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Beta2.php index 3069e49e03..a0c1fe5e73 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Beta2.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320Beta2.php @@ -24,24 +24,47 @@ readonly class Migration320Beta2 extends AbstractMigration { + /** + * Migration version identifier. + * + * @return string The migration version identifier '3.2.0-beta.2'. + */ public function getVersion(): string { return '3.2.0-beta.2'; } + /** + * Migration version identifiers required before applying this migration. + * + * @return string[] An array of version identifiers this migration depends on. + */ public function getDependencies(): array { return ['3.2.0-beta']; } + /** + * Provide a short human-readable description of this migration. + * + * @return string The migration description: "HTML support for contact information, rename contactInformations". + */ public function getDescription(): string { return 'HTML support for contact information, rename contactInformations'; } + /** + * Apply migration changes for version 3.2.0-beta.2. + * + * Adds the configuration key `main.contactInformationHTML` with a default value of `false` + * and renames the configuration key `main.contactInformations` to `main.contactInformation`. + * + * @param OperationRecorder $recorder Recorder used to record configuration changes performed by the migration. + */ public function up(OperationRecorder $recorder): void { $recorder->addConfig('main.contactInformationHTML', false); $recorder->renameConfig('main.contactInformations', 'main.contactInformation'); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320RC.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320RC.php index 7352d62cdd..1337ca4c8d 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320RC.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration320RC.php @@ -24,23 +24,45 @@ readonly class Migration320RC extends AbstractMigration { + /** + * Provide the migration version identifier. + * + * @return string The migration version string "3.2.0-RC". + */ public function getVersion(): string { return '3.2.0-RC'; } + /** + * List of migration versions required to run before this migration. + * + * @return string[] An array of version identifiers this migration depends on. + */ public function getDependencies(): array { return ['3.2.0-beta.2']; } + /** + * Provide a short human-readable description of this migration. + * + * @return string The migration description: 'Add mail address in export config option'. + */ public function getDescription(): string { return 'Add mail address in export config option'; } + /** + * Record adding the `spam.mailAddressInExport` configuration option and enable it. + * + * Uses the provided operation recorder to register the config key `spam.mailAddressInExport` with value `true`. + * + * @param OperationRecorder $recorder Recorder used to record migration operations. + */ public function up(OperationRecorder $recorder): void { $recorder->addConfig('spam.mailAddressInExport', true); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration323.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration323.php index 8900f0d804..fe63b77f4f 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration323.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration323.php @@ -24,21 +24,43 @@ readonly class Migration323 extends AbstractMigration { + /** + * Get the migration version identifier. + * + * @return string The migration version identifier `3.2.3`. + */ public function getVersion(): string { return '3.2.3'; } + /** + * Migration versions that must be applied before this migration. + * + * @return string[] Array of dependent migration version identifiers. + */ public function getDependencies(): array { return ['3.2.0-RC']; } + /** + * Short description of the migration: increase IP column size for IPv6 support. + * + * @return string The migration description. + */ public function getDescription(): string { return 'Increase IP column size for IPv6 support'; } + /** + * Apply schema changes to increase the faquser.ip column size to 64 characters for the active database platform. + * + * Queues database-specific SQL operations (ALTER TABLE or a table rebuild on SQLite) via the provided OperationRecorder. + * + * @param OperationRecorder $recorder Recorder used to enqueue the migration SQL statements and their descriptions. + */ public function up(OperationRecorder $recorder): void { if ($this->isMySql()) { @@ -94,4 +116,4 @@ public function up(OperationRecorder $recorder): void ); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha.php index 0ad282beb5..41adbc0c12 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha.php @@ -25,21 +25,45 @@ readonly class Migration400Alpha extends AbstractMigration { + /** + * Provide the migration's version identifier. + * + * @return string The migration version string (e.g., "4.0.0-alpha"). + */ public function getVersion(): string { return '4.0.0-alpha'; } + /** + * Versions that must be applied before this migration. + * + * @return string[] Array of migration version strings this migration depends on. + */ public function getDependencies(): array { return ['3.2.3']; } + /** + * Short description of the migration: new file layout, bookmarks, sticky order, online update config, and removal of social networks. + * + * @return string A short human-readable description of the migration. + */ public function getDescription(): string { return 'New file layout, bookmarks, sticky order, online update config, remove social networks'; } + /** + * Applies the migration steps required to upgrade the schema and configuration to version 4.0.0-alpha. + * + * Performs file and directory migrations, updates configuration keys, creates and alters database tables + * (bookmarks, sticky ordering, category ordering), removes deprecated settings and tables, and adds + * values used by the new release. + * + * @param OperationRecorder $recorder Recorder used to record filesystem operations, SQL statements, and configuration changes performed by the migration. + */ public function up(OperationRecorder $recorder): void { // Copy database configuration @@ -202,4 +226,4 @@ public function up(OperationRecorder $recorder): void ); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha2.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha2.php index f8e9589de7..3ab5c96682 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha2.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha2.php @@ -24,21 +24,45 @@ readonly class Migration400Alpha2 extends AbstractMigration { + /** + * Migration version identifier for this migration. + * + * @return string The semantic version string "4.0.0-alpha.2". + */ public function getVersion(): string { return '4.0.0-alpha.2'; } + /** + * List migration version identifiers that must be applied before this migration. + * + * @return string[] Array of migration version strings required to run before this migration. + */ public function getDependencies(): array { return ['4.0.0-alpha']; } + /** + * Short human-readable description of this migration. + * + * @return string A brief description of the migration's purpose: "Forms table and forms_edit permission". + */ public function getDescription(): string { return 'Forms table and forms_edit permission'; } + /** + * Applies the migration by removing a deprecated configuration key, granting the forms edit permission, and creating the forms table. + * + * The migration deletes the `main.optionalMailAddress` config key, grants the `forms_edit` permission, and creates a table named `{tablePrefix}faqforms` + * with the columns: `form_id`, `input_id`, `input_type`, `input_label`, `input_active`, `input_required`, and `input_lang`. + * The exact DDL for the table depends on the database dialect (MySQL, SQL Server, or generic). + * + * Note: insertion of form inputs is handled separately by the Forms class. + */ public function up(OperationRecorder $recorder): void { $recorder->deleteConfig('main.optionalMailAddress'); @@ -88,4 +112,4 @@ public function up(OperationRecorder $recorder): void // Note: The form inputs insertion is handled separately through the Forms class // because it requires complex business logic that varies by installation } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha3.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha3.php index 7a3880e021..01bbed5e98 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha3.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Alpha3.php @@ -24,21 +24,41 @@ readonly class Migration400Alpha3 extends AbstractMigration { + /** + * Migration version identifier for this migration. + * + * @return string The migration version (e.g., '4.0.0-alpha.3'). + */ public function getVersion(): string { return '4.0.0-alpha.3'; } + /** + * List migration version identifiers that this migration depends on. + * + * @return string[] Array of migration version strings that must be applied before this migration. + */ public function getDependencies(): array { return ['4.0.0-alpha.2']; } + /** + * Provides a short human-readable description of this migration. + * + * @return string A brief summary of the migration's changes: SEO table, media hosts config, layout settings, and rich snippets. + */ public function getDescription(): string { return 'SEO table, media hosts config, layout settings, rich snippets'; } + /** + * Perform the migration for 4.0.0-alpha.3: create the SEO table (using DB-specific DDL) and apply configuration changes. + * + * @param OperationRecorder $recorder Recorder used to register SQL statements and configuration updates performed by this migration. + */ public function up(OperationRecorder $recorder): void { // Create SEO table @@ -126,4 +146,4 @@ public function up(OperationRecorder $recorder): void $recorder->renameConfig('main.enableCookieConsent', 'layout.enableCookieConsent'); $recorder->renameConfig('main.contactInformationHTML', 'layout.contactInformationHTML'); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Beta2.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Beta2.php index feef59cc74..d2f043f155 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Beta2.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration400Beta2.php @@ -24,21 +24,44 @@ readonly class Migration400Beta2 extends AbstractMigration { + /** + * Provide the migration version identifier for this migration. + * + * @return string The migration version identifier, '4.0.0-beta.2'. + */ public function getVersion(): string { return '4.0.0-beta.2'; } + /** + * List migration versions that must be applied before this migration. + * + * @return string[] An array of migration version identifiers required as dependencies. + */ public function getDependencies(): array { return ['4.0.0-alpha.3']; } + /** + * Provide a short human-readable description of this migration. + * + * @return string Short description of the migration. + */ public function getDescription(): string { return 'WebAuthn support'; } + /** + * Apply the migration: enable WebAuthn support configuration and add the `webauthnkeys` column to the `faquser` table. + * + * Registers the configuration key `security.enableWebAuthnSupport` with a default of `false` and records the SQL + * to add a nullable TEXT `webauthnkeys` column to the `faquser` table (uses a SQLite-specific SQL/message when applicable). + * + * @param OperationRecorder $recorder The recorder used to register configuration and SQL operations for this migration. + */ public function up(OperationRecorder $recorder): void { // WebAuthn support @@ -56,4 +79,4 @@ public function up(OperationRecorder $recorder): void ); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration405.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration405.php index a5285d9943..dd4d212db6 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration405.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration405.php @@ -24,21 +24,46 @@ readonly class Migration405 extends AbstractMigration { + /** + * Gets the migration target version. + * + * @return string The migration version identifier (e.g., "4.0.5"). + */ public function getVersion(): string { return '4.0.5'; } + /** + * Migration versions that must be applied before this migration. + * + * @return string[] Required migration version strings. + */ public function getDependencies(): array { return ['4.0.0-beta.2']; } + /** + * Short human-readable description of the migration: remove old section permissions and increase the faqforms.input_label column size. + * + * @return string A brief description of the migration. + */ public function getDescription(): string { return 'Remove old section permissions, increase forms input_label column'; } + /** + * Remove legacy section permissions and increase the size of the faqforms.input_label column. + * + * Executes SQL statements to delete the old permissions (view_sections, add_section, edit_section, + * delete_section) from the faqright table and updates the faqforms.input_label column to support + * up to 500 characters using database-specific operations (ALTER for MySQL/PostgreSQL/SQL Server, + * table rebuild for SQLite). + * + * @param OperationRecorder $recorder Recorder used to collect and execute migration SQL operations. + */ public function up(OperationRecorder $recorder): void { // Delete old permissions @@ -117,4 +142,4 @@ public function up(OperationRecorder $recorder): void ); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration407.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration407.php index 077d10300a..db3af860c9 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration407.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration407.php @@ -24,21 +24,45 @@ readonly class Migration407 extends AbstractMigration { + /** + * Get the migration version identifier. + * + * @return string The migration version identifier (e.g., "4.0.7"). + */ public function getVersion(): string { return '4.0.7'; } + /** + * List migration version identifiers required before applying this migration. + * + * @return string[] Migration version identifiers that must be applied prior to this migration. + */ public function getDependencies(): array { return ['4.0.5']; } + / ** + * Migration description stating this migration fixes language codes for fr_CA and pt_BR. + * + * @return string The human-readable description: "Fix language codes for fr_CA and pt_BR". + */ public function getDescription(): string { return 'Fix language codes for fr_CA and pt_BR'; } + /** + * Enqueue SQL operations to normalize legacy language codes and update related config references. + * + * This method adds SQL statements to replace "fr-ca" with "fr_ca" and "pt-br" with "pt_br" + * for language columns across multiple tables, and updates the corresponding language file + * references in the faqconfig table. + * + * @param OperationRecorder $recorder Recorder used to collect SQL statements and their descriptions. + */ public function up(OperationRecorder $recorder): void { // Define the table/column mappings for language code fixes @@ -104,4 +128,4 @@ public function up(OperationRecorder $recorder): void 'Fix pt-br language file config reference', ); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration409.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration409.php index fefd6498d9..7ab5a7a52a 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration409.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration409.php @@ -24,21 +24,47 @@ readonly class Migration409 extends AbstractMigration { + /** + * Get the migration version identifier. + * + * @return string The migration version string, e.g. "4.0.9". + */ public function getVersion(): string { return '4.0.9'; } + /** + * List migrations that must be applied before this migration. + * + * @return string[] Array of migration version strings this migration depends on (e.g., ['4.0.7']). + */ public function getDependencies(): array { return ['4.0.7']; } + /** + * Migration description indicating this migration sets up a PostgreSQL sequence for the faqseo table. + * + * @return string The description text: "PostgreSQL sequence for faqseo table". + */ public function getDescription(): string { return 'PostgreSQL sequence for faqseo table'; } + /** + * Prepare and register PostgreSQL SQL operations to create and initialize the faqseo id sequence. + * + * When the connection is PostgreSQL, registers SQL statements to: + * - create the faqseo_id_seq sequence, + * - set faqseo.id default to nextval(...) from that sequence, + * - set the sequence current value to the table's MAX(id), + * - mark faqseo.id as NOT NULL. + * + * @param OperationRecorder $recorder Recorder used to collect SQL operations for execution. + */ public function up(OperationRecorder $recorder): void { // PostgreSQL-only migration for faqseo sequence @@ -67,4 +93,4 @@ public function up(OperationRecorder $recorder): void ); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha.php index ed06fa9a58..df94ad6a6e 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha.php @@ -24,21 +24,41 @@ readonly class Migration410Alpha extends AbstractMigration { + /** + * Migration version identifier for this migration. + * + * @return string The migration version identifier: '4.1.0-alpha'. + */ public function getVersion(): string { return '4.1.0-alpha'; } + /** + * List migration version dependencies that must be applied before this migration. + * + * @return string[] Array of version strings this migration depends on. + */ public function getDependencies(): array { return ['4.0.9']; } + /** + * Provide a human-readable description of this migration. + * + * @return string The migration description: 'robots.txt content configuration for AI crawlers'. + */ public function getDescription(): string { return 'robots.txt content configuration for AI crawlers'; } + /** + * Configure default robots.txt content targeting various AI crawlers and general rules. + * + * Stores a predefined robots.txt text under the configuration key 'seo.contentRobotsText' using the supplied OperationRecorder. + */ public function up(OperationRecorder $recorder): void { $robotsText = <<addConfig('seo.contentRobotsText', $robotsText); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha2.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha2.php index 1224276008..e44501a031 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha2.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha2.php @@ -25,21 +25,41 @@ readonly class Migration410Alpha2 extends AbstractMigration { + /** + * Provide the migration's version identifier. + * + * @return string The migration version identifier (e.g. "4.1.0-alpha.2"). + */ public function getVersion(): string { return '4.1.0-alpha.2'; } + /** + * Migration versions that must be applied before this migration. + * + * @return string[] Array of version identifiers this migration depends on. + */ public function getDependencies(): array { return ['4.1.0-alpha']; } + /** + * Provide a short human-readable description of this migration. + * + * @return string A concise summary of the migration's changes. + */ public function getDescription(): string { return 'Admin session timeout counter, OpenSearch config, FAQ translate permission'; } + /** + * Apply configuration and permission changes introduced by this migration. + * + * @param OperationRecorder $recorder Recorder used to record configuration entries and permission grants to apply during the migration. + */ public function up(OperationRecorder $recorder): void { $recorder->addConfig('security.enableAdminSessionTimeoutCounter', true); @@ -48,4 +68,4 @@ public function up(OperationRecorder $recorder): void // Add new permission to translate FAQs $recorder->grantPermission(PermissionType::FAQ_TRANSLATE->value, 'Right to translate FAQs'); } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha3.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha3.php index f84317472b..ca03d3dbf1 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha3.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration410Alpha3.php @@ -24,21 +24,44 @@ readonly class Migration410Alpha3 extends AbstractMigration { + /** + * Migration version identifier for this migration. + * + * @return string The migration version string (e.g. "4.1.0-alpha.3"). + */ public function getVersion(): string { return '4.1.0-alpha.3'; } + /** + * List migration versions that this migration depends on. + * + * @return string[] An array of migration version strings that must be applied before this migration. + */ public function getDependencies(): array { return ['4.1.0-alpha.2']; } + /** + * Provide a short, human-readable description of this migration. + * + * @return string A short, human-readable description of the migration's changes. + */ public function getDescription(): string { return 'LLMs.txt config, LDAP group integration, search optimization indexes'; } + /** + * Apply migration for 4.1.0-alpha.3: add configuration entries and schedule faqsearches indexes. + * + * Records the LLMs.txt content and LDAP/search configuration keys, and records SQL statements to + * create performance indexes on the faqsearches table using database-appropriate syntax. + * + * @param OperationRecorder $recorder Recorder used to persist configuration entries and SQL statements. + */ public function up(OperationRecorder $recorder): void { $llmsText = @@ -115,4 +138,4 @@ public function up(OperationRecorder $recorder): void ); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration420Alpha.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration420Alpha.php index 603dc16c41..05af16d6f4 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration420Alpha.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration420Alpha.php @@ -25,21 +25,45 @@ readonly class Migration420Alpha extends AbstractMigration { + /** + * Retrieve the migration version identifier for this migration. + * + * @return string The migration version: "4.2.0-alpha". + */ public function getVersion(): string { return '4.2.0-alpha'; } + /** + * Migration versions required to be applied before this migration. + * + * @return string[] An array of migration version strings that must be applied first. + */ public function getDependencies(): array { return ['4.1.0-alpha.3']; } + /** + * Provide a short human-readable description of this migration's changes. + * + * @return string A concise description of the migration's purpose and affected areas. + */ public function getDescription(): string { return 'Admin log hash columns, custom pages, chat messages, translation config'; } + /** + * Applies the 4.2.0-alpha migration: schema changes, permissions, and configuration additions. + * + * Adds hash columns and an index to faqadminlog; creates the faqcustompages table and its slug index; + * grants page management permissions; inserts new configuration keys (URLs, API filters, translation + * provider settings, and comment editor toggle); and creates the faqchat_messages table with indexes. + * + * @param OperationRecorder $recorder Recorder used to register SQL statements, permission grants, and config entries for execution. + */ public function up(OperationRecorder $recorder): void { // Add hash columns to faqadminlog @@ -357,4 +381,4 @@ public function up(OperationRecorder $recorder): void ); } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Update.php b/phpmyfaq/src/phpMyFAQ/Setup/Update.php index a23c8beb60..117afcde95 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Update.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Update.php @@ -84,6 +84,16 @@ class Update extends AbstractSetup } private MigrationExecutor $migrationExecutor; + /** + * Create an Update instance and initialize migration components. + * + * Initializes the Update setup with the provided System and Configuration objects + * and constructs the MigrationRegistry, MigrationTracker, and MigrationExecutor + * used during the update process. + * + * @param System $system Application system utilities (environment and version access). + * @param Configuration $configuration Configuration and database access used by migrations. + */ public function __construct( protected System $system, private readonly Configuration $configuration, @@ -195,8 +205,15 @@ public function checkInitialRewriteBasePath(Request $request): bool } /** - * @throws Exception - * @throws \Exception + * Run pending migrations for the configured version and perform related post-migration work. + * + * When dry-run mode is enabled, migrations are not applied and SQL statements from pending + * migrations and legacy queries are collected instead of executed. When not in dry-run mode, + * migration tracking is ensured, post-migration tasks are run, tables are optimized, legacy + * queries are executed, and the stored version values are updated. + * + * @return bool `true` if all migrations succeeded, `false` otherwise. + * @throws Exception If migration execution or any post-migration step fails. */ public function applyUpdates(): bool { @@ -238,7 +255,9 @@ public function applyUpdates(): bool } /** - * Returns true if all migrations succeeded. + * Determine whether every recorded migration result indicates success. + * + * @return bool `true` if all migrations succeeded, `false` otherwise. */ private function allMigrationsSucceeded(): bool { @@ -251,10 +270,10 @@ private function allMigrationsSucceeded(): bool } /** - * Collects SQL queries from migrations for dry-run backward compatibility. - * - * @param array $migrations - */ + * Collect SQL statements produced by the given migrations and append them to the object's dry-run query list. + * + * @param array $migrations Migrations to inspect for SQL operations. + */ private function collectDryRunQueries(array $migrations): void { $report = $this->migrationExecutor->generateDryRunReport($migrations); @@ -269,7 +288,11 @@ private function collectDryRunQueries(array $migrations): void } /** - * Run any post-migration tasks that can't be handled by the migration system. + * Perform post-migration maintenance steps that are not implemented as migrations. + * + * Executes version-gated tasks: + * - Migrates admin log entry hashes when updating from a version earlier than 4.2.0-alpha. + * - Inserts form input definitions when updating from a version earlier than 4.0.0-alpha.2. */ private function runPostMigrationTasks(): void { @@ -285,7 +308,9 @@ private function runPostMigrationTasks(): void } /** - * Insert form inputs (special handling required due to complex business logic). + * Generate INSERT queries for installer-defined form inputs and append them to the internal query queue. + * + * If generation fails (for example because the inputs already exist), any thrown exception is caught and ignored. */ private function insertFormInputs(): void { @@ -300,6 +325,12 @@ private function insertFormInputs(): void } } + /** + * Prepare appropriate database maintenance queries for the current DB type and append them to the internal query queue. + * + * For MySQL (`mysqli`) this adds an `OPTIMIZE TABLE
` statement for each table in the configured database. + * For PostgreSQL (`pgsql`) this adds a `VACUUM ANALYZE;` statement. + */ public function optimizeTables(): void { switch (Database::getType()) { @@ -317,9 +348,9 @@ public function optimizeTables(): void } /** - * Returns detailed dry-run results including all operation types. + * Generate a detailed dry-run report for pending migrations against the current target version. * - * @return array + * @return array Associative report keyed by migration identifiers containing operation entries (including SQL and non-SQL operation types) and their metadata. */ public function getDryRunResults(): array { @@ -328,7 +359,9 @@ public function getDryRunResults(): array } /** - * Returns the formatted dry-run report as a string. + * Produce a human-readable formatted dry-run report for migrations pending for the current target version. + * + * @return string The formatted dry-run report. */ public function getFormattedDryRunReport(): string { @@ -338,7 +371,12 @@ public function getFormattedDryRunReport(): string } /** - * @throws Exception + * Execute collected SQL queries or record them for a dry run. + * + * When dry-run mode is enabled, appends each query to the dry-run query list. + * When not in dry-run mode, executes each query against the configured database. + * + * @throws Exception If executing any query fails; the thrown exception contains the original error message. */ private function executeQueries(): void { @@ -411,4 +449,4 @@ private function migrateAdminLogHashes(): void } } } -} +} \ No newline at end of file diff --git a/phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php b/phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php index ce0ad40336..bea865ccd0 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/UpdateRunner.php @@ -40,6 +40,12 @@ public function __construct( ) { } + /** + * Executes the full update workflow by running each update task in sequence. + * + * @param Symfony\Component\Console\Style\SymfonyStyle $symfonyStyle Console style helper used for progress and messages. + * @return int `Command::SUCCESS` if all steps completed successfully, `Command::FAILURE` otherwise. + */ public function run(SymfonyStyle $symfonyStyle): int { $steps = [ @@ -64,7 +70,11 @@ public function run(SymfonyStyle $symfonyStyle): int } /** - * Runs the update in dry-run mode, showing what would be executed without making changes. + * Displays a dry-run migration report for a given source version, showing the actions that would be performed without applying any changes. + * + * @param SymfonyStyle $symfonyStyle Console IO helper used to render the report. + * @param string $fromVersion The starting version to simulate updates from. + * @return int Command::SUCCESS if the dry-run report was generated successfully, Command::FAILURE if an error occurred. */ public function runDryRun(SymfonyStyle $symfonyStyle, string $fromVersion): int { @@ -91,10 +101,26 @@ public function runDryRun(SymfonyStyle $symfonyStyle, string $fromVersion): int } /** - * Displays the dry-run report with all operations grouped by type. - * - * @param array $report - */ + * Render a human-readable dry-run update report grouped by operation type. + * + * Prints each migration's version and description, then lists operations grouped by type + * (e.g. `sql`, `config_add`, `config_delete`, `config_rename`, `config_update`, + * `file_copy`, `directory_copy`, `permission_grant`) with compact, formatted details. + * Finally prints a summary with total migrations, total operations, optional per-type + * counts, and a final dry-run warning. + * + * @param array $report { + * Report payload. + * + * @type array>>} $migrations + * Migrations keyed by version with description and a list of operations. + * @type array{ + * migrationCount:int, + * totalOperations:int, + * operationsByType?:array + * } $summary Summary counts for the report. + * } + */ private function displayDryRunReport(SymfonyStyle $symfonyStyle, array $report): void { if (empty($report['migrations'])) { @@ -220,6 +246,13 @@ private function displayDryRunReport(SymfonyStyle $symfonyStyle, array $report): $symfonyStyle->warning('This was a dry-run. No changes were made to the database.'); } + /** + * Normalize whitespace, trim, and truncate a string to a maximum length, appending an ellipsis if truncated. + * + * @param string $str The input string to normalize and truncate. + * @param int $maxLength The maximum allowed length of the returned string (including the ellipsis when applied). + * @return string The normalized string, shortened with '...' if its length exceeded $maxLength. + */ private function truncateString(string $str, int $maxLength): string { $str = preg_replace('/\s+/', ' ', trim($str)); @@ -229,6 +262,12 @@ private function truncateString(string $str, int $maxLength): string return $str; } + /** + * Format a value for human-readable display in dry-run and migration reports. + * + * @param mixed $value The value to format for display. + * @return string The formatted representation: `true`/`false` for booleans, quoted strings (strings longer than 40 characters are truncated to 37 characters plus `...`), `null` for null, or the string cast of other values. + */ private function formatValue(mixed $value): string { if (is_bool($value)) { @@ -246,6 +285,12 @@ private function formatValue(mixed $value): string return (string) $value; } + /** + * Shortens a filesystem path for display by removing the PMF root and truncating long paths. + * + * @param string $path The original filesystem path; if `PMF_ROOT_DIR` is defined the prefix will be removed. + * @return string The path with the PMF root stripped when applicable and truncated to at most 50 characters, prefixed with `...` if truncated. + */ private function shortenPath(string $path): string { if (defined('PMF_ROOT_DIR')) { @@ -259,6 +304,15 @@ private function shortenPath(string $path): string private string $version = ''; + /** + * Performs a pre-update health check: verifies maintenance mode and validates filesystem readiness. + * + * If maintenance mode is not enabled a warning is emitted. If filesystem validation fails an error + * is emitted and the method returns a failure status. + * + * @param SymfonyStyle $symfonyStyle Symfony console helper used to display warnings, errors and success messages. + * @return int `Command::SUCCESS` on successful health check, `Command::FAILURE` if the check fails. + */ private function taskHealthCheck(SymfonyStyle $symfonyStyle): int { $upgrade = new Upgrade($this->system, $this->configuration); @@ -401,6 +455,13 @@ private function taskInstallPackage(SymfonyStyle $symfonyStyle): int return Command::FAILURE; } + /** + * Applies pending database migrations, presents a summary of applied migrations, and updates maintenance mode. + * + * On success disables maintenance mode and displays migration results; on failure or exception displays error details. + * + * @return int `Command::SUCCESS` if migrations were applied successfully, `Command::FAILURE` otherwise. + */ private function taskUpdateDatabase(SymfonyStyle $symfonyStyle): int { $update = new Update($this->system, $this->configuration); @@ -433,10 +494,14 @@ private function taskUpdateDatabase(SymfonyStyle $symfonyStyle): int } /** - * Displays migration results in a formatted table. - * - * @param MigrationResult[] $results - */ + * Render migration results as a table or show a note when no migrations were applied. + * + * When $results is empty a brief note is printed. Otherwise a table is rendered + * with the columns: Version, Description, Operations, Time, and Status (`SUCCESS` or `FAILED`). + * + * @param Symfony\Component\Console\Style\SymfonyStyle $symfonyStyle The console I/O helper used to print output. + * @param MigrationResult[] $results Array of migration results to display. + */ private function displayMigrationResults(SymfonyStyle $symfonyStyle, array $results): void { if (empty($results)) { @@ -459,6 +524,11 @@ private function displayMigrationResults(SymfonyStyle $symfonyStyle, array $resu $symfonyStyle->table(['Version', 'Description', 'Operations', 'Time', 'Status'], $tableRows); } + /** + * Performs cleanup of temporary upgrade artifacts and reports success to the console. + * + * @return int `Command::SUCCESS` on successful cleanup. + */ private function taskCleanup(SymfonyStyle $symfonyStyle): int { $upgrade = new Upgrade($this->system, $this->configuration); @@ -486,4 +556,4 @@ private function withProgress(SymfonyStyle $symfonyStyle, callable $fn): bool return $result; } -} +} \ No newline at end of file