Schema.php 12 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 13
use function array_keys;
use function strpos;
use function strtolower;
14 15

/**
Benjamin Morel's avatar
Benjamin Morel committed
16
 * Object representation of a database schema.
17
 *
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
 * 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.
38 39 40
 */
class Schema extends AbstractAsset
{
41 42 43
    /**
     * The namespaces in this schema.
     *
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 = false;
56

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

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

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

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

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

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

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

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

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

118
        $this->_tables[$tableName] = $table;
119
        $table->setSchemaConfig($this->_schemaConfig);
120 121 122
    }

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

132 133 134
        if (isset($this->_sequences[$seqName])) {
            throw SchemaException::sequenceAlreadyExists($seqName);
        }
135

136 137
        if (
            $namespaceName !== null
Sergei Morozov's avatar
Sergei Morozov committed
138
            && ! $sequence->isInDefaultNamespace($this->getName())
139 140
            && ! $this->hasNamespace($namespaceName)
        ) {
141 142 143
            $this->createNamespace($namespaceName);
        }

144 145 146
        $this->_sequences[$seqName] = $sequence;
    }

147 148 149
    /**
     * Returns the namespaces of this schema.
     *
150
     * @return string[] A list of namespace names.
151 152 153 154 155 156
     */
    public function getNamespaces()
    {
        return $this->namespaces;
    }

157
    /**
Benjamin Morel's avatar
Benjamin Morel committed
158
     * Gets all tables of this schema.
159
     *
160
     * @return Table[]
161 162 163 164 165 166 167 168
     */
    public function getTables()
    {
        return $this->_tables;
    }

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

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

184
    /**
Benjamin Morel's avatar
Benjamin Morel committed
185 186
     * @param string $name
     *
187 188 189 190
     * @return string
     */
    private function getFullQualifiedAssetName($name)
    {
191 192
        $name = $this->getUnquotedAssetName($name);

193 194
        if (strpos($name, '.') === false) {
            $name = $this->getName() . '.' . $name;
195
        }
Benjamin Morel's avatar
Benjamin Morel committed
196

197 198 199
        return strtolower($name);
    }

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
    /**
     * 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
216 217 218 219 220
    /**
     * Does this schema have a namespace with the given name?
     *
     * @param string $namespaceName
     *
221
     * @return bool
Marco Pivetta's avatar
Marco Pivetta committed
222 223 224
     */
    public function hasNamespace($namespaceName)
    {
225
        $namespaceName = strtolower($this->getUnquotedAssetName($namespaceName));
Marco Pivetta's avatar
Marco Pivetta committed
226

227
        return isset($this->namespaces[$namespaceName]);
Marco Pivetta's avatar
Marco Pivetta committed
228 229
    }

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

241 242 243
        return isset($this->_tables[$tableName]);
    }

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

Benjamin Morel's avatar
Benjamin Morel committed
254 255 256
    /**
     * @param string $sequenceName
     *
257
     * @return bool
Benjamin Morel's avatar
Benjamin Morel committed
258
     */
259 260
    public function hasSequence($sequenceName)
    {
261
        $sequenceName = $this->getFullQualifiedAssetName($sequenceName);
Benjamin Morel's avatar
Benjamin Morel committed
262

263 264 265 266
        return isset($this->_sequences[$sequenceName]);
    }

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

280 281 282 283
        return $this->_sequences[$sequenceName];
    }

    /**
284
     * @return Sequence[]
285 286 287 288 289 290
     */
    public function getSequences()
    {
        return $this->_sequences;
    }

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

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

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

        return $this;
    }

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

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

329 330 331 332
        return $table;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
333
     * Renames a table.
334 335 336
     *
     * @param string $oldTableName
     * @param string $newTableName
Benjamin Morel's avatar
Benjamin Morel committed
337
     *
Grégoire Paris's avatar
Grégoire Paris committed
338
     * @return Schema
339 340 341 342 343 344 345 346
     */
    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
347

348 349 350 351
        return $this;
    }

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

364 365 366 367
        return $this;
    }

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

381
        return $seq;
382 383 384 385
    }

    /**
     * @param string $sequenceName
Benjamin Morel's avatar
Benjamin Morel committed
386
     *
Grégoire Paris's avatar
Grégoire Paris committed
387
     * @return Schema
388 389 390
     */
    public function dropSequence($sequenceName)
    {
391
        $sequenceName = $this->getFullQualifiedAssetName($sequenceName);
392
        unset($this->_sequences[$sequenceName]);
Benjamin Morel's avatar
Benjamin Morel committed
393

394 395 396 397
        return $this;
    }

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

        return $sqlCollector->getQueries();
    }

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

        return $dropSqlCollector->getQueries();
    }

423
    /**
424
     * @return string[]
425
     */
Benjamin Morel's avatar
Benjamin Morel committed
426
    public function getMigrateToSql(Schema $toSchema, AbstractPlatform $platform)
427
    {
428
        $comparator = new Comparator();
429
        $schemaDiff = $comparator->compare($this, $toSchema);
Benjamin Morel's avatar
Benjamin Morel committed
430

431
        return $schemaDiff->toSql($platform);
432 433
    }

434
    /**
435
     * @return string[]
436
     */
Benjamin Morel's avatar
Benjamin Morel committed
437
    public function getMigrateFromSql(Schema $fromSchema, AbstractPlatform $platform)
438
    {
439
        $comparator = new Comparator();
440
        $schemaDiff = $comparator->compare($fromSchema, $this);
Benjamin Morel's avatar
Benjamin Morel committed
441

442
        return $schemaDiff->toSql($platform);
443 444
    }

445
    /**
Benjamin Morel's avatar
Benjamin Morel committed
446
     * @return void
447 448 449 450
     */
    public function visit(Visitor $visitor)
    {
        $visitor->acceptSchema($this);
451

452 453 454 455 456 457
        if ($visitor instanceof NamespaceVisitor) {
            foreach ($this->namespaces as $namespace) {
                $visitor->acceptNamespace($namespace);
            }
        }

458
        foreach ($this->_tables as $table) {
459 460
            $table->visit($visitor);
        }
461

462
        foreach ($this->_sequences as $sequence) {
463 464 465
            $sequence->visit($visitor);
        }
    }
466 467 468 469 470 471 472 473

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

478
        foreach ($this->_sequences as $k => $sequence) {
479 480 481
            $this->_sequences[$k] = clone $sequence;
        }
    }
482
}