1<?php 2 3namespace Doctrine\DBAL\Schema; 4 5use Doctrine\DBAL\Platforms\AbstractPlatform; 6use Doctrine\DBAL\Schema\Visitor\CreateSchemaSqlCollector; 7use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector; 8use Doctrine\DBAL\Schema\Visitor\NamespaceVisitor; 9use Doctrine\DBAL\Schema\Visitor\Visitor; 10 11use function array_keys; 12use function strpos; 13use function strtolower; 14 15/** 16 * Object representation of a database schema. 17 * 18 * Different vendors have very inconsistent naming with regard to the concept 19 * of a "schema". Doctrine understands a schema as the entity that conceptually 20 * wraps a set of database objects such as tables, sequences, indexes and 21 * foreign keys that belong to each other into a namespace. A Doctrine Schema 22 * has nothing to do with the "SCHEMA" defined as in PostgreSQL, it is more 23 * related to the concept of "DATABASE" that exists in MySQL and PostgreSQL. 24 * 25 * Every asset in the doctrine schema has a name. A name consists of either a 26 * namespace.local name pair or just a local unqualified name. 27 * 28 * The abstraction layer that covers a PostgreSQL schema is the namespace of an 29 * database object (asset). A schema can have a name, which will be used as 30 * default namespace for the unqualified database objects that are created in 31 * the schema. 32 * 33 * In the case of MySQL where cross-database queries are allowed this leads to 34 * databases being "misinterpreted" as namespaces. This is intentional, however 35 * the CREATE/DROP SQL visitors will just filter this queries and do not 36 * execute them. Only the queries for the currently connected database are 37 * executed. 38 */ 39class Schema extends AbstractAsset 40{ 41 /** 42 * The namespaces in this schema. 43 * 44 * @var string[] 45 */ 46 private $namespaces = []; 47 48 /** @var Table[] */ 49 protected $_tables = []; 50 51 /** @var Sequence[] */ 52 protected $_sequences = []; 53 54 /** @var SchemaConfig */ 55 protected $_schemaConfig; 56 57 /** 58 * @param Table[] $tables 59 * @param Sequence[] $sequences 60 * @param string[] $namespaces 61 * 62 * @throws SchemaException 63 */ 64 public function __construct( 65 array $tables = [], 66 array $sequences = [], 67 ?SchemaConfig $schemaConfig = null, 68 array $namespaces = [] 69 ) { 70 if ($schemaConfig === null) { 71 $schemaConfig = new SchemaConfig(); 72 } 73 74 $this->_schemaConfig = $schemaConfig; 75 $this->_setName($schemaConfig->getName() ?? 'public'); 76 77 foreach ($namespaces as $namespace) { 78 $this->createNamespace($namespace); 79 } 80 81 foreach ($tables as $table) { 82 $this->_addTable($table); 83 } 84 85 foreach ($sequences as $sequence) { 86 $this->_addSequence($sequence); 87 } 88 } 89 90 /** 91 * @return bool 92 */ 93 public function hasExplicitForeignKeyIndexes() 94 { 95 return $this->_schemaConfig->hasExplicitForeignKeyIndexes(); 96 } 97 98 /** 99 * @return void 100 * 101 * @throws SchemaException 102 */ 103 protected function _addTable(Table $table) 104 { 105 $namespaceName = $table->getNamespaceName(); 106 $tableName = $table->getFullQualifiedName($this->getName()); 107 108 if (isset($this->_tables[$tableName])) { 109 throw SchemaException::tableAlreadyExists($tableName); 110 } 111 112 if ( 113 $namespaceName !== null 114 && ! $table->isInDefaultNamespace($this->getName()) 115 && ! $this->hasNamespace($namespaceName) 116 ) { 117 $this->createNamespace($namespaceName); 118 } 119 120 $this->_tables[$tableName] = $table; 121 $table->setSchemaConfig($this->_schemaConfig); 122 } 123 124 /** 125 * @return void 126 * 127 * @throws SchemaException 128 */ 129 protected function _addSequence(Sequence $sequence) 130 { 131 $namespaceName = $sequence->getNamespaceName(); 132 $seqName = $sequence->getFullQualifiedName($this->getName()); 133 134 if (isset($this->_sequences[$seqName])) { 135 throw SchemaException::sequenceAlreadyExists($seqName); 136 } 137 138 if ( 139 $namespaceName !== null 140 && ! $sequence->isInDefaultNamespace($this->getName()) 141 && ! $this->hasNamespace($namespaceName) 142 ) { 143 $this->createNamespace($namespaceName); 144 } 145 146 $this->_sequences[$seqName] = $sequence; 147 } 148 149 /** 150 * Returns the namespaces of this schema. 151 * 152 * @return string[] A list of namespace names. 153 */ 154 public function getNamespaces() 155 { 156 return $this->namespaces; 157 } 158 159 /** 160 * Gets all tables of this schema. 161 * 162 * @return Table[] 163 */ 164 public function getTables() 165 { 166 return $this->_tables; 167 } 168 169 /** 170 * @param string $name 171 * 172 * @return Table 173 * 174 * @throws SchemaException 175 */ 176 public function getTable($name) 177 { 178 $name = $this->getFullQualifiedAssetName($name); 179 if (! isset($this->_tables[$name])) { 180 throw SchemaException::tableDoesNotExist($name); 181 } 182 183 return $this->_tables[$name]; 184 } 185 186 /** 187 * @param string $name 188 * 189 * @return string 190 */ 191 private function getFullQualifiedAssetName($name) 192 { 193 $name = $this->getUnquotedAssetName($name); 194 195 if (strpos($name, '.') === false) { 196 $name = $this->getName() . '.' . $name; 197 } 198 199 return strtolower($name); 200 } 201 202 /** 203 * Returns the unquoted representation of a given asset name. 204 * 205 * @param string $assetName Quoted or unquoted representation of an asset name. 206 * 207 * @return string 208 */ 209 private function getUnquotedAssetName($assetName) 210 { 211 if ($this->isIdentifierQuoted($assetName)) { 212 return $this->trimQuotes($assetName); 213 } 214 215 return $assetName; 216 } 217 218 /** 219 * Does this schema have a namespace with the given name? 220 * 221 * @param string $name 222 * 223 * @return bool 224 */ 225 public function hasNamespace($name) 226 { 227 $name = strtolower($this->getUnquotedAssetName($name)); 228 229 return isset($this->namespaces[$name]); 230 } 231 232 /** 233 * Does this schema have a table with the given name? 234 * 235 * @param string $name 236 * 237 * @return bool 238 */ 239 public function hasTable($name) 240 { 241 $name = $this->getFullQualifiedAssetName($name); 242 243 return isset($this->_tables[$name]); 244 } 245 246 /** 247 * Gets all table names, prefixed with a schema name, even the default one if present. 248 * 249 * @return string[] 250 */ 251 public function getTableNames() 252 { 253 return array_keys($this->_tables); 254 } 255 256 /** 257 * @param string $name 258 * 259 * @return bool 260 */ 261 public function hasSequence($name) 262 { 263 $name = $this->getFullQualifiedAssetName($name); 264 265 return isset($this->_sequences[$name]); 266 } 267 268 /** 269 * @param string $name 270 * 271 * @return Sequence 272 * 273 * @throws SchemaException 274 */ 275 public function getSequence($name) 276 { 277 $name = $this->getFullQualifiedAssetName($name); 278 if (! $this->hasSequence($name)) { 279 throw SchemaException::sequenceDoesNotExist($name); 280 } 281 282 return $this->_sequences[$name]; 283 } 284 285 /** 286 * @return Sequence[] 287 */ 288 public function getSequences() 289 { 290 return $this->_sequences; 291 } 292 293 /** 294 * Creates a new namespace. 295 * 296 * @param string $name The name of the namespace to create. 297 * 298 * @return Schema This schema instance. 299 * 300 * @throws SchemaException 301 */ 302 public function createNamespace($name) 303 { 304 $unquotedName = strtolower($this->getUnquotedAssetName($name)); 305 306 if (isset($this->namespaces[$unquotedName])) { 307 throw SchemaException::namespaceAlreadyExists($unquotedName); 308 } 309 310 $this->namespaces[$unquotedName] = $name; 311 312 return $this; 313 } 314 315 /** 316 * Creates a new table. 317 * 318 * @param string $name 319 * 320 * @return Table 321 * 322 * @throws SchemaException 323 */ 324 public function createTable($name) 325 { 326 $table = new Table($name); 327 $this->_addTable($table); 328 329 foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) { 330 $table->addOption($option, $value); 331 } 332 333 return $table; 334 } 335 336 /** 337 * Renames a table. 338 * 339 * @param string $oldName 340 * @param string $newName 341 * 342 * @return Schema 343 * 344 * @throws SchemaException 345 */ 346 public function renameTable($oldName, $newName) 347 { 348 $table = $this->getTable($oldName); 349 $table->_setName($newName); 350 351 $this->dropTable($oldName); 352 $this->_addTable($table); 353 354 return $this; 355 } 356 357 /** 358 * Drops a table from the schema. 359 * 360 * @param string $name 361 * 362 * @return Schema 363 * 364 * @throws SchemaException 365 */ 366 public function dropTable($name) 367 { 368 $name = $this->getFullQualifiedAssetName($name); 369 $this->getTable($name); 370 unset($this->_tables[$name]); 371 372 return $this; 373 } 374 375 /** 376 * Creates a new sequence. 377 * 378 * @param string $name 379 * @param int $allocationSize 380 * @param int $initialValue 381 * 382 * @return Sequence 383 * 384 * @throws SchemaException 385 */ 386 public function createSequence($name, $allocationSize = 1, $initialValue = 1) 387 { 388 $seq = new Sequence($name, $allocationSize, $initialValue); 389 $this->_addSequence($seq); 390 391 return $seq; 392 } 393 394 /** 395 * @param string $name 396 * 397 * @return Schema 398 */ 399 public function dropSequence($name) 400 { 401 $name = $this->getFullQualifiedAssetName($name); 402 unset($this->_sequences[$name]); 403 404 return $this; 405 } 406 407 /** 408 * Returns an array of necessary SQL queries to create the schema on the given platform. 409 * 410 * @return string[] 411 */ 412 public function toSql(AbstractPlatform $platform) 413 { 414 $sqlCollector = new CreateSchemaSqlCollector($platform); 415 $this->visit($sqlCollector); 416 417 return $sqlCollector->getQueries(); 418 } 419 420 /** 421 * Return an array of necessary SQL queries to drop the schema on the given platform. 422 * 423 * @return string[] 424 */ 425 public function toDropSql(AbstractPlatform $platform) 426 { 427 $dropSqlCollector = new DropSchemaSqlCollector($platform); 428 $this->visit($dropSqlCollector); 429 430 return $dropSqlCollector->getQueries(); 431 } 432 433 /** 434 * @return string[] 435 * 436 * @throws SchemaException 437 */ 438 public function getMigrateToSql(Schema $toSchema, AbstractPlatform $platform) 439 { 440 $comparator = new Comparator(); 441 $schemaDiff = $comparator->compare($this, $toSchema); 442 443 return $schemaDiff->toSql($platform); 444 } 445 446 /** 447 * @return string[] 448 * 449 * @throws SchemaException 450 */ 451 public function getMigrateFromSql(Schema $fromSchema, AbstractPlatform $platform) 452 { 453 $comparator = new Comparator(); 454 $schemaDiff = $comparator->compare($fromSchema, $this); 455 456 return $schemaDiff->toSql($platform); 457 } 458 459 /** 460 * @return void 461 */ 462 public function visit(Visitor $visitor) 463 { 464 $visitor->acceptSchema($this); 465 466 if ($visitor instanceof NamespaceVisitor) { 467 foreach ($this->namespaces as $namespace) { 468 $visitor->acceptNamespace($namespace); 469 } 470 } 471 472 foreach ($this->_tables as $table) { 473 $table->visit($visitor); 474 } 475 476 foreach ($this->_sequences as $sequence) { 477 $sequence->visit($visitor); 478 } 479 } 480 481 /** 482 * Cloning a Schema triggers a deep clone of all related assets. 483 * 484 * @return void 485 */ 486 public function __clone() 487 { 488 foreach ($this->_tables as $k => $table) { 489 $this->_tables[$k] = clone $table; 490 } 491 492 foreach ($this->_sequences as $k => $sequence) { 493 $this->_sequences[$k] = clone $sequence; 494 } 495 } 496} 497