Completed
Pull Request — master (#13)
by Alexander
02:09
created

Mysql::boundDebugString()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 8
nop 3
dl 0
loc 14
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace alkemann\h2l\data;
4
5
use alkemann\h2l\exceptions\ConnectionError;
6
use PDO;
7
use alkemann\h2l\Log;
8
9
class Mysql implements Source
10
{
11
12
    /**
13
     * @var array
14
     */
15
    protected $config = [];
16
17
    /**
18
     * @var PDO
19
     */
20
    protected $db = null;
21
22
    public function __construct(array $config = [])
23
    {
24
        $defaults = [
25
            'host' => 'localhost',
26
            'db' => 'test',
27
            'user' => null,
28
            'pass' => null
29
        ];
30
        $this->config = $config + $defaults;
31
    }
32
33
    private function handler() //: PDO
34
    {
35
        if ($this->db) {
36
            return $this->db;
37
        }
38
39
        $host = $this->config['host'];
40
        $db = $this->config['db'];
41
        $user = $this->config['user'];
42
        $pass = $this->config['pass'];
43
        $opts = [
44
            PDO::ATTR_EMULATE_PREPARES => false,
45
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
46
        ];
47
        try {
48
            $this->db = new PDO("mysql:host={$host};dbname={$db}", $user, $pass, $opts);
49
            // @TODO use this?
1 ignored issue
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
50
            // $this->db->setAttribute( PDO::ATTR_EMULATE_PREPARES, false);
1 ignored issue
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
51
        } catch (\PDOException $e) {
52
            throw new ConnectionError("Unable to connect to $host : $db with user $user");
1 ignored issue
show
Unused Code introduced by
The call to ConnectionError::__construct() has too many arguments starting with "Unable to connect to {$...$db} with user {$user}".

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
53
        }
54
        return $this->db;
55
    }
56
57
    public function query($query, array $params = [])
58
    {
59
        Log::debug("PDO:QUERY [$query]");
60
        $result = $this->handler()->query($query);
61
        return $result->fetchAll(PDO::FETCH_ASSOC);
62
    }
63
64
    public function one(string $table, array $conditions, array $options = []): ?array
65
    {
66
        $result = $this->find($table, $conditions, $options);
67
        $result = iterator_to_array($result);
68
        $hits = sizeof($result);
69
        if ($hits === 0) {
70
            return null;
71
        }
72
        if ($hits > 1) {
73
            throw new \Error("One request found more than 1 match!");
74
        }
75
76
        return $result[0];
77
    }
78
79
    public function find(string $table, array $conditions, array $options = []): iterable
80
    {
81
        $where = $this->where($conditions);
82
        $limit = $this->limit($options);
83
        $query = "SELECT * FROM `{$table}` {$where}{$limit};";
84
        $params = $this->boundDebugString($conditions, $options);
85
        Log::debug("PDO:QUERY [$query][$params]");
86
        $dbh = $this->handler();
87
        $stmt = $dbh->prepare($query);
88
        foreach ($conditions as $key => $value) {
89
            $stmt->bindValue(":c_{$key}", $value);
90
        }
91
        $this->bindPaginationToStatement($options, $stmt);
92
        $result = $stmt->execute();
93
        if ($result === false) {
94
            return new \EmptyIterator;
95
        }
96
        // @codeCoverageIgnoreStart
97
        if ($stmt instanceof \PDOStatement) {
98
            $stmt->setFetchMode(PDO::FETCH_ASSOC);
99
        }
100
        // @codeCoverageIgnoreEnd
101
        return $stmt;
102
    }
103
104
    private function where(array $conditions): string
105
    {
106
        if (empty($conditions)) {
107
            return "";
108
        }
109
        $fun = function ($o, $v) {
110
            return "{$o}{$v} = :c_{$v}";
111
        };
112
        $where = array_reduce(array_keys($conditions), $fun, "");
113
        return "WHERE {$where} ";
114
    }
115
116
    private function limit(array $options): string
117
    {
118
        return array_key_exists('limit', $options) ? "LIMIT :o_offset,:o_limit " : '';
119
    }
120
121
    private function boundDebugString(array $conditions, array $options, array $data = []): string
122
    {
123
        $out = [];
124
        foreach ($conditions as $k => $v) {
125
            $out[] = "c_{$k}:'{$v}'";
126
        }
127
        foreach ($data as $k => $v) {
128
            $out[] = "d_{$k}:{$v}";
129
        }
130
        foreach ($options as $k => $v) {
131
            $out[] = "o_{$k}:{$v}";
132
        }
133
        return join(", ", $out);
134
    }
135
136
    private function bindPaginationToStatement(array $options, $stmt): void
137
    {
138
        if (array_key_exists('limit', $options)) {
139
            $stmt->bindValue(":o_offset", $options['offset'] ?? 0, PDO::PARAM_INT);
140
            $stmt->bindValue(":o_limit", $options['limit'], PDO::PARAM_INT);
141
        }
142
    }
143
144
    public function update(string $table, array $conditions, array $data, array $options = []): int
145
    {
146
        if (empty($conditions) || empty($data)) {
147
            return 0;
148
        }
149
150
        $datasql = $this->data($data);
151
        $where = $this->where($conditions);
152
        $query = "UPDATE `{$table}` SET {$datasql} {$where};";
153
154
        $params = $this->boundDebugString($conditions, $options, $data);
155
        Log::debug("PDO:QUERY [$query][$params]");
156
        $dbh = $this->handler();
157
        $stmt = $dbh->prepare($query);
158
        foreach ($data as $key => $value) {
159
            $stmt->bindValue(":d_{$key}", $value);
160
        }
161
        foreach ($conditions as $key => $value) {
162
            $stmt->bindValue(":c_{$key}", $value);
163
        }
164
        $result = $stmt->execute();
165
        return ($result === true) ? $stmt->rowCount() : 0;
166
    }
167
168
    private function data(array $data): string
169
    {
170
        $fun = function ($o, $v) {
171
            return "{$o}{$v} = :d_{$v}";
172
        };
173
        return array_reduce(array_keys($data), $fun, "");
174
    }
175
176
    public function insert(string $table, array $data, array $options = []): ?int
177
    {
178
        $keys = implode(', ', array_keys($data));
179
        $data_phs = ':d_' . implode(', :d_', array_keys($data));
180
        $query = "INSERT INTO `{$table}` ({$keys}) VALUES ({$data_phs});";
181
        $params = $this->boundDebugString([], [], $data);
182
        Log::debug("PDO:QUERY [$query][$params]");
183
        $dbh = $this->handler();
184
        $stmt = $dbh->prepare($query);
185
        foreach ($data as $key => $value) {
186
            $stmt->bindValue(':d_' . $key, $value);
187
        }
188
        $result = $stmt->execute();
189
        return ($result === true) ? $dbh->lastInsertId() : null;
190
    }
191
192
    public function delete(string $table, array $conditions, array $options = []): int
193
    {
194
        $where = $this->where($conditions);
195
        if (empty($where)) {
196
            return 0;
197
        }
198
        $limit = $this->limit($options);
199
        $query = "DELETE FROM `{$table}` {$where}{$limit};";
200
        $params = $this->boundDebugString($conditions, $options);
201
        Log::debug("PDO:QUERY [$query][$params]");
202
        $dbh = $this->handler();
203
        $stmt = $dbh->prepare($query);
204
        foreach ($conditions as $key => $value) {
205
            $stmt->bindValue(":c_{$key}", $value);
206
        }
207
        $this->bindPaginationToStatement($options, $stmt);
208
        $result = $stmt->execute();
209
        return ($result === true) ? $stmt->rowCount() : 0;
210
    }
211
}
212