Completed
Push — master ( 741fdb...1d6281 )
by Vitaly
02:51
created

Database::database()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 2
Metric Value
c 3
b 1
f 2
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php declare(strict_types=1);
2
/**
3
 * Created by PhpStorm.
4
 * User: egorov
5
 * Date: 09.05.2015
6
 * Time: 13:05
7
 */
8
namespace samsonframework\orm;
9
10
/**
11
 * Class Database
12
 * @package samsonframework\orm
13
 */
14
class Database implements DatabaseInterface
15
{
16
    /** Table name prefix */
17
    public static $prefix = '';
18
19
    /** @var \PDO Database driver */
20
    protected $driver;
21
22
    /** @var string Database name */
23
    protected $database;
24
25
    /** @var int Amount of milliseconds spent on queries */
26
    protected $elapsed;
27
28
    /** @var int Amount queries executed */
29
    protected $count;
30
31
    /** Do not serialize anything */
32
    public function __sleep()
33
    {
34
        return array();
35
    }
36
37
    /**
38
     * Connect to a database using driver with parameters
39
     * @param string $database Database name
40
     * @param string $username Database username
41
     * @param string $password Database password
42
     * @param string $host Database host(localhost by default)
43
     * @param int $port Database port(3306 by default)
44
     * @param string $driver Database driver for interaction(MySQL by default)
45
     * @param string $charset Database character set
46
     * @return bool True if connection to database was successful
47
     */
48
    public function connect(
49
        $database,
50
        $username,
51
        $password,
52
        $host = 'localhost',
53
        $port = 3306,
54
        $driver = 'mysql',
55
        $charset = 'utf8'
56
    ) {
57
        // If we have not connected yet
58
        if ($this->driver === null) {
59
            $this->database = $database;
60
61
            // Check if configured database exists
62
            $this->driver = new PDO($host, $database, $username, $password, $charset, $port, $driver);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \samsonframework\orm...harset, $port, $driver) of type object<samsonframework\orm\PDO> is incompatible with the declared type object<PDO> of property $driver.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
63
64
            // Set correct encodings
65
            $this->execute("set character_set_client='utf8'");
66
            $this->execute("set character_set_results='utf8'");
67
            $this->execute("set collation_connection='utf8_general_ci'");
68
69
            //new ManagerGenerator($this);
70
        }
71
    }
72
73
    /**
74
     * Get database name
75
     * @return string
76
     * @deprecated
77
     */
78
    public function database()
79
    {
80
        return $this->database;
81
    }
82
83
    /**
84
     * High-level database query executor
85
     * @param string $sql SQL statement
86
     * @return mixed Database query result
87
     * @deprecated Use execute()
88
     */
89
    public function query($sql)
90
    {
91
        return $this->execute($sql);
92
    }
93
94
    /**
95
     * Internal error beautifier.
96
     *
97
     * @param \Exception $exception
98
     * @param            $sql
99
     * @param string     $text
100
     *
101
     * @throws \Exception
102
     */
103
    private function outputError(\Exception $exception, $sql, $text = 'Error executing database query:')
0 ignored issues
show
Unused Code introduced by
The parameter $sql is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $text is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
104
    {
105
        throw $exception;
106
107
        echo("\n" . '<div style="font-size:12px; position:relative; background:red; z-index:9999999;">'
0 ignored issues
show
Unused Code introduced by
echo ' ' . '<div style="... . '</textarea></div>'; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
108
            .'<div style="padding:4px 10px;">'.$text.'</div>'
109
            .'<div style="padding:0px 10px;">['.htmlspecialchars($exception->getMessage()).']</div>'
110
            .'<textarea style="display:block; width:100%; min-height:100px;">'.$sql . '</textarea></div>');
111
    }
112
113
    /**
114
     * Proxy function for executing database fetching logic with exception,
115
     * error, profile handling
116
     * @param callback $fetcher Callback for fetching
117
     * @return mixed Fetching function result
118
     */
119
    private function executeFetcher($fetcher, $sql)
120
    {
121
        $result = array();
122
123
        if (isset($this->driver)) {
124
            // Store timestamp
125
            $tsLast = microtime(true);
126
127
            try { // Call fetcher
128
                // Get argument and remove first one
129
                $args = func_get_args();
130
                array_shift($args);
131
132
                // Proxy calling of fetcher function with passing parameters
133
                $result = call_user_func_array($fetcher, $args);
134
            } catch (\PDOException $exception) {
135
                $this->outputError($exception, $sql, 'Error executing ['.$fetcher[1].']');
136
            }
137
138
            // Store queries count
139
            $this->count++;
140
141
            // Count elapsed time
142
            $this->elapsed += microtime(true) - $tsLast;
143
        }
144
145
        return $result;
146
    }
147
148
    /**
149
     * High-level database query executor
150
     * @param string $sql SQL statement
151
     * @return mixed Database query result
152
     */
153
    private function innerQuery($sql)
154
    {
155
        try {
156
            // Perform database query
157
            return $this->driver->prepare($sql)->execute();
158
        } catch (\PDOException $e) {
159
            $this->outputError($e, $sql);
160
        }
161
162
        return null;
163
    }
164
165
    /**
166
     * Retrieve array of records from a database, if $className is passed method
167
     * will try to create an object of that type. If request has failed than
168
     * method will return empty array of stdClass all arrays regarding to $className is
169
     * passed or not.
170
     *
171
     * @param string $sql SQL statement
172
     * @return array Collection of arrays or objects
173
     */
174
    private function innerFetch($sql, $className = null)
175
    {
176
        try {
177
            // Perform database query
178
            if (!isset($className)) { // Return array
179
                return $this->driver->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
180
            } else { // Create object of passed class name
181
                return $this->driver->query($sql)->fetchAll(\PDO::FETCH_CLASS, $className, array(&$this));
182
            }
183
        } catch (\PDOException $e) {
184
            $this->outputError($e, $sql, 'Fetching database records:');
185
        }
186
187
        return array();
188
    }
189
190
    /**
191
     * Special accelerated function to retrieve db record fields instead of objects
192
     *
193
     * @param string $sql SQL statement
194
     * @param int $columnIndex Needed column index
195
     *
196
     * @return array Database records column value collection
197
     */
198
    private function innerFetchColumn($sql, $columnIndex)
199
    {
200
        try {
201
            // Perform database query
202
            return $this->driver->query($sql)->fetchAll(\PDO::FETCH_COLUMN, $columnIndex);
203
        } catch (\PDOException $e) {
204
            $this->outputError($e, $sql, 'Error fetching records column values:');
205
        }
206
207
        return array();
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function execute(string $sql)
214
    {
215
        return $this->executeFetcher(array($this, 'innerQuery'), $sql);
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function fetch(string $sql)
222
    {
223
        return $this->executeFetcher(array($this, 'innerFetch'), $sql);
224
    }
225
226
    /**
227
     * Special accelerated function to retrieve db record fields instead of objects
228
     * TODO: Change to be independent of query and class name, just SQL, this SQL
229
     * should only have one column in SELECT part and then we do not need parameter
230
     * for this as we can always take 0.
231
     *
232
     * @param string $entity Entity identifier
233
     * @param QueryInterface Query object
234
     * @param string $field Entity field identifier
235
     *
236
     * @return array Collection of rows with field value
237
     * @deprecated
238
     */
239
    public function fetchColumn($entity, QueryInterface $query, $field)
240
    {
241
        // TODO: Remove old attributes retrieval
242
243
        return $this->executeFetcher(
244
            array($this, 'innerFetchColumn'),
245
            $this->prepareSQL($entity, $query),
246
            array_search($field, array_values($entity::$_table_attributes))
247
        );
248
    }
249
250
    /**
251
     * Count resulting rows.
252
     *
253
     * @param string Entity identifier
254
     * @param QueryInterface Query object
255
     *
256
     * @return int Amount of rows
257
     */
258
    public function count($entity, QueryInterface $query)
259
    {
260
        // Modify query SQL and add counting
261
        $result = $this->fetch('SELECT Count(*) as __Count FROM (' . $this->prepareSQL($entity, $query) . ') as __table');
262
263
        return isset($result[0]) ? (int)$result[0]['__Count'] : 0;
264
    }
265
266
    /**
267
     * Quote variable for security reasons.
268
     *
269
     * @param string $value
270
     * @return string Quoted value
271
     */
272
    protected function quote($value)
273
    {
274
        return $this->driver->quote($value);
275
    }
276
277
    /**
278
     * Convert QueryInterface into SQL statement.
279
     *
280
     * @param string Entity identifier
281
     * @param QueryInterface Query object
282
     *
283
     * @return string SQL statement
284
     */
285
    protected function prepareSQL($entity, QueryInterface $query)
0 ignored issues
show
Unused Code introduced by
The parameter $entity is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
286
    {
287
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293
    public function fetchArray(string $sql) : array
0 ignored issues
show
Unused Code introduced by
The parameter $sql is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
294
    {
295
        // TODO: Implement fetchArray() method.
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    public function fetchObjects(string $sql, string $className) : array
302
    {
303
        return $this->toRecords(
0 ignored issues
show
Bug introduced by
The method toRecords() does not seem to exist on object<samsonframework\orm\Database>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
304
            $className,
305
            $this->fetchArray($sql),
306
            $query->join,
0 ignored issues
show
Bug introduced by
The variable $query does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
307
            array_merge($query->own_virtual_fields, $query->virtual_fields)
308
        );
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314
    public function fetchColumns(string $sql, int $columnIndex) : array
315
    {
316
        return $this->executeFetcher(array($this, 'innerFetchColumn'), $sql, $columnIndex);
317
    }
318
319
    /**
320
     * Regroup database rows by primary field value.
321
     *
322
     * @param array  $rows Collection of records received from database
323
     * @param string $primaryField Primary field name for grouping
324
     *
325
     * @return array Grouped rows by primary field value
326
     */
327
    protected function groupResults(array $rows, string $primaryField) : array
328
    {
329
        /** @var array $grouped Collection of database rows grouped by primary field value */
330
        $grouped = [];
331
332
        // Iterate result set
333
        for ($i = 0, $rowsCount = count($rows); $i < $rowsCount; $i++) {
334
            $row = $rows[$i];
335
336
            // Group by primary field value
337
            $grouped[$row[$primaryField]][] = $row;
338
        }
339
340
        return $grouped;
341
    }
342
343
    /**
344
     * Fill entity instance fields from row column values according to entity metadata attributes.
345
     *
346
     * @param mixed $instance Entity instance
347
     * @param array $attributes Metadata entity attributes
348
     * @param array $row        Database results row
349
     */
350
    protected function fillEntityFieldValues($instance, array $attributes, array $row)
351
    {
352
        // Iterate attribute metadata
353
        foreach ($attributes as $alias) {
354
            // If database row has aliased field column
355
            if (array_key_exists($alias, $row)) {
356
                // Store attribute value
357
                $instance->$alias = $row[$alias];
358
            } else {
359
                throw new \InvalidArgumentException('Database row does not have requested column:'.$alias);
360
            }
361
        }
362
363
        // Call handler for object filling
364
        $instance->filled();
365
    }
366
367
    /**
368
     * Create entity instances and its joined entities.
369
     *
370
     * @param array  $rows
371
     * @param string $primaryField
372
     * @param string $className
373
     * @param array  $joinedClassNames
374
     *
375
     * @return array
376
     */
377
    protected function createEntities(array $rows, string $primaryField, string $className, array $joinedClassNames)
378
    {
379
        $objects = [];
380
381
        /** @var array $entityRows Iterate entity rows */
382
        foreach ($this->groupResults($rows, $primaryField) as $primaryValue => $entityRows) {
383
            // Create entity instance
384
            $instance = $objects[$primaryValue] = new $className();
385
386
            // TODO: $attributes argument should be filled with selected fields?
387
            $this->fillEntityFieldValues($instance, $className::$_attributes, $entityRows[0]);
388
389
            // Iterate inner rows for nested entities creation
390
            foreach ($entityRows as $row) {
391
                // Iterate all joined entities
392
                foreach ($joinedClassNames as $joinedClassName) {
393
                    // Create joined instance and add to parent instance
394
                    $joinedInstance = $instance->$joinedClassName[] = new $joinedClassName();
395
396
                    // TODO: We need to change metadata retrieval
397
                    $this->fillEntityFieldValues($joinedInstance, $joinedClassName::$_attributes, $row);
398
                }
399
            }
400
        }
401
402
        return $objects;
403
    }
404
}
405