Passed
Pull Request — master (#171)
by ARP
03:17
created

TDBMSchemaAnalyzer::getSchema()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 19
rs 9.7998
cc 4
nc 4
nop 0
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM;
5
6
use BrainDiminished\SchemaVersionControl\SchemaVersionControlService;
7
use Doctrine\Common\Cache\Cache;
8
use Doctrine\DBAL\Connection;
9
use Doctrine\DBAL\Schema\Column;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use Doctrine\DBAL\Schema\Schema;
12
use Doctrine\DBAL\Schema\Table;
13
use Doctrine\DBAL\Types\DateType;
14
use Doctrine\DBAL\Types\Type;
15
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
16
use TheCodingMachine\TDBM\Utils\ImmutableCaster;
17
use TheCodingMachine\TDBM\Utils\RootProjectLocator;
18
19
/**
20
 * This class is used to analyze the schema and return valuable information / hints.
21
 */
22
class TDBMSchemaAnalyzer
23
{
24
    const schemaFileName =  'tdbm.lock.yml';
25
26
    private $connection;
27
28
    /**
29
     * @var Schema
30
     */
31
    private $schema;
32
33
    /**
34
     * @var string
35
     */
36
    private $cachePrefix;
37
38
    /**
39
     * @var Cache
40
     */
41
    private $cache;
42
43
    /**
44
     * @var SchemaAnalyzer
45
     */
46
    private $schemaAnalyzer;
47
48
    /**
49
     * @var SchemaVersionControlService
50
     */
51
    private $schemaVersionControlService;
52
53
    /**
54
     * @param Connection     $connection     The DBAL DB connection to use
55
     * @param Cache          $cache          A cache service to be used
56
     * @param SchemaAnalyzer $schemaAnalyzer The schema analyzer that will be used to find shortest paths...
57
     *                                       Will be automatically created if not passed
58
     */
59
    public function __construct(Connection $connection, Cache $cache, SchemaAnalyzer $schemaAnalyzer)
60
    {
61
        $this->connection = $connection;
62
        $this->cache = $cache;
63
        $this->schemaAnalyzer = $schemaAnalyzer;
64
        $this->schemaVersionControlService = new SchemaVersionControlService($this->connection, self::getLockFilePath());
65
    }
66
67
    /**
68
     * Returns a unique ID for the current connection. Useful for namespacing cache entries in the current connection.
69
     *
70
     * @return string
71
     */
72
    public function getCachePrefix(): string
73
    {
74
        if ($this->cachePrefix === null) {
75
            $this->cachePrefix = hash('md4', $this->connection->getHost().'-'.$this->connection->getPort().'-'.$this->connection->getDatabase().'-'.$this->connection->getDriver()->getName());
76
        }
77
78
        return $this->cachePrefix;
79
    }
80
81
    public static function getLockFilePath(): string
82
    {
83
        return RootProjectLocator::getRootLocationPath().self::schemaFileName;
84
    }
85
86
    /**
87
     * Returns the (cached) schema.
88
     *
89
     * @return Schema
90
     */
91
    public function getSchema(): Schema
92
    {
93
        if ($this->schema === null) {
94
            $cacheKey = $this->getCachePrefix().'_immutable_schema';
95
            if ($this->cache->contains($cacheKey)) {
96
                $this->schema = $this->cache->fetch($cacheKey);
97
            } elseif (file_exists(self::getLockFilePath())) {
98
                $this->schema = $this->schemaVersionControlService->loadSchemaFile();
99
                ImmutableCaster::castSchemaToImmutable($this->schema);
100
                $this->cache->save($cacheKey, $this->schema);
101
            } else {
102
                $this->schema = $this->connection->getSchemaManager()->createSchema();
103
                $this->generateLockFile();
104
                ImmutableCaster::castSchemaToImmutable($this->schema);
105
                $this->cache->save($cacheKey, $this->schema);
106
            }
107
        }
108
109
        return $this->schema;
110
    }
111
112
    public function generateLockFile(): void
113
    {
114
        $this->schemaVersionControlService->dumpSchema();
115
    }
116
117
    /**
118
     * Returns the list of pivot tables linked to table $tableName.
119
     *
120
     * @param string $tableName
121
     *
122
     * @return string[]
123
     */
124
    public function getPivotTableLinkedToTable(string $tableName): array
125
    {
126
        $cacheKey = $this->getCachePrefix().'_pivottables_link_'.$tableName;
127
        if ($this->cache->contains($cacheKey)) {
128
            return $this->cache->fetch($cacheKey);
129
        }
130
131
        $pivotTables = [];
132
133
        $junctionTables = $this->schemaAnalyzer->detectJunctionTables(true);
134
        foreach ($junctionTables as $table) {
135
            $fks = $table->getForeignKeys();
136
            foreach ($fks as $fk) {
137
                if ($fk->getForeignTableName() == $tableName) {
138
                    $pivotTables[] = $table->getName();
139
                    break;
140
                }
141
            }
142
        }
143
144
        $this->cache->save($cacheKey, $pivotTables);
145
146
        return $pivotTables;
147
    }
148
149
    /**
150
     * Returns the list of foreign keys pointing to the table represented by this bean, excluding foreign keys
151
     * from junction tables and from inheritance.
152
     * It will also suppress doubles if 2 foreign keys are using the same columns.
153
     *
154
     * @return ForeignKeyConstraint[]
155
     */
156
    public function getIncomingForeignKeys(string $tableName): array
157
    {
158
        $junctionTables = $this->schemaAnalyzer->detectJunctionTables(true);
159
        $junctionTableNames = array_map(function (Table $table) {
160
            return $table->getName();
161
        }, $junctionTables);
162
        $childrenRelationships = $this->schemaAnalyzer->getChildrenRelationships($tableName);
163
164
        $fks = [];
165
        foreach ($this->getSchema()->getTables() as $table) {
166
            $uniqueForeignKeys = $this->removeDuplicates($table->getForeignKeys());
167
            foreach ($uniqueForeignKeys as $fk) {
168
                if ($fk->getForeignTableName() === $tableName) {
169
                    if (in_array($fk->getLocalTableName(), $junctionTableNames)) {
170
                        continue;
171
                    }
172
                    foreach ($childrenRelationships as $childFk) {
173
                        if ($fk->getLocalTableName() === $childFk->getLocalTableName() && $fk->getUnquotedLocalColumns() === $childFk->getUnquotedLocalColumns()) {
174
                            continue 2;
175
                        }
176
                    }
177
                    $fks[] = $fk;
178
                }
179
            }
180
        }
181
182
        return $fks;
183
    }
184
185
    /**
186
     * Remove duplicate foreign keys (assumes that all foreign yes are from the same local table)
187
     *
188
     * @param ForeignKeyConstraint[] $foreignKeys
189
     * @return ForeignKeyConstraint[]
190
     */
191
    private function removeDuplicates(array $foreignKeys): array
192
    {
193
        $fks = [];
194
        foreach ($foreignKeys as $foreignKey) {
195
            $key = implode('__`__', $foreignKey->getUnquotedLocalColumns());
196
            if (!isset($fks[$key])) {
197
                $fks[$key] = $foreignKey;
198
            }
199
        }
200
201
        return array_values($fks);
202
    }
203
}
204