Lines
92.72%
102 / 110
Methods
91.66%
11 / 12
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| table | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | ||
| addInteger | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 2 | ||
| addVarchar | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | ||
| addText | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| addBoolean | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 3 | ||
| addTimestamp | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | ||
| modifyColumn | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 1 | ||
| dropColumn | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 1 | ||
| build | 100.00% | 31 / 31 | 100.00% | 1 / 1 | 11 | ||
| buildCombined | 75.00% | 24 / 32 | 0.00% | 0 / 1 | 17.06 | ||
| addColumn | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 1 | ||
| 25 | class AlterTableBuilder | |
| 26 | { | |
| 27 | private string $tableName = ''; | |
| 28 | private DialectInterface $dialect; | |
| 29 | ||
| 30 | /** @var array<int, array{action: string, column: string, type: string|null, after: string|null, default: string|null, nullable: bool}> */ | |
| 31 | private array $alterations = []; | |
| 32 | ||
| 33 | public function __construct(?DialectInterface $dialect = null) | |
| 34 | { | |
| 35 | $this->dialect = $dialect ?? DialectFactory::create(); | |
| 36 | } | |
| 37 | ||
| 38 | /** | |
| 39 | * Sets the table name. | |
| 40 | */ | |
| 41 | public function table(string $name, bool $withPrefix = true): self | |
| 42 | { | |
| 43 | $this->tableName = $withPrefix ? Database::getTablePrefix() . $name : $name; | |
| 44 | return $this; | |
| 45 | } | |
| 46 | ||
| 47 | /** | |
| 48 | * Adds a new INTEGER column. | |
| 49 | */ | |
| 50 | public function addInteger(string $name, bool $nullable = true, ?int $default = null, ?string $after = null): self | |
| 51 | { | |
| 52 | return $this->addColumn( | |
| 53 | 'ADD', | |
| 54 | $name, | |
| 55 | $this->dialect->integer(), | |
| 56 | $nullable, | |
| 57 | $default !== null ? (string) $default : null, | |
| 58 | $after, | |
| 59 | ); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Adds a new VARCHAR column. | |
| 64 | */ | |
| 65 | public function addVarchar( | |
| 66 | string $name, | |
| 67 | int $length, | |
| 68 | bool $nullable = true, | |
| 69 | ?string $default = null, | |
| 70 | ?string $after = null, | |
| 71 | ): self { | |
| 72 | // Escape single quotes in default value per SQL string literal rules (replace ' with '') | |
| 73 | $defaultVal = $default !== null ? "'" . str_replace(search: "'", replace: "''", subject: $default) . "'" : null; | |
| 74 | return $this->addColumn('ADD', $name, $this->dialect->varchar($length), $nullable, $defaultVal, $after); | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Adds a new TEXT column. | |
| 79 | */ | |
| 80 | public function addText(string $name, bool $nullable = true, ?string $after = null): self | |
| 81 | { | |
| 82 | return $this->addColumn('ADD', $name, $this->dialect->text(), $nullable, null, $after); | |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * Adds a new BOOLEAN column. | |
| 87 | */ | |
| 88 | public function addBoolean(string $name, bool $nullable = true, ?bool $default = null, ?string $after = null): self | |
| 89 | { | |
| 90 | $defaultVal = null; | |
| 91 | if ($default !== null) { | |
| 92 | $defaultVal = $default ? '1' : '0'; | |
| 93 | } | |
| 94 | ||
| 95 | return $this->addColumn('ADD', $name, $this->dialect->boolean(), $nullable, $defaultVal, $after); | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Adds a new TIMESTAMP column. | |
| 100 | */ | |
| 101 | public function addTimestamp( | |
| 102 | string $name, | |
| 103 | bool $nullable = true, | |
| 104 | bool $defaultCurrent = false, | |
| 105 | ?string $after = null, | |
| 106 | ): self { | |
| 107 | $default = $defaultCurrent ? $this->dialect->currentTimestamp() : null; | |
| 108 | return $this->addColumn('ADD', $name, $this->dialect->timestamp(), $nullable, $default, $after); | |
| 109 | } | |
| 110 | ||
| 111 | /** | |
| 112 | * Modifies an existing column type. | |
| 113 | */ | |
| 114 | public function modifyColumn(string $name, string $type): self | |
| 115 | { | |
| 116 | $this->alterations[] = [ | |
| 117 | 'action' => 'MODIFY', | |
| 118 | 'column' => $name, | |
| 119 | 'type' => $type, | |
| 120 | 'after' => null, | |
| 121 | 'default' => null, | |
| 122 | 'nullable' => true, | |
| 123 | ]; | |
| 124 | return $this; | |
| 125 | } | |
| 126 | ||
| 127 | /** | |
| 128 | * Drops a column. | |
| 129 | */ | |
| 130 | public function dropColumn(string $name): self | |
| 131 | { | |
| 132 | $this->alterations[] = [ | |
| 133 | 'action' => 'DROP', | |
| 134 | 'column' => $name, | |
| 135 | 'type' => null, | |
| 136 | 'after' => null, | |
| 137 | 'default' => null, | |
| 138 | 'nullable' => true, | |
| 139 | ]; | |
| 140 | return $this; | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Builds the ALTER TABLE statement(s). | |
| 145 | * Returns an array of statements because some databases require separate statements for each alteration. | |
| 146 | * | |
| 147 | * @return string[] | |
| 148 | */ | |
| 149 | public function build(): array | |
| 150 | { | |
| 151 | // Validate that table() was called before building | |
| 152 | if ($this->tableName === '') { | |
| 153 | throw new \RuntimeException('Table name not set. Call table() before building ALTER TABLE statements.'); | |
| 154 | } | |
| 155 | ||
| 156 | $statements = []; | |
| 157 | ||
| 158 | foreach ($this->alterations as $alt) { | |
| 159 | switch ($alt['action']) { | |
| 160 | case 'ADD': | |
| 161 | $type = (string) $alt['type']; | |
| 162 | if (!$alt['nullable']) { | |
| 163 | $type .= ' NOT NULL'; | |
| 164 | } | |
| 165 | ||
| 166 | if ($alt['nullable'] && $alt['default'] === null) { | |
| 167 | $type .= ' NULL'; | |
| 168 | } | |
| 169 | ||
| 170 | if ($alt['default'] !== null) { | |
| 171 | $type .= ' DEFAULT ' . $alt['default']; | |
| 172 | } | |
| 173 | $statements[] = $this->dialect->addColumn( | |
| 174 | $this->tableName, | |
| 175 | $alt['column'], | |
| 176 | $type, | |
| 177 | $this->dialect->supportsColumnPositioning() ? $alt['after'] : null, | |
| 178 | ); | |
| 179 | break; | |
| 180 | ||
| 181 | case 'MODIFY': | |
| 182 | $statements[] = $this->dialect->modifyColumn( | |
| 183 | $this->tableName, | |
| 184 | $alt['column'], | |
| 185 | (string) $alt['type'], | |
| 186 | ); | |
| 187 | break; | |
| 188 | ||
| 189 | case 'DROP': | |
| 190 | $statements[] = "ALTER TABLE {$this->tableName} DROP COLUMN {$alt['column']}"; | |
| 191 | break; | |
| 192 | } | |
| 193 | } | |
| 194 | ||
| 195 | return $statements; | |
| 196 | } | |
| 197 | ||
| 198 | /** | |
| 199 | * Builds a single ALTER TABLE statement combining all alterations (MySQL only). | |
| 200 | */ | |
| 201 | public function buildCombined(): string | |
| 202 | { | |
| 203 | if (!$this->dialect->supportsCombinedAlter()) { | |
| 204 | throw new LogicException(sprintf( | |
| 205 | 'Combined ALTER TABLE is only supported on MySQL. Current dialect: %s. Use build() instead.', | |
| 206 | $this->dialect->getType(), | |
| 207 | )); | |
| 208 | } | |
| 209 | ||
| 210 | // Validate that table() was called before building | |
| 211 | if ($this->tableName === '') { | |
| 212 | throw new \RuntimeException('Table name not set. Call table() before building ALTER TABLE statements.'); | |
| 213 | } | |
| 214 | ||
| 215 | if ($this->alterations === []) { | |
| 216 | throw new LogicException('No alterations defined for combined ALTER TABLE statement.'); | |
| 217 | } | |
| 218 | ||
| 219 | $parts = []; | |
| 220 | ||
| 221 | foreach ($this->alterations as $alt) { | |
| 222 | switch ($alt['action']) { | |
| 223 | case 'ADD': | |
| 224 | $type = (string) $alt['type']; | |
| 225 | if (!$alt['nullable']) { | |
| 226 | $type .= ' NOT NULL'; | |
| 227 | } | |
| 228 | ||
| 229 | if ($alt['nullable'] && $alt['default'] === null) { | |
| 230 | $type .= ' NULL'; | |
| 231 | } | |
| 232 | ||
| 233 | if ($alt['default'] !== null) { | |
| 234 | $type .= ' DEFAULT ' . $alt['default']; | |
| 235 | } | |
| 236 | $part = "ADD COLUMN {$alt['column']} {$type}"; | |
| 237 | if ($this->dialect->supportsColumnPositioning() && $alt['after'] !== null) { | |
| 238 | $part .= " AFTER {$alt['after']}"; | |
| 239 | } | |
| 240 | $parts[] = $part; | |
| 241 | break; | |
| 242 | ||
| 243 | case 'MODIFY': | |
| 244 | $parts[] = "MODIFY {$alt['column']} {$alt['type']}"; | |
| 245 | break; | |
| 246 | ||
| 247 | case 'DROP': | |
| 248 | $parts[] = "DROP COLUMN {$alt['column']}"; | |
| 249 | break; | |
| 250 | } | |
| 251 | } | |
| 252 | ||
| 253 | return "ALTER TABLE {$this->tableName} " . implode(', ', $parts); | |
| 254 | } | |
| 255 | ||
| 256 | private function addColumn( | |
| 257 | string $action, | |
| 258 | string $name, | |
| 259 | string $type, | |
| 260 | bool $nullable, | |
| 261 | ?string $default, | |
| 262 | ?string $after, | |
| 263 | ): self { | |
| 264 | $this->alterations[] = [ | |
| 265 | 'action' => $action, | |
| 266 | 'column' => $name, | |
| 267 | 'type' => $type, | |
| 268 | 'after' => $after, | |
| 269 | 'default' => $default, | |
| 270 | 'nullable' => $nullable, | |
| 271 | ]; | |
| 272 | return $this; | |
| 273 | } | |
| 274 | } |