Passed
Branch master (77dc06)
by Lawrence
02:42
created

PDO::__call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 8
ccs 3
cts 3
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
/*
5
 +-----------------------------------------------------------------------------+
6
 | PHPPackage - PDO Wrapper
7
 +-----------------------------------------------------------------------------+
8
 | Copyright (c)2018 (http://github.com/phppackage/pdo-wrapper)
9
 +-----------------------------------------------------------------------------+
10
 | This source file is subject to MIT License
11
 | that is bundled with this package in the file LICENSE.
12
 |
13
 | If you did not receive a copy of the license and are unable to
14
 | obtain it through the world-wide-web, please send an email
15
 | to [email protected] so we can send you a copy immediately.
16
 +-----------------------------------------------------------------------------+
17
 | Authors:
18
 |   Your Name <[email protected]>
19
 +-----------------------------------------------------------------------------+
20
 */
21
22
namespace PHPPackage\PDOWrapper;
23
24
class PDO extends \PDO
25
{
26
    /**
27
     * @var construct arguments
0 ignored issues
show
Bug introduced by
The type PHPPackage\PDOWrapper\construct was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
     */
29
    private $dsn;
30
    private $username;
31
    private $password;
32
33
    /**
34
     * @var array Default options for database connection.
35
     */
36
    private $options = array(
37
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
38
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
39
        PDO::ATTR_EMULATE_PREPARES   => false
40
    );
41
42
    /**
43
     * @var array PDO attribute keys.
44
     */
45
    private $attributes = array(
46
        'AUTOCOMMIT', 'ERRMODE', 'CASE', 'CLIENT_VERSION', 'CONNECTION_STATUS',
47
        'ORACLE_NULLS', 'PERSISTENT', 'PREFETCH', 'SERVER_INFO', 'SERVER_VERSION',
48
        'TIMEOUT', 'DRIVER_NAME'
49
    );
50
51
    /**
52
     * PDO construct, defaults to tmp sqlite file if no arguments are passed.
53
     *
54
     * @param string $dsn
55
     * @param string $username
56
     * @param string $password
57
     * @param array  $options
58
     */
59 10
    public function __construct(
60
        string $dsn = null,
61
        string $username = null,
62
        string $password = null,
63
        array  $options = []
64
    ) {
65 10
        $this->dsn = $dsn;
0 ignored issues
show
Documentation Bug introduced by
It seems like $dsn can also be of type string. However, the property $dsn is declared as type PHPPackage\PDOWrapper\construct. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
66 10
        $this->username = $username;
67 10
        $this->password = $password;
68 10
        $this->options = $options+$this->options;
69
70 10
        if (is_null($this->dsn)) {
71 9
            $this->dsn = 'sqlite:/'.sys_get_temp_dir().'/PDOWrapper.db';
72
        }
73
74 10
        parent::__construct($this->dsn, $this->username, $this->password, $this->options);
75 10
    }
76
77
    /**
78
     * Get database name from dsn
79
     *
80
     * @throws RuntimeException
81
     * @return string
82
     */
83 2
    public function getDatabaseName(): string
84
    {
85
        // match database from dsn & set working vars
86 2
        if (!preg_match('/dbname=(\w+);/', $this->dsn, $results)) {
87 1
            throw new \RuntimeException('Could not match database name from dsn');
88
        }
89
90 1
        return $results[1];
91
    }
92
93
    /**
94
     * Create database
95
     *
96
     * @return bool
97
     */
98 1
    public function createDatabase($name): bool
99
    {
100 1
        if (!in_array($name, $this->databases())) {
101 1
            return (bool) $this->exec("
102 1
                CREATE DATABASE `$name`;
103 1
                CREATE USER '{$this->username}'@'%' IDENTIFIED BY '{$this->password}';
104 1
                GRANT ALL ON `$name`.* TO '{$this->username}'@'localhost';
105
                FLUSH PRIVILEGES;
106
            ");
107
        }
108 1
        return false;
109
    }
110
111
    /**
112
     * Enumarate PDO attributes
113
     *
114
     * @param string $key Pick out a attribute by key
115
     * @return mixed
116
     */
117 2
    public function info(string $key = null)
118
    {
119 2
        $return = [];
120 2
        foreach ($this->attributes as $value) {
121
            try {
122 2
                $return['PDO::ATTR_'.$value] = $this->getAttribute(constant('PDO::ATTR_'.$value));
123 2
            } catch (\PDOException $e) {
124 2
                $return['PDO::ATTR_'.$value] = null;
125
            }
126
        }
127 2
        return (!is_null($key) && isset($return[$key])) ? $return[$key] : $return;
128
    }
129
130
    /**
131
     * Returns an array of databases
132
     * @return mixed
133
     */
134 2
    public function databases(): array
135
    {
136 2
        $stmt = $this->query("SHOW DATABASES");
137
138 1
        $result = [];
139 1
        while ($row = $stmt->fetchColumn(0)) {
140 1
            $result[] = $row;
141
        }
142
143 1
        return $result;
144
    }
145
146
    /**
147
     * Returns an array of tables
148
     * @return array
149
     */
150 1
    public function tables(): array
151
    {
152 1
        $stmt = $this->query("SHOW TABLES");
153
154 1
        $result = [];
155 1
        while ($row = $stmt->fetchColumn(0)) {
156 1
            $result[] = $row;
157
        }
158
159 1
        return $result;
160
    }
161
162
    /**
163
     * Run query and return PDOStatement
164
     *
165
     * @param string $sql
166
     * @param array  $values
167
     * @throws InvalidArgumentException
168
     * @return mixed
169
     */
170 4
    public function run(string $sql, array $values = [])
171
    {
172 4
        if (empty($sql)) {
173 1
            throw new \InvalidArgumentException('1st argument cannot be empty');
174
        }
175
176 4
        if (empty($values)) {
177 3
            return $this->query($sql);
178
        }
179
180 3
        if (!is_array($values[0])) {
181 3
            $stmt = $this->prepare($sql);
182 3
            $stmt->execute($values);
183 3
            return $stmt;
184
        }
185
186 1
        return $this->multi($sql, $values);
187
    }
188
189
    /**
190
     * Execute multiple querys which returns row count
191
     *
192
     * @param string $sql
193
     * @param array  $values
194
     * @throws InvalidArgumentException
195
     * @return int
196
     */
197 2
    public function multi($sql, $values = []): int
198
    {
199 2
        if (empty($sql)) {
200 1
            throw new \InvalidArgumentException('1st argument cannot be empty');
201
        }
202
203 2
        if (empty($values[0]) || !is_array($values[0])) {
204 1
            throw new \InvalidArgumentException('2nd argument must be an array of arrays');
205
        }
206
207 2
        $stmt = $this->prepare($sql);
208
209 2
        $row_count = 0;
210 2
        foreach ($values as $value) {
211 2
            $stmt->execute($value);
212 2
            $row_count += $stmt->rowCount();
213
        }
214
215 2
        return $row_count;
216
    }
217
218
    /**
219
     * Quick queries
220
     * Allows you to run a query without chaining the return type manually. This allows for slightly shorter syntax.
221
     */
222
223 1
    public function row($query, $values = array()): array
224
    {
225 1
        return $this->run($query, $values)->fetch();
226
    }
227
228 1
    public function cell($query, $values = array()): string
229
    {
230 1
        return $this->run($query, $values)->fetchColumn();
231
    }
232
233 1
    public function all($query, $values = array()): array
234
    {
235 1
        return $this->run($query, $values)->fetchAll();
236
    }
237
238
    /**
239
     * Checks import/export system requirements.
240
     *  - Supports only mySQL
241
     *
242
     * @param string $method
243
     * @throws RuntimeException
244
     */
245 5
    private function checkImportExportRequirements(string $method)
246
    {
247 5
        if ($this->info('DRIVER_NAME') !== 'mysql') {
248 5
            new \RuntimeException('Driver not supported for '.$method.'()');
249
        }
250
251 5
        if (!function_exists('shell_exec')) {
252 1
            new \RuntimeException('shell_exec must be enabled for '.$method.'()');
253
        }
254
255 5
        if (empty(shell_exec('which gzip'))) {
256 1
            new \RuntimeException('gzip must be installed to use '.$method.'()');
257
        }
258
259 5
        if (empty(shell_exec('which zcat'))) {
260 1
            new \RuntimeException('zcat must be installed to use '.$method.'()');
261
        }
262
263 5
        if (empty(shell_exec('which mysqldump'))) {
264 1
            new \RuntimeException('mysqldump must be installed to use '.$method.'()');
265
        }
266 5
    }
267
268
    /**
269
     * Import database (using )
270
     *
271
     * @param string $file
272
     * @param bool   $backup Do backup before import
273
     * @throws RuntimeException
274
     * @return bool
275
     */
276 2
    public function import(string $file, $backup = true)
277
    {
278 2
        $this->checkImportExportRequirements('import');
279
280 2
        if (!file_exists($file)) {
281 1
            throw new \DomainException('Import file does not exist');
282
        }
283
284
        // set working vars
285 1
        $database = $this->getDatabaseName();
286 1
        $date = date_create()->format('Y-m-d_H:i:s');
0 ignored issues
show
Unused Code introduced by
The assignment to $date is dead and can be removed.
Loading history...
287 1
        $dir = dirname($file);
288
289
        // backup current
290 1
        if ($backup) {
291 1
            $this->export($dir);
292
        }
293
294
        // restore
295 1
        `zcat {$dir}/{$file} | mysql --user={$this->username} --password={$this->password} {$database}`;
296
297 1
        return true;
298
    }
299
300
    /**
301
     * Export database using mysqldump
302
     *
303
     * @param string $destination Directory to store database exports
304
     * @throws DomainException
305
     * @return string
306
     */
307 3
    public function export($destination = './'): string
308
    {
309 3
        $this->checkImportExportRequirements('export');
310
311 3
        if (!is_dir($destination)) {
312 1
            throw new \DomainException('Export destination must be a directory');
313
        }
314
315
        // set working vars
316 2
        $database = $this->getDatabaseName();
317 2
        $date = date_create()->format('Y-m-d_H:i:s');
318 2
        $destination = rtrim($destination, '/');
319
320 2
        `mysqldump --add-drop-table --user={$this->username} --password={$this->password} --host=127.0.0.1 {$database} | gzip > {$destination}/{$date}.sql.gz &`;
321
322 2
        return $destination.'/'.$date.'.sql.gz';
323
    }
324
325
326
    /**
327
     * Magic caller, so can return a BadMethodCallException
328
     *
329
     * @param string $method
330
     * @param array  $arguments
331
     * @throws BadMethodCallException
332
     * @return mixed
333
     */
334 1
    public function __call($method, $arguments)
335
    {
336 1
        if (!method_exists($this, $method)) {
337 1
            throw new \BadMethodCallException('Call to undefined method '.__CLASS__.'::'.$method.'()');
338
        }
339
340
        // @codeCoverageIgnoreStart
341
        return $this->{$method}(...$arguments);
342
        // @codeCoverageIgnoreEnd
343
    }
344
}
345