Completed
Branch feature/pre-split (60f5c0)
by Anton
03:19
created

SchemaBuilder::requestTable()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 18
nc 5
nop 4
dl 0
loc 29
rs 8.5806
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\SynchronizationBus;
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($schema->getTable(), $schema->getDatabase(), true, true);
178
179
            //Define it's schema
180
            $table = $schema->defineTable($table);
181
182
            //Working with indexes
183
            foreach ($schema->getIndexes() as $index) {
184
                $table->index($index->getColumns())->unique($index->isUnique());
185
            }
186
187
            //And put it back :)
188
            $this->pushTable($table, $schema->getDatabase());
189
        }
190
191
        //Working with defined relations
192
193
        return $this;
194
    }
195
196
    /**
197
     * Get all defined tables, make sure to call renderSchema() first. Attention, all given tables
198
     * will be returned in detached state.
199
     *
200
     * @return AbstractTable[]
201
     *
202
     * @throws SchemaException
203
     */
204
    public function getTables(): array
205
    {
206
        if (empty($this->tables) && !empty($this->schemas)) {
207
            throw new SchemaException(
208
                "Unable to get tables, no defined tables were found, call defineTables() first"
209
            );
210
        }
211
212
        $result = [];
213
        foreach ($this->tables as $table) {
214
            //Detaching
215
            $result[] = clone $table;
216
        }
217
218
        return $result;
219
    }
220
221
    /**
222
     * Save every change made to generated tables. Method utilizes default DBAL diff mechanism,
223
     * use getTables() method in order to generate your own migrations.
224
     *
225
     * @param LoggerInterface|null $logger
226
     *
227
     * @throws SchemaException
228
     * @throws DBALException
229
     * @throws QueryException
230
     * @throws DriverException
231
     */
232
    public function pushSchema(LoggerInterface $logger = null)
233
    {
234
        $bus = new SynchronizationBus($this->getTables());
235
        $bus->run($logger);
236
    }
237
238
    /**
239
     * Pack declared schemas in a normalized form, make sure to call renderSchema() first.
240
     *
241
     * @return array
242
     *
243
     * @throws SchemaException
244
     */
245
    public function packSchema(): array
246
    {
247
        if (empty($this->tables) && !empty($this->schemas)) {
248
            throw new SchemaException(
249
                "Unable to pack schema, no defined tables were found, call defineTables() first"
250
            );
251
        }
252
253
        $result = [];
254
        foreach ($this->schemas as $class => $schema) {
255
            $result[$class][] = [
256
                ORMInterface::R_INSTANTIATOR => $schema->getInstantiator(),
257
                ORMInterface::R_SCHEMA       => $schema->packSchema($this, null),
258
                ORMInterface::R_SOURCE_CLASS => $this->getSource($class),
259
                ORMInterface::R_DATABASE     => $schema->getDatabase(),
260
                ORMInterface::R_TABLE        => $schema->getTable(),
261
                ORMInterface::R_RELATIONS    => [/*external manager*/]
262
                //relations???
263
            ];
264
        }
265
266
        return $result;
267
    }
268
269
    /**
270
     * Request table schema by name/database combination.
271
     *
272
     * @param string      $table
273
     * @param string|null $database
274
     * @param bool        $resetState When set to true current table state will be reset in order
275
     *                                to
276
     *                                allows model to redefine it's schema.
277
     * @param bool        $unique     Set to true (default), to throw an exception when table
278
     *                                already referenced by another model.
279
     *
280
     * @return AbstractTable          Unlinked.
281
     *
282
     * @throws DoubleReferenceException When two records refers to same table and unique option
283
     *                                  set.
284
     */
285
    protected function requestTable(
286
        string $table,
287
        string $database = null,
288
        bool $unique = true,
289
        bool $resetState = false
290
    ): AbstractTable {
291
        if (isset($this->tables[$database . '.table'])) {
292
            $schema = $this->tables[$database . '.table'];
293
294
            if ($unique) {
295
                throw new DoubleReferenceException(
296
                    "Table '{$table}' of '{$database} 'been requested by multiple models"
297
                );
298
            }
299
        } else {
300
            //Requesting thought DatabaseManager
301
            $schema = $this->manager->database($database)->table($table)->getSchema();
302
            $this->tables[$database . '.' . $table] = $schema;
303
        }
304
305
        $schema = clone $schema;
306
307
        if ($resetState) {
308
            //Emptying our current state (initial not affected)
309
            $schema->setState(null);
310
        }
311
312
        return $schema;
313
    }
314
315
    /**
316
     * Update table state.
317
     *
318
     * @param AbstractTable $table
319
     * @param string|null   $database
320
     */
321
    protected function pushTable(AbstractTable $table, string $database = null)
322
    {
323
        $this->tables[$database . '.' . $table->getName()] = $table;
324
325
    }
326
}