Completed
Pull Request — master (#25)
by David
08:09
created

TDBMSchemaAnalyzer::castSchemaToImmutable()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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