<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add client_id to remaining tables (join tables, provider tables, OAuth tables).
* Positions client_id as the second column (after id).
*/
final class Version20251022101000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add client_id to all remaining tables and position it after id column';
}
private function columnExists(string $table, string $column): bool
{
$result = $this->connection->fetchOne(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?",
[$table, $column]
);
return $result > 0;
}
private function tableExists(string $table): bool
{
$result = $this->connection->fetchOne(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?",
[$table]
);
return $result > 0;
}
private function foreignKeyExists(string $table, string $fkName): bool
{
$result = $this->connection->fetchOne(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND CONSTRAINT_NAME = ?
AND CONSTRAINT_TYPE = 'FOREIGN KEY'",
[$table, $fkName]
);
return $result > 0;
}
public function up(Schema $schema): void
{
// ============================================================
// Tables to update with client_id
// Excluding: client (is the client table itself),
// doctrine_migration_versions (system table)
// ============================================================
$tablesToUpdate = [
// Join tables
'cart_item_person',
'course_occurrence_speaker',
'email_history_entry_person',
'person_course_occurrence',
// Provider join tables
'course_provider',
'occurrence_provider',
'speaker_provider',
'venue_provider',
// Tags join table
'tags_person',
// OAuth tables
'oauth_client',
'oauth_auth_code',
'oauth2_access_token',
'oauth2_authorization_code',
'oauth2_refresh_token',
];
foreach ($tablesToUpdate as $table) {
// Skip if table doesn't exist
if (!$this->tableExists($table)) {
continue;
}
// Add client_id column if it doesn't exist
if (!$this->columnExists($table, 'client_id')) {
// Add column positioned after id
if ($this->columnExists($table, 'id')) {
$this->addSql("ALTER TABLE $table ADD client_id INT DEFAULT NULL AFTER id");
} else {
// If no id column, add as first column
$this->addSql("ALTER TABLE $table ADD client_id INT DEFAULT NULL FIRST");
}
}
}
// ============================================================
// Add Foreign Keys and Indexes
// ============================================================
$constraints = [
'cart_item_person' => 'FK_CIP19EB6921',
'course_occurrence_speaker' => 'FK_COS19EB6921',
'email_history_entry_person' => 'FK_EHEP19EB6921',
'person_course_occurrence' => 'FK_PCO19EB6921',
'course_provider' => 'FK_CP19EB6921',
'occurrence_provider' => 'FK_OP19EB6921',
'speaker_provider' => 'FK_SP19EB6921',
'venue_provider' => 'FK_VP19EB6921',
'tags_person' => 'FK_TP19EB6921',
'oauth_client' => 'FK_OC19EB6921',
'oauth_auth_code' => 'FK_OAC19EB6921',
'oauth2_access_token' => 'FK_O2AT19EB6921',
'oauth2_authorization_code' => 'FK_O2AC19EB6921',
'oauth2_refresh_token' => 'FK_O2RT19EB6921',
];
foreach ($constraints as $table => $fkName) {
// Skip if table doesn't exist
if (!$this->tableExists($table)) {
continue;
}
// Only add FK and index if column exists and FK doesn't exist yet
if ($this->columnExists($table, 'client_id') && !$this->foreignKeyExists($table, $fkName)) {
try {
$this->addSql("ALTER TABLE $table ADD CONSTRAINT $fkName FOREIGN KEY (client_id) REFERENCES client (id)");
$this->addSql("CREATE INDEX $fkName ON $table (client_id)");
} catch (\Exception $e) {
// Skip if there's an error (e.g., constraint already exists with different name)
}
}
}
// ============================================================
// Special case: search_index_entry - rename clientId to client_id
// ============================================================
if ($this->tableExists('search_index_entry')) {
if ($this->columnExists('search_index_entry', 'clientId') && !$this->columnExists('search_index_entry', 'client_id')) {
$this->addSql("ALTER TABLE search_index_entry CHANGE clientId client_id INT NOT NULL FIRST");
} elseif ($this->columnExists('search_index_entry', 'client_id')) {
// Position it first if already renamed
$this->addSql("ALTER TABLE search_index_entry MODIFY client_id INT NOT NULL FIRST");
}
}
}
public function down(Schema $schema): void
{
// Rollback: Remove client_id from tables
$tables = [
'cart_item_person', 'course_occurrence_speaker', 'email_history_entry_person',
'person_course_occurrence', 'course_provider', 'occurrence_provider',
'speaker_provider', 'venue_provider', 'tags_person', 'oauth_client',
'oauth_auth_code', 'oauth2_access_token', 'oauth2_authorization_code',
'oauth2_refresh_token'
];
$constraints = [
'cart_item_person' => 'FK_CIP19EB6921',
'course_occurrence_speaker' => 'FK_COS19EB6921',
'email_history_entry_person' => 'FK_EHEP19EB6921',
'person_course_occurrence' => 'FK_PCO19EB6921',
'course_provider' => 'FK_CP19EB6921',
'occurrence_provider' => 'FK_OP19EB6921',
'speaker_provider' => 'FK_SP19EB6921',
'venue_provider' => 'FK_VP19EB6921',
'tags_person' => 'FK_TP19EB6921',
'oauth_client' => 'FK_OC19EB6921',
'oauth_auth_code' => 'FK_OAC19EB6921',
'oauth2_access_token' => 'FK_O2AT19EB6921',
'oauth2_authorization_code' => 'FK_O2AC19EB6921',
'oauth2_refresh_token' => 'FK_O2RT19EB6921',
];
foreach ($tables as $table) {
if ($this->tableExists($table) && $this->columnExists($table, 'client_id')) {
$fkName = $constraints[$table];
// Drop FK if exists
if ($this->foreignKeyExists($table, $fkName)) {
$this->addSql("ALTER TABLE $table DROP FOREIGN KEY $fkName");
$this->addSql("DROP INDEX $fkName ON $table");
}
// Drop column
$this->addSql("ALTER TABLE $table DROP client_id");
}
}
// Reverse search_index_entry
if ($this->tableExists('search_index_entry') && $this->columnExists('search_index_entry', 'client_id')) {
$this->addSql("ALTER TABLE search_index_entry CHANGE client_id clientId INT NOT NULL FIRST");
}
}
}