Completed
Branch feature/pre-split (f1ffcf)
by Anton
03:59
created

SchemaBuilder::packSchema()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 3
nop 0
dl 0
loc 23
rs 8.7972
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Schemas;
8
9
use Psr\Log\LoggerInterface;
10
use Spiral\Database\DatabaseManager;
11
use Spiral\Database\Exceptions\DBALException;
12
use Spiral\Database\Exceptions\DriverException;
13
use Spiral\Database\Exceptions\QueryException;
14
use Spiral\Database\Helpers\SynchronizationPool;
15
use Spiral\Database\Schemas\Prototypes\AbstractTable;
16
use Spiral\ORM\Exceptions\DoubleReferenceException;
17
use Spiral\ORM\Exceptions\SchemaException;
18
use Spiral\ORM\ORMInterface;
19
20
class SchemaBuilder
21
{
22
    /**
23
     * @var DatabaseManager
24
     */
25
    private $manager;
26
27
    /**
28
     * @var AbstractTable[]
29
     */
30
    private $tables = [];
31
32
    /**
33
     * @var SchemaInterface[]
34
     */
35
    private $schemas = [];
36
37
    /**
38
     * Class names of sources associated with specific class.
39
     *
40
     * @var array
41
     */
42
    private $sources = [];
43
44
    /**
45
     * @param DatabaseManager $manager
46
     */
47
    public function __construct(DatabaseManager $manager)
48
    {
49
        $this->manager = $manager;
50
    }
51
52
    /**
53
     * Add new model schema into pool.
54
     *
55
     * @param SchemaInterface $schema
56
     *
57
     * @return self|$this
58
     */
59
    public function addSchema(SchemaInterface $schema): SchemaBuilder
60
    {
61
        $this->schemas[$schema->getClass()] = $schema;
62
63
        return $this;
64
    }
65
66
    /**
67
     * @param string $class
68
     *
69
     * @return bool
70
     */
71
    public function hasSchema(string $class): bool
72
    {
73
        return isset($this->schemas[$class]);
74
    }
75
76
    /**
77
     * @param string $class
78
     *
79
     * @return SchemaInterface
80
     *
81
     * @throws SchemaException
82
     */
83
    public function getSchema(string $class): SchemaInterface
84
    {
85
        if (!$this->hasSchema($class)) {
86
            throw new SchemaException("Unable to find schema for class '{$class}'");
87
        }
88
89
        return $this->schemas[$class];
90
    }
91
92
    /**
93
     * All available document schemas.
94
     *
95
     * @return SchemaInterface[]
96
     */
97
    public function getSchemas(): array
98
    {
99
        return $this->schemas;
100
    }
101
102
    /**
103
     * Associate source class with entity class. Source will be automatically associated with given
104
     * class and all classes from the same collection which extends it.
105
     *
106
     * @param string $class
107
     * @param string $source
108
     *
109
     * @return SchemaBuilder
110
     *
111
     * @throws SchemaException
112
     */
113
    public function addSource(string $class, string $source): SchemaBuilder
114
    {
115
        if (!$this->hasSchema($class)) {
116
            throw new SchemaException("Unable to add source to '{$class}', class is unknown to ORM");
117
        }
118
119
        $this->sources[$class] = $source;
120
121
        return $this;
122
    }
123
124
    /**
125
     * Check if given entity has associated source.
126
     *
127
     * @param string $class
128
     *
129
     * @return bool
130
     */
131
    public function hasSource(string $class): bool
132
    {
133
        return array_key_exists($class, $this->sources);
134
    }
135
136
    /**
137
     * Get source associated with specific class, if any.
138
     *
139
     * @param string $class
140
     *
141
     * @return string|null
142
     */
143
    public function getSource(string $class)
144
    {
145
        if (!$this->hasSource($class)) {
146
            return null;
147
        }
148
149
        return $this->sources[$class];
150
    }
151
152
    /**
153
     * Process all added schemas and relations in order to created needed tables, indexes and etc.
154
     * Attention, this method will return new instance of SchemaBuilder without affecting original
155
     * object. You MUST call this method before calling packSchema() method.
156
     *
157
     * Attention, this methods DOES NOT write anything into database, use pushSchema() to push
158
     * changes into database using automatic diff generation. You can also access list of
159
     * generated/changed tables via getTables() to create your own migrations.
160
     *
161
     * @see packSchema()
162
     * @see pushSchema()
163
     * @see getTables()
164
     *
165
     * @return SchemaBuilder
166
     *
167
     * @throws SchemaException
168
     */
169
    public function renderSchema(): SchemaBuilder
170
    {
171
        $builder = clone $this;
172
173
        //Relation manager?
174
175
        foreach ($builder->schemas as $schema) {
176
            //Get table state (empty one)
177
            $table = $this->requestTable(
178
                $schema->getTable(),
179
                $schema->getDatabase(),
180
                true,
181
                true
182
            );
183
184
            //Define it's schema
185
            $table = $schema->renderTable($table);
186
187
            //Working with indexes
188
            foreach ($schema->getIndexes() as $index) {
189
                $table->index($index->getColumns())->unique($index->isUnique());
190
                $table->index($index->getColumns())->setName($index->getName());
191
            }
192
193
            //And put it back :)
194
            $this->pushTable($table, $schema->getDatabase());
195
        }
196
197
        //Working with defined relations
198
199
        return $this;
200
    }
201
202
    /**
203
     * Get all defined tables, make sure to call renderSchema() first. Attention, all given tables
204
     * will be returned in detached state.
205
     *
206
     * @return AbstractTable[]
207
     *
208
     * @throws SchemaException
209
     */
210
    public function getTables(): array
211
    {
212
        if (empty($this->tables) && !empty($this->schemas)) {
213
            throw new SchemaException(
214
                "Unable to get tables, no tables are were found, call renderSchema() first"
215
            );
216
        }
217
218
        $result = [];
219
        foreach ($this->tables as $table) {
220
            //Detaching
221
            $result[] = clone $table;
222
        }
223
224
        return $result;
225
    }
226
227
    /**
228
     * Indication that tables in database require syncing before being matched with ORM models.
229
     *
230
     * @return bool
231
     */
232
    public function hasChanges(): bool
233
    {
234
        foreach ($this->getTables() as $table) {
235
            if ($table->getComparator()->hasChanges()) {
236
                return true;
237
            }
238
        }
239
240
        return false;
241
    }
242
243
    /**
244
     * Save every change made to generated tables. Method utilizes default DBAL diff mechanism,
245
     * use getTables() method in order to generate your own migrations.
246
     *
247
     * @param LoggerInterface|null $logger
248
     *
249
     * @throws SchemaException
250
     * @throws DBALException
251
     * @throws QueryException
252
     * @throws DriverException
253
     */
254
    public function pushSchema(LoggerInterface $logger = null)
255
    {
256
        $bus = new SynchronizationPool($this->getTables());
257
        $bus->run($logger);
258
    }
259
260
    /**
261
     * Pack declared schemas in a normalized form, make sure to call renderSchema() first.
262
     *
263
     * @return array
264
     *
265
     * @throws SchemaException
266
     */
267
    public function packSchema(): array
268
    {
269
        if (empty($this->tables) && !empty($this->schemas)) {
270
            throw new SchemaException(
271
                "Unable to pack schema, no defined tables were found, call defineTables() first"
272
            );
273
        }
274
275
        $result = [];
276
        foreach ($this->schemas as $class => $schema) {
277
            $result[$class][] = [
278
                ORMInterface::R_INSTANTIATOR => $schema->getInstantiator(),
279
                ORMInterface::R_SCHEMA       => $schema->packSchema($this, null),
280
                ORMInterface::R_SOURCE_CLASS => $this->getSource($class),
281
                ORMInterface::R_DATABASE     => $schema->getDatabase(),
282
                ORMInterface::R_TABLE        => $schema->getTable(),
283
                ORMInterface::R_RELATIONS    => [/*external manager*/]
284
                //relations???
285
            ];
286
        }
287
288
        return $result;
289
    }
290
291
    /**
292
     * Request table schema by name/database combination.
293
     *
294
     * @param string      $table
295
     * @param string|null $database
296
     * @param bool        $resetState When set to true current table state will be reset in order
297
     *                                to
298
     *                                allows model to redefine it's schema.
299
     * @param bool        $unique     Set to true (default), to throw an exception when table
300
     *                                already referenced by another model.
301
     *
302
     * @return AbstractTable          Unlinked.
303
     *
304
     * @throws DoubleReferenceException When two records refers to same table and unique option
305
     *                                  set.
306
     */
307
    protected function requestTable(
308
        string $table,
309
        string $database = null,
310
        bool $unique = true,
311
        bool $resetState = false
312
    ): AbstractTable {
313
        if (isset($this->tables[$database . '.table'])) {
314
            $schema = $this->tables[$database . '.table'];
315
316
            if ($unique) {
317
                throw new DoubleReferenceException(
318
                    "Table '{$table}' of '{$database} 'been requested by multiple models"
319
                );
320
            }
321
        } else {
322
            //Requesting thought DatabaseManager
323
            $schema = $this->manager->database($database)->table($table)->getSchema();
324
            $this->tables[$database . '.' . $table] = $schema;
325
        }
326
327
        $schema = clone $schema;
328
329
        if ($resetState) {
330
            //Emptying our current state (initial not affected)
331
            $schema->setState(null);
332
        }
333
334
        return $schema;
335
    }
336
337
    /**
338
     * Update table state.
339
     *
340
     * @param AbstractTable $table
341
     * @param string|null   $database
342
     */
343
    private function pushTable(AbstractTable $table, string $database = null)
344
    {
345
        $this->tables[$database . '.' . $table->getName()] = $table;
346
    }
347
}