Schema.php 11.9 KB
Newer Older
1 2 3 4
<?php

namespace Doctrine\DBAL\Schema;

5
use Doctrine\DBAL\Platforms\AbstractPlatform;
6
use Doctrine\DBAL\Schema\Visitor\CreateSchemaSqlCollector;
7
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
8
use Doctrine\DBAL\Schema\Visitor\NamespaceVisitor;
9
use Doctrine\DBAL\Schema\Visitor\Visitor;
10 11 12
use function array_keys;
use function strpos;
use function strtolower;
13 14

/**
Benjamin Morel's avatar
Benjamin Morel committed
15
 * Object representation of a database schema.
16
 *
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
 * Different vendors have very inconsistent naming with regard to the concept
 * of a "schema". Doctrine understands a schema as the entity that conceptually
 * wraps a set of database objects such as tables, sequences, indexes and
 * foreign keys that belong to each other into a namespace. A Doctrine Schema
 * has nothing to do with the "SCHEMA" defined as in PostgreSQL, it is more
 * related to the concept of "DATABASE" that exists in MySQL and PostgreSQL.
 *
 * Every asset in the doctrine schema has a name. A name consists of either a
 * namespace.local name pair or just a local unqualified name.
 *
 * The abstraction layer that covers a PostgreSQL schema is the namespace of an
 * database object (asset). A schema can have a name, which will be used as
 * default namespace for the unqualified database objects that are created in
 * the schema.
 *
 * In the case of MySQL where cross-database queries are allowed this leads to
 * databases being "misinterpreted" as namespaces. This is intentional, however
 * the CREATE/DROP SQL visitors will just filter this queries and do not
 * execute them. Only the queries for the currently connected database are
 * executed.
37 38 39
 */
class Schema extends AbstractAsset
{
40 41 42
    /**
     * The namespaces in this schema.
     *
43
     * @var string[]
44
     */
45
    private $namespaces = [];
46

47
    /** @var Table[] */
48
    protected $_tables = [];
49

50
    /** @var Sequence[] */
51
    protected $_sequences = [];
52

53
    /** @var SchemaConfig */
54
    protected $_schemaConfig = false;
55

56
    /**
57 58
     * @param Table[]    $tables
     * @param Sequence[] $sequences
59
     * @param string[]   $namespaces
60
     */
61
    public function __construct(
62 63
        array $tables = [],
        array $sequences = [],
64
        ?SchemaConfig $schemaConfig = null,
65
        array $namespaces = []
66
    ) {
67
        if ($schemaConfig === null) {
68 69
            $schemaConfig = new SchemaConfig();
        }
Grégoire Paris's avatar
Grégoire Paris committed
70

71
        $this->_schemaConfig = $schemaConfig;
72
        $this->_setName($schemaConfig->getName() ?? 'public');
73

74 75 76 77
        foreach ($namespaces as $namespace) {
            $this->createNamespace($namespace);
        }

78
        foreach ($tables as $table) {
79 80
            $this->_addTable($table);
        }
81

82
        foreach ($sequences as $sequence) {
83 84
            $this->_addSequence($sequence);
        }
85 86 87
    }

    /**
88
     * @return bool
89 90 91
     */
    public function hasExplicitForeignKeyIndexes()
    {
92
        return $this->_schemaConfig->hasExplicitForeignKeyIndexes();
93 94 95
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
96 97
     * @return void
     *
98
     * @throws SchemaException
99 100 101
     */
    protected function _addTable(Table $table)
    {
102
        $namespaceName = $table->getNamespaceName();
103
        $tableName     = $table->getFullQualifiedName($this->getName());
104

Steve Müller's avatar
Steve Müller committed
105
        if (isset($this->_tables[$tableName])) {
106 107 108
            throw SchemaException::tableAlreadyExists($tableName);
        }

Sergei Morozov's avatar
Sergei Morozov committed
109 110 111
        if ($namespaceName !== null
            && ! $table->isInDefaultNamespace($this->getName())
            && ! $this->hasNamespace($namespaceName)) {
112 113 114
            $this->createNamespace($namespaceName);
        }

115
        $this->_tables[$tableName] = $table;
116
        $table->setSchemaConfig($this->_schemaConfig);
117 118 119
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
120 121
     * @return void
     *
122
     * @throws SchemaException
123 124 125
     */
    protected function _addSequence(Sequence $sequence)
    {
126
        $namespaceName = $sequence->getNamespaceName();
127
        $seqName       = $sequence->getFullQualifiedName($this->getName());
128

129 130 131
        if (isset($this->_sequences[$seqName])) {
            throw SchemaException::sequenceAlreadyExists($seqName);
        }
132

Sergei Morozov's avatar
Sergei Morozov committed
133 134 135
        if ($namespaceName !== null
            && ! $sequence->isInDefaultNamespace($this->getName())
            && ! $this->hasNamespace($namespaceName)) {
136 137 138
            $this->createNamespace($namespaceName);
        }

139 140 141
        $this->_sequences[$seqName] = $sequence;
    }

142 143 144
    /**
     * Returns the namespaces of this schema.
     *
145
     * @return string[] A list of namespace names.
146 147 148 149 150 151
     */
    public function getNamespaces()
    {
        return $this->namespaces;
    }

152
    /**
Benjamin Morel's avatar
Benjamin Morel committed
153
     * Gets all tables of this schema.
154
     *
155
     * @return Table[]
156 157 158 159 160 161 162 163
     */
    public function getTables()
    {
        return $this->_tables;
    }

    /**
     * @param string $tableName
Benjamin Morel's avatar
Benjamin Morel committed
164
     *
165
     * @return Table
Benjamin Morel's avatar
Benjamin Morel committed
166
     *
167
     * @throws SchemaException
168 169 170
     */
    public function getTable($tableName)
    {
171
        $tableName = $this->getFullQualifiedAssetName($tableName);
172
        if (! isset($this->_tables[$tableName])) {
173 174 175 176 177 178
            throw SchemaException::tableDoesNotExist($tableName);
        }

        return $this->_tables[$tableName];
    }

179
    /**
Benjamin Morel's avatar
Benjamin Morel committed
180 181
     * @param string $name
     *
182 183 184 185
     * @return string
     */
    private function getFullQualifiedAssetName($name)
    {
186 187
        $name = $this->getUnquotedAssetName($name);

188 189
        if (strpos($name, '.') === false) {
            $name = $this->getName() . '.' . $name;
190
        }
Benjamin Morel's avatar
Benjamin Morel committed
191

192 193 194
        return strtolower($name);
    }

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
    /**
     * Returns the unquoted representation of a given asset name.
     *
     * @param string $assetName Quoted or unquoted representation of an asset name.
     *
     * @return string
     */
    private function getUnquotedAssetName($assetName)
    {
        if ($this->isIdentifierQuoted($assetName)) {
            return $this->trimQuotes($assetName);
        }

        return $assetName;
    }

Marco Pivetta's avatar
Marco Pivetta committed
211 212 213 214 215
    /**
     * Does this schema have a namespace with the given name?
     *
     * @param string $namespaceName
     *
216
     * @return bool
Marco Pivetta's avatar
Marco Pivetta committed
217 218 219
     */
    public function hasNamespace($namespaceName)
    {
220
        $namespaceName = strtolower($this->getUnquotedAssetName($namespaceName));
Marco Pivetta's avatar
Marco Pivetta committed
221

222
        return isset($this->namespaces[$namespaceName]);
Marco Pivetta's avatar
Marco Pivetta committed
223 224
    }

225 226
    /**
     * Does this schema have a table with the given name?
227
     *
Benjamin Morel's avatar
Benjamin Morel committed
228 229
     * @param string $tableName
     *
230
     * @return bool
231 232 233
     */
    public function hasTable($tableName)
    {
234
        $tableName = $this->getFullQualifiedAssetName($tableName);
Benjamin Morel's avatar
Benjamin Morel committed
235

236 237 238
        return isset($this->_tables[$tableName]);
    }

239
    /**
Benjamin Morel's avatar
Benjamin Morel committed
240
     * Gets all table names, prefixed with a schema name, even the default one if present.
241
     *
242
     * @return string[]
243
     */
244
    public function getTableNames()
245
    {
246
        return array_keys($this->_tables);
247 248
    }

Benjamin Morel's avatar
Benjamin Morel committed
249 250 251
    /**
     * @param string $sequenceName
     *
252
     * @return bool
Benjamin Morel's avatar
Benjamin Morel committed
253
     */
254 255
    public function hasSequence($sequenceName)
    {
256
        $sequenceName = $this->getFullQualifiedAssetName($sequenceName);
Benjamin Morel's avatar
Benjamin Morel committed
257

258 259 260 261
        return isset($this->_sequences[$sequenceName]);
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
262 263
     * @param string $sequenceName
     *
264
     * @return Sequence
Benjamin Morel's avatar
Benjamin Morel committed
265
     *
266
     * @throws SchemaException
267 268 269
     */
    public function getSequence($sequenceName)
    {
270
        $sequenceName = $this->getFullQualifiedAssetName($sequenceName);
271
        if (! $this->hasSequence($sequenceName)) {
272 273
            throw SchemaException::sequenceDoesNotExist($sequenceName);
        }
Benjamin Morel's avatar
Benjamin Morel committed
274

275 276 277 278
        return $this->_sequences[$sequenceName];
    }

    /**
279
     * @return Sequence[]
280 281 282 283 284 285
     */
    public function getSequences()
    {
        return $this->_sequences;
    }

286 287 288 289 290
    /**
     * Creates a new namespace.
     *
     * @param string $namespaceName The name of the namespace to create.
     *
Grégoire Paris's avatar
Grégoire Paris committed
291
     * @return Schema This schema instance.
292 293
     *
     * @throws SchemaException
294 295 296 297 298 299 300 301 302 303 304 305 306 307
     */
    public function createNamespace($namespaceName)
    {
        $unquotedNamespaceName = strtolower($this->getUnquotedAssetName($namespaceName));

        if (isset($this->namespaces[$unquotedNamespaceName])) {
            throw SchemaException::namespaceAlreadyExists($unquotedNamespaceName);
        }

        $this->namespaces[$unquotedNamespaceName] = $namespaceName;

        return $this;
    }

308
    /**
Benjamin Morel's avatar
Benjamin Morel committed
309 310 311
     * Creates a new table.
     *
     * @param string $tableName
312
     *
313
     * @return Table
314 315 316 317 318
     */
    public function createTable($tableName)
    {
        $table = new Table($tableName);
        $this->_addTable($table);
319 320 321 322 323

        foreach ($this->_schemaConfig->getDefaultTableOptions() as $name => $value) {
            $table->addOption($name, $value);
        }

324 325 326 327
        return $table;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
328
     * Renames a table.
329 330 331
     *
     * @param string $oldTableName
     * @param string $newTableName
Benjamin Morel's avatar
Benjamin Morel committed
332
     *
Grégoire Paris's avatar
Grégoire Paris committed
333
     * @return Schema
334 335 336 337 338 339 340 341
     */
    public function renameTable($oldTableName, $newTableName)
    {
        $table = $this->getTable($oldTableName);
        $table->_setName($newTableName);

        $this->dropTable($oldTableName);
        $this->_addTable($table);
Benjamin Morel's avatar
Benjamin Morel committed
342

343 344 345 346
        return $this;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
347
     * Drops a table from the schema.
348 349
     *
     * @param string $tableName
Benjamin Morel's avatar
Benjamin Morel committed
350
     *
Grégoire Paris's avatar
Grégoire Paris committed
351
     * @return Schema
352 353 354
     */
    public function dropTable($tableName)
    {
355
        $tableName = $this->getFullQualifiedAssetName($tableName);
Benjamin Morel's avatar
Benjamin Morel committed
356
        $this->getTable($tableName);
357
        unset($this->_tables[$tableName]);
Benjamin Morel's avatar
Benjamin Morel committed
358

359 360 361 362
        return $this;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
363 364
     * Creates a new sequence.
     *
365 366 367
     * @param string $sequenceName
     * @param int    $allocationSize
     * @param int    $initialValue
368
     *
369
     * @return Sequence
370
     */
371
    public function createSequence($sequenceName, $allocationSize = 1, $initialValue = 1)
372 373 374
    {
        $seq = new Sequence($sequenceName, $allocationSize, $initialValue);
        $this->_addSequence($seq);
Benjamin Morel's avatar
Benjamin Morel committed
375

376
        return $seq;
377 378 379 380
    }

    /**
     * @param string $sequenceName
Benjamin Morel's avatar
Benjamin Morel committed
381
     *
Grégoire Paris's avatar
Grégoire Paris committed
382
     * @return Schema
383 384 385
     */
    public function dropSequence($sequenceName)
    {
386
        $sequenceName = $this->getFullQualifiedAssetName($sequenceName);
387
        unset($this->_sequences[$sequenceName]);
Benjamin Morel's avatar
Benjamin Morel committed
388

389 390 391 392
        return $this;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
393
     * Returns an array of necessary SQL queries to create the schema on the given platform.
394
     *
395
     * @return string[]
396
     */
Benjamin Morel's avatar
Benjamin Morel committed
397
    public function toSql(AbstractPlatform $platform)
398 399 400 401 402 403 404
    {
        $sqlCollector = new CreateSchemaSqlCollector($platform);
        $this->visit($sqlCollector);

        return $sqlCollector->getQueries();
    }

405
    /**
Benjamin Morel's avatar
Benjamin Morel committed
406
     * Return an array of necessary SQL queries to drop the schema on the given platform.
407
     *
408
     * @return string[]
409
     */
Benjamin Morel's avatar
Benjamin Morel committed
410
    public function toDropSql(AbstractPlatform $platform)
411 412 413 414 415 416 417
    {
        $dropSqlCollector = new DropSchemaSqlCollector($platform);
        $this->visit($dropSqlCollector);

        return $dropSqlCollector->getQueries();
    }

418
    /**
419
     * @return string[]
420
     */
Benjamin Morel's avatar
Benjamin Morel committed
421
    public function getMigrateToSql(Schema $toSchema, AbstractPlatform $platform)
422
    {
423
        $comparator = new Comparator();
424
        $schemaDiff = $comparator->compare($this, $toSchema);
Benjamin Morel's avatar
Benjamin Morel committed
425

426
        return $schemaDiff->toSql($platform);
427 428
    }

429
    /**
430
     * @return string[]
431
     */
Benjamin Morel's avatar
Benjamin Morel committed
432
    public function getMigrateFromSql(Schema $fromSchema, AbstractPlatform $platform)
433
    {
434
        $comparator = new Comparator();
435
        $schemaDiff = $comparator->compare($fromSchema, $this);
Benjamin Morel's avatar
Benjamin Morel committed
436

437
        return $schemaDiff->toSql($platform);
438 439
    }

440
    /**
Benjamin Morel's avatar
Benjamin Morel committed
441
     * @return void
442 443 444 445
     */
    public function visit(Visitor $visitor)
    {
        $visitor->acceptSchema($this);
446

447 448 449 450 451 452
        if ($visitor instanceof NamespaceVisitor) {
            foreach ($this->namespaces as $namespace) {
                $visitor->acceptNamespace($namespace);
            }
        }

453
        foreach ($this->_tables as $table) {
454 455
            $table->visit($visitor);
        }
456

457
        foreach ($this->_sequences as $sequence) {
458 459 460
            $sequence->visit($visitor);
        }
    }
461 462 463 464 465 466 467 468

    /**
     * Cloning a Schema triggers a deep clone of all related assets.
     *
     * @return void
     */
    public function __clone()
    {
469
        foreach ($this->_tables as $k => $table) {
470 471
            $this->_tables[$k] = clone $table;
        }
Grégoire Paris's avatar
Grégoire Paris committed
472

473
        foreach ($this->_sequences as $k => $sequence) {
474 475 476
            $this->_sequences[$k] = clone $sequence;
        }
    }
477
}