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    public function __construct(
63        array $tables = [],
64        array $sequences = [],
65        ?SchemaConfig $schemaConfig = null,
66        array $namespaces = []
67    ) {
68        if ($schemaConfig === null) {
69            $schemaConfig = new SchemaConfig();
70        }
71
72        $this->_schemaConfig = $schemaConfig;
73        $this->_setName($schemaConfig->getName() ?: 'public');
74
75        foreach ($namespaces as $namespace) {
76            $this->createNamespace($namespace);
77        }
78
79        foreach ($tables as $table) {
80            $this->_addTable($table);
81        }
82
83        foreach ($sequences as $sequence) {
84            $this->_addSequence($sequence);
85        }
86    }
87
88    /**
89     * @return bool
90     */
91    public function hasExplicitForeignKeyIndexes()
92    {
93        return $this->_schemaConfig->hasExplicitForeignKeyIndexes();
94    }
95
96    /**
97     * @return void
98     *
99     * @throws SchemaException
100     */
101    protected function _addTable(Table $table)
102    {
103        $namespaceName = $table->getNamespaceName();
104        $tableName     = $table->getFullQualifiedName($this->getName());
105
106        if (isset($this->_tables[$tableName])) {
107            throw SchemaException::tableAlreadyExists($tableName);
108        }
109
110        if (
111            $namespaceName !== null
112            && ! $table->isInDefaultNamespace($this->getName())
113            && ! $this->hasNamespace($namespaceName)
114        ) {
115            $this->createNamespace($namespaceName);
116        }
117
118        $this->_tables[$tableName] = $table;
119        $table->setSchemaConfig($this->_schemaConfig);
120    }
121
122    /**
123     * @return void
124     *
125     * @throws SchemaException
126     */
127    protected function _addSequence(Sequence $sequence)
128    {
129        $namespaceName = $sequence->getNamespaceName();
130        $seqName       = $sequence->getFullQualifiedName($this->getName());
131
132        if (isset($this->_sequences[$seqName])) {
133            throw SchemaException::sequenceAlreadyExists($seqName);
134        }
135
136        if (
137            $namespaceName !== null
138            && ! $sequence->isInDefaultNamespace($this->getName())
139            && ! $this->hasNamespace($namespaceName)
140        ) {
141            $this->createNamespace($namespaceName);
142        }
143
144        $this->_sequences[$seqName] = $sequence;
145    }
146
147    /**
148     * Returns the namespaces of this schema.
149     *
150     * @return string[] A list of namespace names.
151     */
152    public function getNamespaces()
153    {
154        return $this->namespaces;
155    }
156
157    /**
158     * Gets all tables of this schema.
159     *
160     * @return Table[]
161     */
162    public function getTables()
163    {
164        return $this->_tables;
165    }
166
167    /**
168     * @param string $name
169     *
170     * @return Table
171     *
172     * @throws SchemaException
173     */
174    public function getTable($name)
175    {
176        $name = $this->getFullQualifiedAssetName($name);
177        if (! isset($this->_tables[$name])) {
178            throw SchemaException::tableDoesNotExist($name);
179        }
180
181        return $this->_tables[$name];
182    }
183
184    /**
185     * @param string $name
186     *
187     * @return string
188     */
189    private function getFullQualifiedAssetName($name)
190    {
191        $name = $this->getUnquotedAssetName($name);
192
193        if (strpos($name, '.') === false) {
194            $name = $this->getName() . '.' . $name;
195        }
196
197        return strtolower($name);
198    }
199
200    /**
201     * Returns the unquoted representation of a given asset name.
202     *
203     * @param string $assetName Quoted or unquoted representation of an asset name.
204     *
205     * @return string
206     */
207    private function getUnquotedAssetName($assetName)
208    {
209        if ($this->isIdentifierQuoted($assetName)) {
210            return $this->trimQuotes($assetName);
211        }
212
213        return $assetName;
214    }
215
216    /**
217     * Does this schema have a namespace with the given name?
218     *
219     * @param string $name
220     *
221     * @return bool
222     */
223    public function hasNamespace($name)
224    {
225        $name = strtolower($this->getUnquotedAssetName($name));
226
227        return isset($this->namespaces[$name]);
228    }
229
230    /**
231     * Does this schema have a table with the given name?
232     *
233     * @param string $name
234     *
235     * @return bool
236     */
237    public function hasTable($name)
238    {
239        $name = $this->getFullQualifiedAssetName($name);
240
241        return isset($this->_tables[$name]);
242    }
243
244    /**
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
254    /**
255     * @param string $name
256     *
257     * @return bool
258     */
259    public function hasSequence($name)
260    {
261        $name = $this->getFullQualifiedAssetName($name);
262
263        return isset($this->_sequences[$name]);
264    }
265
266    /**
267     * @param string $name
268     *
269     * @return Sequence
270     *
271     * @throws SchemaException
272     */
273    public function getSequence($name)
274    {
275        $name = $this->getFullQualifiedAssetName($name);
276        if (! $this->hasSequence($name)) {
277            throw SchemaException::sequenceDoesNotExist($name);
278        }
279
280        return $this->_sequences[$name];
281    }
282
283    /**
284     * @return Sequence[]
285     */
286    public function getSequences()
287    {
288        return $this->_sequences;
289    }
290
291    /**
292     * Creates a new namespace.
293     *
294     * @param string $name The name of the namespace to create.
295     *
296     * @return Schema This schema instance.
297     *
298     * @throws SchemaException
299     */
300    public function createNamespace($name)
301    {
302        $unquotedName = strtolower($this->getUnquotedAssetName($name));
303
304        if (isset($this->namespaces[$unquotedName])) {
305            throw SchemaException::namespaceAlreadyExists($unquotedName);
306        }
307
308        $this->namespaces[$unquotedName] = $name;
309
310        return $this;
311    }
312
313    /**
314     * Creates a new table.
315     *
316     * @param string $name
317     *
318     * @return Table
319     */
320    public function createTable($name)
321    {
322        $table = new Table($name);
323        $this->_addTable($table);
324
325        foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) {
326            $table->addOption($option, $value);
327        }
328
329        return $table;
330    }
331
332    /**
333     * Renames a table.
334     *
335     * @param string $oldName
336     * @param string $newName
337     *
338     * @return Schema
339     */
340    public function renameTable($oldName, $newName)
341    {
342        $table = $this->getTable($oldName);
343        $table->_setName($newName);
344
345        $this->dropTable($oldName);
346        $this->_addTable($table);
347
348        return $this;
349    }
350
351    /**
352     * Drops a table from the schema.
353     *
354     * @param string $name
355     *
356     * @return Schema
357     */
358    public function dropTable($name)
359    {
360        $name = $this->getFullQualifiedAssetName($name);
361        $this->getTable($name);
362        unset($this->_tables[$name]);
363
364        return $this;
365    }
366
367    /**
368     * Creates a new sequence.
369     *
370     * @param string $name
371     * @param int    $allocationSize
372     * @param int    $initialValue
373     *
374     * @return Sequence
375     */
376    public function createSequence($name, $allocationSize = 1, $initialValue = 1)
377    {
378        $seq = new Sequence($name, $allocationSize, $initialValue);
379        $this->_addSequence($seq);
380
381        return $seq;
382    }
383
384    /**
385     * @param string $name
386     *
387     * @return Schema
388     */
389    public function dropSequence($name)
390    {
391        $name = $this->getFullQualifiedAssetName($name);
392        unset($this->_sequences[$name]);
393
394        return $this;
395    }
396
397    /**
398     * Returns an array of necessary SQL queries to create the schema on the given platform.
399     *
400     * @return string[]
401     */
402    public function toSql(AbstractPlatform $platform)
403    {
404        $sqlCollector = new CreateSchemaSqlCollector($platform);
405        $this->visit($sqlCollector);
406
407        return $sqlCollector->getQueries();
408    }
409
410    /**
411     * Return an array of necessary SQL queries to drop the schema on the given platform.
412     *
413     * @return string[]
414     */
415    public function toDropSql(AbstractPlatform $platform)
416    {
417        $dropSqlCollector = new DropSchemaSqlCollector($platform);
418        $this->visit($dropSqlCollector);
419
420        return $dropSqlCollector->getQueries();
421    }
422
423    /**
424     * @return string[]
425     */
426    public function getMigrateToSql(Schema $toSchema, AbstractPlatform $platform)
427    {
428        $comparator = new Comparator();
429        $schemaDiff = $comparator->compare($this, $toSchema);
430
431        return $schemaDiff->toSql($platform);
432    }
433
434    /**
435     * @return string[]
436     */
437    public function getMigrateFromSql(Schema $fromSchema, AbstractPlatform $platform)
438    {
439        $comparator = new Comparator();
440        $schemaDiff = $comparator->compare($fromSchema, $this);
441
442        return $schemaDiff->toSql($platform);
443    }
444
445    /**
446     * @return void
447     */
448    public function visit(Visitor $visitor)
449    {
450        $visitor->acceptSchema($this);
451
452        if ($visitor instanceof NamespaceVisitor) {
453            foreach ($this->namespaces as $namespace) {
454                $visitor->acceptNamespace($namespace);
455            }
456        }
457
458        foreach ($this->_tables as $table) {
459            $table->visit($visitor);
460        }
461
462        foreach ($this->_sequences as $sequence) {
463            $sequence->visit($visitor);
464        }
465    }
466
467    /**
468     * Cloning a Schema triggers a deep clone of all related assets.
469     *
470     * @return void
471     */
472    public function __clone()
473    {
474        foreach ($this->_tables as $k => $table) {
475            $this->_tables[$k] = clone $table;
476        }
477
478        foreach ($this->_sequences as $k => $sequence) {
479            $this->_sequences[$k] = clone $sequence;
480        }
481    }
482}
483