Completed
Push — master ( 407844...9daecf )
by Ivan
02:25
created

Schema::tables()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace vakata\database\driver\postgre;
4
5
use \vakata\database\DBException;
6
use \vakata\database\DriverInterface;
7
use \vakata\database\DriverAbstract;
8
use \vakata\database\StatementInterface;
9
use \vakata\database\schema\Table;
10
use \vakata\database\schema\TableRelation;
11
use \vakata\collection\Collection;
12
13
trait Schema
14
{
15
    public function table(string $table, bool $detectRelations = true) : Table
16
    {
17
        static $tables = [];
18
        if (isset($tables[$table])) {
19
            return $tables[$table];
20
        }
21
22
        static $relationsT = null;
23
        static $relationsR = null;
24
        if (!isset($relationsT) || !isset($relationsR)) {
25
            $relationsT = [];
26
            $relationsR = [];
27
            $col = Collection::from(
28
                $this->query(
0 ignored issues
show
Bug introduced by
It seems like query() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
29
                    "SELECT
30
                        kc.table_name,
31
                        kc.column_name,
32
                        kc.constraint_name,
33
                        ct.table_name AS referenced_table_name,
34
                        (SELECT column_name
35
                         FROM information_schema.constraint_column_usage
36
                         WHERE constraint_name = kc.constraint_name AND table_name = ct.table_name
37
                         LIMIT 1 OFFSET kc.position_in_unique_constraint - 1
38
                        ) AS referenced_column_name
39
                     FROM information_schema.key_column_usage kc
40
                     JOIN information_schema.constraint_table_usage ct ON kc.constraint_name = ct.constraint_name AND ct.table_schema = kc.table_schema
41
                     WHERE
42
                        kc.table_schema = ? AND kc.table_name IS NOT NULL AND kc.position_in_unique_constraint IS NOT NULL",
43
                    [ $this->connection['opts']['schema'] ?? $this->connection['name'] ]
0 ignored issues
show
Bug introduced by
The property connection does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
44
                )
45
            )->toArray();
46
            foreach ($col as $row) {
47
                $relationsT[$row['table_name']][] = $row;
48
                $relationsR[$row['referenced_table_name']][] = $row;
49
            }
50
        }
51
52
        $columns = Collection::from($this
0 ignored issues
show
Bug introduced by
It seems like query() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
53
            ->query(
54
                "SELECT * FROM information_schema.columns WHERE table_name = ? AND table_schema = ?",
55
                [ $table, $this->connection['opts']['schema'] ?? $this->connection['name'] ]
56
            ))
57
            ->mapKey(function ($v) { return $v['column_name']; })
58
            ->map(function ($v) {
59
                $v['length'] = null;
60
                if (!isset($v['data_type'])) {
61
                    return $v;
62
                }
63
                switch ($v['data_type']) {
64
                    case 'character':
65
                    case 'character varying':
66
                        $v['length'] = (int)$v['character_maximum_length'];
67
                        break;
68
                }
69
                return $v;
70
            })
71
            ->toArray();
72
        if (!count($columns)) {
73
            throw new DBException('Table not found by name');
74
        }
75
        $pkname = Collection::from($this
0 ignored issues
show
Bug introduced by
It seems like query() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
76
            ->query(
77
                "SELECT constraint_name FROM information_schema.table_constraints
78
                WHERE table_name = ? AND constraint_type = ? AND table_schema = ?",
79
                [ $table, 'PRIMARY KEY', $this->connection['opts']['schema'] ?? $this->connection['name'] ]
80
            ))
81
            ->pluck('constraint_name')
82
            ->value();
83
        $primary = [];
84
        if ($pkname) {
85
            $primary = Collection::from($this
0 ignored issues
show
Bug introduced by
It seems like query() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
86
                ->query(
87
                    "SELECT column_name FROM information_schema.constraint_column_usage
88
                     WHERE table_name = ? AND constraint_name = ? AND table_schema = ?",
89
                    [ $table, $pkname, $this->connection['opts']['schema'] ?? $this->connection['name'] ]
90
                ))
91
                ->pluck('column_name')
92
                ->toArray();
93
        }
94
        $tables[$table] = $definition = (new Table($table))
95
            ->addColumns($columns)
96
            ->setPrimaryKey($primary)
97
            ->setComment('');
98
99
        if ($detectRelations) {
100
            // relations where the current table is referenced
101
            // assuming current table is on the "one" end having "many" records in the referencing table
102
            // resulting in a "hasMany" or "manyToMany" relationship (if a pivot table is detected)
103
            $relations = [];
104
            foreach ($relationsR[$table] ?? [] as $relation) {
105
                $relations[$relation['constraint_name']]['table'] = $relation['table_name'];
106
                $relations[$relation['constraint_name']]['keymap'][$relation['referenced_column_name']] = $relation['column_name'];
107
            }
108
            foreach ($relations as $data) {
109
                $rtable = $this->table($data['table'], true);
110
                $columns = [];
111
                foreach ($rtable->getColumns() as $column) {
112
                    if (!in_array($column, $data['keymap'])) {
113
                        $columns[] = $column;
114
                    }
115
                }
116
                $foreign = [];
117
                $usedcol = [];
118
                if (count($columns)) {
119
                    foreach (Collection::from($relationsT[$data['table']] ?? [])
120
                        ->filter(function ($v) use ($columns) {
121
                            return in_array($v['column_name'], $columns);
122
                        }) as $relation
123
                    ) {
124
                        $foreign[$relation['constraint_name']]['table'] = $relation['referenced_table_name'];
125
                        $foreign[$relation['constraint_name']]['keymap'][$relation['column_name']] = $relation['referenced_column_name'];
126
                        $usedcol[] = $relation['column_name'];
127
                    }
128
                }
129
                if (count($foreign) === 1 && !count(array_diff($columns, $usedcol))) {
130
                    $foreign = current($foreign);
131
                    $relname = $foreign['table'];
132
                    $cntr = 1;
133
                    while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
134
                        $relname = $foreign['table'] . '_' . (++ $cntr);
135
                    }
136
                    $definition->addRelation(
137
                        new TableRelation(
138
                            $relname,
139
                            $this->table($foreign['table'], true),
140
                            $data['keymap'],
141
                            true,
142
                            $rtable,
143
                            $foreign['keymap']
144
                        )
145
                    );
146
                } else {
147
                    $relname = $data['table'];
148
                    $cntr = 1;
149
                    while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
150
                        $relname = $data['table'] . '_' . (++ $cntr);
151
                    }
152
                    $definition->addRelation(
153
                        new TableRelation(
154
                            $relname,
155
                            $this->table($data['table'], true),
156
                            $data['keymap'],
157
                            true
158
                        )
159
                    );
160
                }
161
            }
162
            // relations where the current table references another table
163
            // assuming current table is linked to "one" record in the referenced table
164
            // resulting in a "belongsTo" relationship
165
            $relations = [];
166
            foreach ($relationsT[$table] ?? [] as $relation) {
167
                $relations[$relation['constraint_name']]['table'] = $relation['referenced_table_name'];
168
                $relations[$relation['constraint_name']]['keymap'][$relation['column_name']] = $relation['referenced_column_name'];
169
            }
170
            foreach ($relations as $name => $data) {
171
                $relname = $data['table'];
172
                $cntr = 1;
173
                while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
174
                    $relname = $data['table'] . '_' . (++ $cntr);
175
                }
176
                $definition->addRelation(
177
                    new TableRelation(
178
                        $relname,
179
                        $this->table($data['table'], true),
180
                        $data['keymap'],
181
                        false
182
                    )
183
                );
184
            }
185
        }
186
        return $definition->toLowerCase();
187
    }
188
    public function tables() : array
189
    {
190
        return Collection::from($this
0 ignored issues
show
Bug introduced by
It seems like query() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
191
            ->query(
192
                "SELECT table_name FROM information_schema.tables where table_schema = ?",
193
                [ $this->connection['opts']['schema'] ?? $this->connection['name'] ]
194
            ))
195
            ->pluck('table_name')
196
            ->map(function ($v) {
197
                return $this->table($v);
198
            })
199
            ->toArray();
200
    }
201
}