Completed
Push — develop ( 7610d7...67789b )
by John
03:16
created

ActiveRecordProviderMySQL::backupDatabase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Alpha\Model;
4
5
use Alpha\Model\Type\Integer;
6
use Alpha\Model\Type\Timestamp;
7
use Alpha\Model\Type\DEnum;
8
use Alpha\Model\Type\Relation;
9
use Alpha\Model\Type\RelationLookup;
10
use Alpha\Model\Type\Double;
11
use Alpha\Model\Type\Text;
12
use Alpha\Model\Type\SmallText;
13
use Alpha\Model\Type\Date;
14
use Alpha\Model\Type\Enum;
15
use Alpha\Model\Type\Boolean;
16
use Alpha\Util\Config\ConfigProvider;
17
use Alpha\Util\Logging\Logger;
18
use Alpha\Util\Helper\Validator;
19
use Alpha\Util\Service\ServiceFactory;
20
use Alpha\Exception\AlphaException;
21
use Alpha\Exception\FailedSaveException;
22
use Alpha\Exception\FailedDeleteException;
23
use Alpha\Exception\FailedIndexCreateException;
24
use Alpha\Exception\LockingException;
25
use Alpha\Exception\ValidationException;
26
use Alpha\Exception\CustomQueryException;
27
use Alpha\Exception\RecordNotFoundException;
28
use Alpha\Exception\BadTableNameException;
29
use Alpha\Exception\ResourceNotAllowedException;
30
use Alpha\Exception\IllegalArguementException;
31
use Alpha\Exception\PHPException;
32
use Exception;
33
use ReflectionClass;
34
use Mysqli;
35
36
/**
37
 * MySQL active record provider (uses the MySQLi native API in PHP).
38
 *
39
 * @since 1.1
40
 *
41
 * @author John Collins <[email protected]>
42
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
43
 * @copyright Copyright (c) 2017, John Collins (founder of Alpha Framework).
44
 * All rights reserved.
45
 *
46
 * <pre>
47
 * Redistribution and use in source and binary forms, with or
48
 * without modification, are permitted provided that the
49
 * following conditions are met:
50
 *
51
 * * Redistributions of source code must retain the above
52
 *   copyright notice, this list of conditions and the
53
 *   following disclaimer.
54
 * * Redistributions in binary form must reproduce the above
55
 *   copyright notice, this list of conditions and the
56
 *   following disclaimer in the documentation and/or other
57
 *   materials provided with the distribution.
58
 * * Neither the name of the Alpha Framework nor the names
59
 *   of its contributors may be used to endorse or promote
60
 *   products derived from this software without specific
61
 *   prior written permission.
62
 *
63
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
64
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
65
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
66
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
67
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
68
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
69
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
70
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
71
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
72
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
73
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
74
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
75
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
76
 * </pre>
77
 */
78
class ActiveRecordProviderMySQL implements ActiveRecordProviderInterface
79
{
80
    /**
81
     * Trace logger.
82
     *
83
     * @var \Alpha\Util\Logging\Logger
84
     *
85
     * @since 1.1
86
     */
87
    private static $logger = null;
88
89
    /**
90
     * Datebase connection.
91
     *
92
     * @var Mysqli
93
     *
94
     * @since 1.1
95
     */
96
    private static $connection;
97
98
    /**
99
     * The business object that we are mapping back to.
100
     *
101
     * @var \Alpha\Model\ActiveRecord
102
     *
103
     * @since 1.1
104
     */
105
    private $record;
106
107
    /**
108
     * The constructor.
109
     *
110
     * @since 1.1
111
     */
112
    public function __construct()
113
    {
114
        self::$logger = new Logger('ActiveRecordProviderMySQL');
115
        self::$logger->debug('>>__construct()');
116
117
        self::$logger->debug('<<__construct');
118
    }
119
120
    /**
121
     * (non-PHPdoc).
122
     *
123
     * @see Alpha\Model\ActiveRecordProviderInterface::getConnection()
124
     */
125
    public static function getConnection()
126
    {
127
        $config = ConfigProvider::getInstance();
128
129
        if (!isset(self::$connection)) {
130
            try {
131
                self::$connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'), $config->get('db.name'));
132
            } catch (\Exception $e) {
133
                // if we failed to connect because the database does not exist, create it and try again
134
                if (strpos($e->getMessage(), 'HY000/1049') !== false) {
135
                    self::createDatabase();
136
                    self::$connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'), $config->get('db.name'));
137
                }
138
            }
139
140
            self::$connection->set_charset('utf8');
141
142
            if (mysqli_connect_error()) {
143
                self::$logger->fatal('Could not connect to database: ['.mysqli_connect_errno().'] '.mysqli_connect_error());
144
            }
145
        }
146
147
        return self::$connection;
148
    }
149
150
    /**
151
     * (non-PHPdoc).
152
     *
153
     * @see Alpha\Model\ActiveRecordProviderInterface::disconnect()
154
     */
155
    public static function disconnect()
156
    {
157
        if (isset(self::$connection)) {
158
            self::$connection->close();
159
            self::$connection = null;
160
        }
161
    }
162
163
    /**
164
     * (non-PHPdoc).
165
     *
166
     * @see Alpha\Model\ActiveRecordProviderInterface::getLastDatabaseError()
167
     */
168
    public static function getLastDatabaseError()
169
    {
170
        return self::getConnection()->error;
171
    }
172
173
    /**
174
     * (non-PHPdoc).
175
     *
176
     * @see Alpha\Model\ActiveRecordProviderInterface::query()
177
     */
178
    public function query($sqlQuery)
179
    {
180
        $this->record->setLastQuery($sqlQuery);
181
182
        $resultArray = array();
183
184
        if (!$result = self::getConnection()->query($sqlQuery)) {
185
            throw new CustomQueryException('Failed to run the custom query, MySql error is ['.self::getConnection()->error.'], query ['.$sqlQuery.']');
186
        } else {
187
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
188
                array_push($resultArray, $row);
189
            }
190
191
            return $resultArray;
192
        }
193
    }
194
195
    /**
196
     * (non-PHPdoc).
197
     *
198
     * @see Alpha\Model\ActiveRecordProviderInterface::load()
199
     */
200
    public function load($ID, $version = 0)
201
    {
202
        self::$logger->debug('>>load(ID=['.$ID.'], version=['.$version.'])');
203
204
        $attributes = $this->record->getPersistentAttributes();
205
        $fields = '';
206
        foreach ($attributes as $att) {
207
            $fields .= $att.',';
208
        }
209
        $fields = mb_substr($fields, 0, -1);
210
211
        if ($version > 0) {
212
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().'_history WHERE ID = ? AND version_num = ? LIMIT 1;';
213
        } else {
214
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE ID = ? LIMIT 1;';
215
        }
216
        $this->record->setLastQuery($sqlQuery);
217
        $stmt = self::getConnection()->stmt_init();
218
219
        $row = array();
220
221
        if ($stmt->prepare($sqlQuery)) {
222
            if ($version > 0) {
223
                $stmt->bind_param('ii', $ID, $version);
224
            } else {
225
                $stmt->bind_param('i', $ID);
226
            }
227
228
            $stmt->execute();
229
230
            $result = $this->bindResult($stmt);
231
            if (isset($result[0])) {
232
                $row = $result[0];
233
            }
234
235
            $stmt->close();
236
        } else {
237
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.'], ID is ['.print_r($ID, true).'], MySql error is ['.self::getConnection()->error.']');
238
            if (!$this->record->checkTableExists()) {
239
                $this->record->makeTable();
240
241
                throw new RecordNotFoundException('Failed to load object of ID ['.$ID.'], table ['.$this->record->getTableName().'] did not exist so had to create!');
242
            }
243
244
            return;
245
        }
246
247
        if (!isset($row['ID']) || $row['ID'] < 1) {
248
            self::$logger->debug('<<load');
249
            throw new RecordNotFoundException('Failed to load object of ID ['.$ID.'] not found in database.');
250
        }
251
252
        // get the class attributes
253
        $reflection = new ReflectionClass(get_class($this->record));
254
        $properties = $reflection->getProperties();
255
256
        try {
257
            foreach ($properties as $propObj) {
258
                $propName = $propObj->name;
259
260
                // filter transient attributes
261
                if (!in_array($propName, $this->record->getTransientAttributes())) {
262
                    $this->record->set($propName, $row[$propName]);
263
                } elseif (!$propObj->isPrivate() && $this->record->getPropObject($propName) instanceof Relation) {
264
                    $prop = $this->record->getPropObject($propName);
265
266
                    // handle the setting of ONE-TO-MANY relation values
267
                    if ($prop->getRelationType() == 'ONE-TO-MANY') {
268
                        $this->record->set($propObj->name, $this->record->getID());
269
                    }
270
271
                    // handle the setting of MANY-TO-ONE relation values
272
                    if ($prop->getRelationType() == 'MANY-TO-ONE' && isset($row[$propName])) {
273
                        $this->record->set($propObj->name, $row[$propName]);
274
                    }
275
                }
276
            }
277
        } catch (IllegalArguementException $e) {
278
            self::$logger->warn('Bad data stored in the table ['.$this->record->getTableName().'], field ['.$propObj->name.'] bad value['.$row[$propObj->name].'], exception ['.$e->getMessage().']');
279
        } catch (PHPException $e) {
280
            // it is possible that the load failed due to the table not being up-to-date
281
            if ($this->record->checkTableNeedsUpdate()) {
282
                $missingFields = $this->record->findMissingFields();
283
284
                $count = count($missingFields);
285
286
                for ($i = 0; $i < $count; ++$i) {
287
                    $this->record->addProperty($missingFields[$i]);
288
                }
289
290
                self::$logger->warn('<<load');
291
                throw new RecordNotFoundException('Failed to load object of ID ['.$ID.'], table ['.$this->record->getTableName().'] was out of sync with the database so had to be updated!');
292
            }
293
        }
294
295
        self::$logger->debug('<<load ['.$ID.']');
296
    }
297
298
    /**
299
     * (non-PHPdoc).
300
     *
301
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllOldVersions()
302
     */
303
    public function loadAllOldVersions($ID)
304
    {
305
        self::$logger->debug('>>loadAllOldVersions(ID=['.$ID.'])');
306
307
        if (!$this->record->getMaintainHistory()) {
308
            throw new RecordFoundException('loadAllOldVersions method called on an active record where no history is maintained!');
309
        }
310
311
        $sqlQuery = 'SELECT version_num FROM '.$this->record->getTableName().'_history WHERE ID = \''.$ID.'\' ORDER BY version_num;';
312
313
        $this->record->setLastQuery($sqlQuery);
314
315
        if (!$result = self::getConnection()->query($sqlQuery)) {
316
            self::$logger->debug('<<loadAllOldVersions [0]');
317
            throw new RecordNotFoundException('Failed to load object versions, MySQL error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
318
        }
319
320
        // now build an array of objects to be returned
321
        $objects = array();
322
        $count = 0;
323
        $RecordClass = get_class($this->record);
324
325
        while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
326
            try {
327
                $obj = new $RecordClass();
328
                $obj->load($ID, $row['version_num']);
329
                $objects[$count] = $obj;
330
                ++$count;
331
            } catch (ResourceNotAllowedException $e) {
332
                // the resource not allowed will be absent from the list
333
            }
334
        }
335
336
        self::$logger->debug('<<loadAllOldVersions ['.count($objects).']');
337
338
        return $objects;
339
    }
340
341
    /**
342
     * (non-PHPdoc).
343
     *
344
     * @see Alpha\Model\ActiveRecordProviderInterface::loadByAttribute()
345
     */
346
    public function loadByAttribute($attribute, $value, $ignoreClassType = false, $loadAttributes = array())
347
    {
348
        self::$logger->debug('>>loadByAttribute(attribute=['.$attribute.'], value=['.$value.'], ignoreClassType=['.$ignoreClassType.'],
349
			loadAttributes=['.var_export($loadAttributes, true).'])');
350
351
        if (count($loadAttributes) == 0) {
352
            $attributes = $this->record->getPersistentAttributes();
353
        } else {
354
            $attributes = $loadAttributes;
355
        }
356
357
        $fields = '';
358
        foreach ($attributes as $att) {
359
            $fields .= $att.',';
360
        }
361
        $fields = mb_substr($fields, 0, -1);
362
363
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
364
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE '.$attribute.' = ? AND classname = ? LIMIT 1;';
365
        } else {
366
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE '.$attribute.' = ? LIMIT 1;';
367
        }
368
369
        self::$logger->debug('Query=['.$sqlQuery.']');
370
371
        $this->record->setLastQuery($sqlQuery);
372
        $stmt = self::getConnection()->stmt_init();
373
374
        $row = array();
375
376
        if ($stmt->prepare($sqlQuery)) {
377
            if ($this->record->getPropObject($attribute) instanceof Integer) {
378
                if (!$ignoreClassType && $this->record->isTableOverloaded()) {
379
                    $classname = get_class($this->record);
380
                    $stmt->bind_param('is', $value, $classname);
381
                } else {
382
                    $stmt->bind_param('i', $value);
383
                }
384
            } else {
385
                if (!$ignoreClassType && $this->record->isTableOverloaded()) {
386
                    $classname = get_class($this->record);
387
                    $stmt->bind_param('ss', $value, $classname);
388
                } else {
389
                    $stmt->bind_param('s', $value);
390
                }
391
            }
392
393
            $stmt->execute();
394
395
            $result = $this->bindResult($stmt);
396
397
            if (isset($result[0])) {
398
                $row = $result[0];
399
            }
400
401
            $stmt->close();
402
        } else {
403
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
404
            if (!$this->record->checkTableExists()) {
405
                $this->record->makeTable();
406
407
                throw new RecordNotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], table did not exist so had to create!');
408
            }
409
410
            return;
411
        }
412
413
        if (!isset($row['ID']) || $row['ID'] < 1) {
414
            self::$logger->debug('<<loadByAttribute');
415
            throw new RecordNotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], not found in database.');
416
        }
417
418
        $this->record->setID($row['ID']);
419
420
        // get the class attributes
421
        $reflection = new ReflectionClass(get_class($this->record));
422
        $properties = $reflection->getProperties();
423
424
        try {
425
            foreach ($properties as $propObj) {
426
                $propName = $propObj->name;
427
428
                if (isset($row[$propName])) {
429
                    // filter transient attributes
430
                    if (!in_array($propName, $this->record->getTransientAttributes())) {
431
                        $this->record->set($propName, $row[$propName]);
432
                    } elseif (!$propObj->isPrivate() && $this->record->get($propName) != '' && $this->record->getPropObject($propName) instanceof Relation) {
433
                        $prop = $this->record->getPropObject($propName);
434
435
                        // handle the setting of ONE-TO-MANY relation values
436
                        if ($prop->getRelationType() == 'ONE-TO-MANY') {
437
                            $this->record->set($propObj->name, $this->record->getID());
438
                        }
439
                    }
440
                }
441
            }
442
        } catch (IllegalArguementException $e) {
443
            self::$logger->warn('Bad data stored in the table ['.$this->record->getTableName().'], field ['.$propObj->name.'] bad value['.$row[$propObj->name].'], exception ['.$e->getMessage().']');
444
        } catch (PHPException $e) {
445
            // it is possible that the load failed due to the table not being up-to-date
446
            if ($this->record->checkTableNeedsUpdate()) {
447
                $missingFields = $this->record->findMissingFields();
448
449
                $count = count($missingFields);
450
451
                for ($i = 0; $i < $count; ++$i) {
452
                    $this->record->addProperty($missingFields[$i]);
453
                }
454
455
                self::$logger->debug('<<loadByAttribute');
456
                throw new RecordNotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], table ['.$this->record->getTableName().'] was out of sync with the database so had to be updated!');
457
            }
458
        }
459
460
        self::$logger->debug('<<loadByAttribute');
461
    }
462
463
    /**
464
     * (non-PHPdoc).
465
     *
466
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAll()
467
     */
468
    public function loadAll($start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false)
469
    {
470
        self::$logger->debug('>>loadAll(start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
471
472
        // ensure that the field name provided in the orderBy param is legit
473
        try {
474
            $this->record->get($orderBy);
475
        } catch (AlphaException $e) {
476
            throw new AlphaException('The field name ['.$orderBy.'] provided in the param orderBy does not exist on the class ['.get_class($this->record).']');
477
        }
478
479
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
480
            if ($limit == 0) {
481
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE classname = \''.addslashes(get_class($this->record)).'\' ORDER BY '.$orderBy.' '.$order.';';
482
            } else {
483
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE classname = \''.addslashes(get_class($this->record)).'\' ORDER BY '.$orderBy.' '.$order.' LIMIT '.
484
                    $start.', '.$limit.';';
485
            }
486
        } else {
487
            if ($limit == 0) {
488
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' ORDER BY '.$orderBy.' '.$order.';';
489
            } else {
490
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' ORDER BY '.$orderBy.' '.$order.' LIMIT '.$start.', '.$limit.';';
491
            }
492
        }
493
494
        $this->record->setLastQuery($sqlQuery);
495
496
        if (!$result = self::getConnection()->query($sqlQuery)) {
497
            self::$logger->debug('<<loadAll [0]');
498
            throw new RecordNotFoundException('Failed to load object IDs, MySql error is ['.self::getConnection()->error.'], query ['.$this->record->getLastQuery().']');
499
        }
500
501
        // now build an array of objects to be returned
502
        $objects = array();
503
        $count = 0;
504
        $RecordClass = get_class($this->record);
505
506
        while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
507
            try {
508
                $obj = new $RecordClass();
509
                $obj->load($row['ID']);
510
                $objects[$count] = $obj;
511
                ++$count;
512
            } catch (ResourceNotAllowedException $e) {
513
                // the resource not allowed will be absent from the list
514
            }
515
        }
516
517
        self::$logger->debug('<<loadAll ['.count($objects).']');
518
519
        return $objects;
520
    }
521
522
    /**
523
     * (non-PHPdoc).
524
     *
525
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByAttribute()
526
     */
527
    public function loadAllByAttribute($attribute, $value, $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false, $constructorArgs = array())
528
    {
529
        self::$logger->debug('>>loadAllByAttribute(attribute=['.$attribute.'], value=['.$value.'], start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.'], constructorArgs=['.print_r($constructorArgs, true).']');
530
531
        if ($limit != 0) {
532
            $limit = ' LIMIT '.$start.', '.$limit.';';
533
        } else {
534
            $limit = ';';
535
        }
536
537
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
538
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE $attribute = ? AND classname = ? ORDER BY ".$orderBy.' '.$order.$limit;
539
        } else {
540
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE $attribute = ? ORDER BY ".$orderBy.' '.$order.$limit;
541
        }
542
543
        $this->record->setLastQuery($sqlQuery);
544
        self::$logger->debug($sqlQuery);
545
546
        $stmt = self::getConnection()->stmt_init();
547
548
        $row = array();
549
550
        if ($stmt->prepare($sqlQuery)) {
551
            if ($this->record->getPropObject($attribute) instanceof Integer) {
552
                if ($this->record->isTableOverloaded()) {
553
                    $classname = get_class($this->record);
554
                    $stmt->bind_param('is', $value, $classname);
555
                } else {
556
                    $stmt->bind_param('i', $value);
557
                }
558
            } else {
559
                if ($this->record->isTableOverloaded()) {
560
                    $classname = get_class($this->record);
561
                    $stmt->bind_param('ss', $value, $classname);
562
                } else {
563
                    $stmt->bind_param('s', $value);
564
                }
565
            }
566
567
            $stmt->execute();
568
569
            $result = $this->bindResult($stmt);
570
571
            $stmt->close();
572
        } else {
573
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
574
            if (!$this->record->checkTableExists()) {
575
                $this->record->makeTable();
576
577
                throw new RecordNotFoundException('Failed to load objects by attribute ['.$attribute.'] and value ['.$value.'], table did not exist so had to create!');
578
            }
579
            self::$logger->debug('<<loadAllByAttribute []');
580
581
            return array();
582
        }
583
584
        // now build an array of objects to be returned
585
        $objects = array();
586
        $count = 0;
587
        $RecordClass = get_class($this->record);
588
589
        foreach ($result as $row) {
590
            try {
591
                $argsCount = count($constructorArgs);
592
593
                if ($argsCount < 1) {
594
                    $obj = new $RecordClass();
595
                } else {
596
                    switch ($argsCount) {
597
                        case 1:
598
                            $obj = new $RecordClass($constructorArgs[0]);
599
                        break;
600
                        case 2:
601
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1]);
602
                        break;
603
                        case 3:
604
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2]);
605
                        break;
606
                        case 4:
607
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3]);
608
                        break;
609
                        case 5:
610
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3], $constructorArgs[4]);
611
                        break;
612
                        default:
613
                            throw new IllegalArguementException('Too many elements in the $constructorArgs array passed to the loadAllByAttribute method!');
614
                    }
615
                }
616
617
                $obj->load($row['ID']);
618
                $objects[$count] = $obj;
619
                ++$count;
620
            } catch (ResourceNotAllowedException $e) {
621
                // the resource not allowed will be absent from the list
622
            }
623
        }
624
625
        self::$logger->debug('<<loadAllByAttribute ['.count($objects).']');
626
627
        return $objects;
628
    }
629
630
    /**
631
     * (non-PHPdoc).
632
     *
633
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByAttributes()
634
     */
635
    public function loadAllByAttributes($attributes = array(), $values = array(), $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false, $constructorArgs = array())
636
    {
637
        self::$logger->debug('>>loadAllByAttributes(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'], start=['.
638
            $start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.'], constructorArgs=['.print_r($constructorArgs, true).']');
639
640
        $whereClause = ' WHERE';
641
642
        $count = count($attributes);
643
644
        for ($i = 0; $i < $count; ++$i) {
645
            $whereClause .= ' '.$attributes[$i].' = ? AND';
646
            self::$logger->debug($whereClause);
647
        }
648
649
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
650
            $whereClause .= ' classname = ? AND';
651
        }
652
653
        // remove the last " AND"
654
        $whereClause = mb_substr($whereClause, 0, -4);
655
656
        if ($limit != 0) {
657
            $limit = ' LIMIT '.$start.', '.$limit.';';
658
        } else {
659
            $limit = ';';
660
        }
661
662
        $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().$whereClause.' ORDER BY '.$orderBy.' '.$order.$limit;
663
664
        $this->record->setLastQuery($sqlQuery);
665
666
        $stmt = self::getConnection()->stmt_init();
667
668
        if ($stmt->prepare($sqlQuery)) {
669
            // bind params where required attributes are provided
670
            if (count($attributes) > 0 && count($attributes) == count($values)) {
671
                $stmt = $this->bindParams($stmt, $attributes, $values);
672
            } else {
673
                // we'll still need to bind the "classname" for overloaded records...
674
                if ($this->record->isTableOverloaded()) {
675
                    $classname = get_class($this->record);
676
                    $stmt->bind_param('s', $classname);
677
                }
678
            }
679
            $stmt->execute();
680
681
            $result = $this->bindResult($stmt);
682
683
            $stmt->close();
684
        } else {
685
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
686
687
            if (!$this->record->checkTableExists()) {
688
                $this->record->makeTable();
689
690
                throw new RecordNotFoundException('Failed to load objects by attributes ['.var_export($attributes, true).'] and values ['.
691
                    var_export($values, true).'], table did not exist so had to create!');
692
            }
693
694
            self::$logger->debug('<<loadAllByAttributes []');
695
696
            return array();
697
        }
698
699
        // now build an array of objects to be returned
700
        $objects = array();
701
        $count = 0;
702
        $RecordClass = get_class($this->record);
703
704
        foreach ($result as $row) {
705
            try {
706
                $argsCount = count($constructorArgs);
707
708
                if ($argsCount < 1) {
709
                    $obj = new $RecordClass();
710
                } else {
711
                    switch ($argsCount) {
712
                        case 1:
713
                            $obj = new $RecordClass($constructorArgs[0]);
714
                        break;
715
                        case 2:
716
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1]);
717
                        break;
718
                        case 3:
719
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2]);
720
                        break;
721
                        case 4:
722
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3]);
723
                        break;
724
                        case 5:
725
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3], $constructorArgs[4]);
726
                        break;
727
                        default:
728
                            throw new IllegalArguementException('Too many elements in the $constructorArgs array passed to the loadAllByAttribute method!');
729
                    }
730
                }
731
732
                $obj->load($row['ID']);
733
                $objects[$count] = $obj;
734
                ++$count;
735
            } catch (ResourceNotAllowedException $e) {
736
                // the resource not allowed will be absent from the list
737
            }
738
        }
739
740
        self::$logger->debug('<<loadAllByAttributes ['.count($objects).']');
741
742
        return $objects;
743
    }
744
745
    /**
746
     * (non-PHPdoc).
747
     *
748
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByDayUpdated()
749
     */
750
    public function loadAllByDayUpdated($date, $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false)
751
    {
752
        self::$logger->debug('>>loadAllByDayUpdated(date=['.$date.'], start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
753
754
        if ($start != 0 && $limit != 0) {
755
            $limit = ' LIMIT '.$start.', '.$limit.';';
756
        } else {
757
            $limit = ';';
758
        }
759
760
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
761
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE updated_ts >= '".$date." 00:00:00' AND updated_ts <= '".$date." 23:59:59' AND classname = '".addslashes(get_class($this->record))."' ORDER BY ".$orderBy.' '.$order.$limit;
762
        } else {
763
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE updated_ts >= '".$date." 00:00:00' AND updated_ts <= '".$date." 23:59:59' ORDER BY ".$orderBy.' '.$order.$limit;
764
        }
765
766
        $this->record->setLastQuery($sqlQuery);
767
768
        if (!$result = self::getConnection()->query($sqlQuery)) {
769
            self::$logger->debug('<<loadAllByDayUpdated []');
770
            throw new RecordNotFoundException('Failed to load object IDs, MySql error is ['.self::getConnection()->error.'], query ['.$this->record->getLastQuery().']');
771
        }
772
773
        // now build an array of objects to be returned
774
        $objects = array();
775
        $count = 0;
776
        $RecordClass = get_class($this->record);
777
778
        while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
779
            $obj = new $RecordClass();
780
            $obj->load($row['ID']);
781
            $objects[$count] = $obj;
782
            ++$count;
783
        }
784
785
        self::$logger->debug('<<loadAllByDayUpdated ['.count($objects).']');
786
787
        return $objects;
788
    }
789
790
    /**
791
     * (non-PHPdoc).
792
     *
793
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllFieldValuesByAttribute()
794
     */
795
    public function loadAllFieldValuesByAttribute($attribute, $value, $returnAttribute, $order = 'ASC', $ignoreClassType = false)
796
    {
797
        self::$logger->debug('>>loadAllFieldValuesByAttribute(attribute=['.$attribute.'], value=['.$value.'], returnAttribute=['.$returnAttribute.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
798
799
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
800
            $sqlQuery = 'SELECT '.$returnAttribute.' FROM '.$this->record->getTableName()." WHERE $attribute = '$value' AND classname = '".addslashes(get_class($this->record))."' ORDER BY ID ".$order.';';
801
        } else {
802
            $sqlQuery = 'SELECT '.$returnAttribute.' FROM '.$this->record->getTableName()." WHERE $attribute = '$value' ORDER BY ID ".$order.';';
803
        }
804
805
        $this->record->setLastQuery($sqlQuery);
806
807
        self::$logger->debug('lastQuery ['.$sqlQuery.']');
808
809
        if (!$result = self::getConnection()->query($sqlQuery)) {
810
            self::$logger->debug('<<loadAllFieldValuesByAttribute []');
811
            throw new RecordNotFoundException('Failed to load field ['.$returnAttribute.'] values, MySql error is ['.self::getConnection()->error.'], query ['.$this->record->getLastQuery().']');
812
        }
813
814
        // now build an array of attribute values to be returned
815
        $values = array();
816
        $count = 0;
817
818
        while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
819
            $values[$count] = $row[$returnAttribute];
820
            ++$count;
821
        }
822
823
        self::$logger->debug('<<loadAllFieldValuesByAttribute ['.count($values).']');
824
825
        return $values;
826
    }
827
828
    /**
829
     * (non-PHPdoc).
830
     *
831
     * @see Alpha\Model\ActiveRecordProviderInterface::save()
832
     */
833
    public function save()
834
    {
835
        self::$logger->debug('>>save()');
836
837
        $config = ConfigProvider::getInstance();
838
        $sessionProvider = $config->get('session.provider.name');
839
        $session = ServiceFactory::getInstance($sessionProvider, 'Alpha\Util\Http\Session\SessionProviderInterface');
840
841
        // get the class attributes
842
        $reflection = new ReflectionClass(get_class($this->record));
843
        $properties = $reflection->getProperties();
844
845
        if ($this->record->getVersion() != $this->record->getVersionNumber()->getValue()) {
846
            throw new LockingException('Could not save the object as it has been updated by another user.  Please try saving again.');
847
        }
848
849
        // set the "updated by" fields, we can only set the user id if someone is logged in
850
        if ($session->get('currentUser') != null) {
851
            $this->record->set('updated_by', $session->get('currentUser')->getID());
852
        }
853
854
        $this->record->set('updated_ts', new Timestamp(date('Y-m-d H:i:s')));
855
856
        // check to see if it is a transient object that needs to be inserted
857
        if ($this->record->isTransient()) {
858
            $savedFieldsCount = 0;
859
            $sqlQuery = 'INSERT INTO '.$this->record->getTableName().' (';
860
861
            foreach ($properties as $propObj) {
862
                $propName = $propObj->name;
863
                if (!in_array($propName, $this->record->getTransientAttributes())) {
864
                    // Skip the ID, database auto number takes care of this.
865
                    if ($propName != 'ID' && $propName != 'version_num') {
866
                        $sqlQuery .= "$propName,";
867
                        ++$savedFieldsCount;
868
                    }
869
870
                    if ($propName == 'version_num') {
871
                        $sqlQuery .= 'version_num,';
872
                        ++$savedFieldsCount;
873
                    }
874
                }
875
            }
876
877
            if ($this->record->isTableOverloaded()) {
878
                $sqlQuery .= 'classname,';
879
            }
880
881
            $sqlQuery = rtrim($sqlQuery, ',');
882
883
            $sqlQuery .= ') VALUES (';
884
885
            for ($i = 0; $i < $savedFieldsCount; ++$i) {
886
                $sqlQuery .= '?,';
887
            }
888
889
            if ($this->record->isTableOverloaded()) {
890
                $sqlQuery .= '?,';
891
            }
892
893
            $sqlQuery = rtrim($sqlQuery, ',').')';
894
895
            $this->record->setLastQuery($sqlQuery);
896
            self::$logger->debug('Query ['.$sqlQuery.']');
897
898
            $stmt = self::getConnection()->stmt_init();
899
900
            if ($stmt->prepare($sqlQuery)) {
901
                $stmt = $this->bindParams($stmt);
902
                $stmt->execute();
903
            } else {
904
                throw new FailedSaveException('Failed to save object, error is ['.$stmt->error.'], query ['.$this->record->getLastQuery().']');
905
            }
906
        } else {
907
            // assume that it is a persistent object that needs to be updated
908
            $savedFieldsCount = 0;
909
            $sqlQuery = 'UPDATE '.$this->record->getTableName().' SET ';
910
911
            foreach ($properties as $propObj) {
912
                $propName = $propObj->name;
913
                if (!in_array($propName, $this->record->getTransientAttributes())) {
914
                    // Skip the ID, database auto number takes care of this.
915
                    if ($propName != 'ID' && $propName != 'version_num') {
916
                        $sqlQuery .= "$propName = ?,";
917
                        ++$savedFieldsCount;
918
                    }
919
920
                    if ($propName == 'version_num') {
921
                        $sqlQuery .= 'version_num = ?,';
922
                        ++$savedFieldsCount;
923
                    }
924
                }
925
            }
926
927
            if ($this->record->isTableOverloaded()) {
928
                $sqlQuery .= 'classname = ?,';
929
            }
930
931
            $sqlQuery = rtrim($sqlQuery, ',');
932
933
            $sqlQuery .= ' WHERE ID = ?;';
934
935
            $this->record->setLastQuery($sqlQuery);
936
            $stmt = self::getConnection()->stmt_init();
937
938
            if ($stmt->prepare($sqlQuery)) {
939
                $this->bindParams($stmt);
940
                $stmt->execute();
941
            } else {
942
                throw new FailedSaveException('Failed to save object, error is ['.$stmt->error.'], query ['.$this->record->getLastQuery().']');
943
            }
944
        }
945
946
        if ($stmt != null && $stmt->error == '') {
947
            // populate the updated ID in case we just done an insert
948
            if ($this->record->isTransient()) {
949
                $this->record->setID(self::getConnection()->insert_id);
950
            }
951
952
            try {
953
                foreach ($properties as $propObj) {
954
                    $propName = $propObj->name;
955
956
                    if ($this->record->getPropObject($propName) instanceof Relation) {
957
                        $prop = $this->record->getPropObject($propName);
958
959
                        // handle the saving of MANY-TO-MANY relation values
960
                        if ($prop->getRelationType() == 'MANY-TO-MANY' && count($prop->getRelatedIDs()) > 0) {
961
                            try {
962
                                try {
963
                                    // check to see if the rel is on this class
964
                                    $side = $prop->getSide(get_class($this->record));
965
                                } catch (IllegalArguementException $iae) {
966
                                    $side = $prop->getSide(get_parent_class($this->record));
967
                                }
968
969
                                $lookUp = $prop->getLookup();
970
971
                                // first delete all of the old RelationLookup objects for this rel
972
                                try {
973
                                    if ($side == 'left') {
974
                                        $lookUp->deleteAllByAttribute('leftID', $this->record->getID());
975
                                    } else {
976
                                        $lookUp->deleteAllByAttribute('rightID', $this->record->getID());
977
                                    }
978
                                } catch (\Exception $e) {
979
                                    throw new FailedSaveException('Failed to delete old RelationLookup objects on the table ['.$prop->getLookup()->getTableName().'], error is ['.$e->getMessage().']');
980
                                }
981
982
                                $IDs = $prop->getRelatedIDs();
983
984
                                if (isset($IDs) && !empty($IDs[0])) {
985
                                    // now for each posted ID, create a new RelationLookup record and save
986
                                    foreach ($IDs as $id) {
987
                                        $newLookUp = new RelationLookup($lookUp->get('leftClassName'), $lookUp->get('rightClassName'));
988
                                        if ($side == 'left') {
989
                                            $newLookUp->set('leftID', $this->record->getID());
990
                                            $newLookUp->set('rightID', $id);
991
                                        } else {
992
                                            $newLookUp->set('rightID', $this->record->getID());
993
                                            $newLookUp->set('leftID', $id);
994
                                        }
995
                                        $newLookUp->save();
996
                                    }
997
                                }
998
                            } catch (\Exception $e) {
999
                                throw new FailedSaveException('Failed to update a MANY-TO-MANY relation on the object, error is ['.$e->getMessage().']');
1000
                            }
1001
                        }
1002
1003
                        // handle the saving of ONE-TO-MANY relation values
1004
                        if ($prop->getRelationType() == 'ONE-TO-MANY') {
1005
                            $prop->setValue($this->record->getID());
1006
                        }
1007
                    }
1008
                }
1009
            } catch (\Exception $e) {
1010
                throw new FailedSaveException('Failed to save object, error is ['.$e->getMessage().']');
1011
            }
1012
1013
            $stmt->close();
1014
        } else {
1015
            // there has been an error, so decrement the version number back
1016
            $temp = $this->record->getVersionNumber()->getValue();
1017
            $this->record->set('version_num', $temp-1);
1018
1019
            // check for unique violations
1020
            if (self::getConnection()->errno == '1062') {
1021
                throw new ValidationException('Failed to save, the value '.$this->findOffendingValue(self::getConnection()->error).' is already in use!');
1022
            } else {
1023
                throw new FailedSaveException('Failed to save object, MySql error is ['.self::getConnection()->error.'], query ['.$this->record->getLastQuery().']');
1024
            }
1025
        }
1026
1027
        if ($this->record->getMaintainHistory()) {
1028
            $this->record->saveHistory();
1029
        }
1030
    }
1031
1032
    /**
1033
     * (non-PHPdoc).
1034
     *
1035
     * @see Alpha\Model\ActiveRecordProviderInterface::saveAttribute()
1036
     */
1037
    public function saveAttribute($attribute, $value)
1038
    {
1039
        self::$logger->debug('>>saveAttribute(attribute=['.$attribute.'], value=['.$value.'])');
1040
1041
        $config = ConfigProvider::getInstance();
1042
        $sessionProvider = $config->get('session.provider.name');
1043
        $session = ServiceFactory::getInstance($sessionProvider, 'Alpha\Util\Http\Session\SessionProviderInterface');
1044
1045
        if ($this->record->getVersion() != $this->record->getVersionNumber()->getValue()) {
1046
            throw new LockingException('Could not save the object as it has been updated by another user.  Please try saving again.');
1047
        }
1048
1049
        // set the "updated by" fields, we can only set the user id if someone is logged in
1050
        if ($session->get('currentUser') != null) {
1051
            $this->record->set('updated_by', $session->get('currentUser')->getID());
1052
        }
1053
1054
        $this->record->set('updated_ts', new Timestamp(date('Y-m-d H:i:s')));
1055
1056
        // assume that it is a persistent object that needs to be updated
1057
        $sqlQuery = 'UPDATE '.$this->record->getTableName().' SET '.$attribute.' = ?, version_num = ? , updated_by = ?, updated_ts = ? WHERE ID = ?;';
1058
1059
        $this->record->setLastQuery($sqlQuery);
1060
        $stmt = self::getConnection()->stmt_init();
1061
1062
        $newVersionNumber = $this->record->getVersionNumber()->getValue()+1;
1063
1064
        if ($stmt->prepare($sqlQuery)) {
1065
            if ($this->record->getPropObject($attribute) instanceof Integer) {
1066
                $bindingsType = 'i';
1067
            } else {
1068
                $bindingsType = 's';
1069
            }
1070
            $ID = $this->record->getID();
1071
            $updatedBy = $this->record->get('updated_by');
1072
            $updatedTS = $this->record->get('updated_ts');
1073
            $stmt->bind_param($bindingsType.'iisi', $value, $newVersionNumber, $updatedBy, $updatedTS, $ID);
1074
            self::$logger->debug('Binding params ['.$bindingsType.'iisi, '.$value.', '.$newVersionNumber.', '.$updatedBy.', '.$updatedTS.', '.$ID.']');
1075
            $stmt->execute();
1076
        } else {
1077
            throw new FailedSaveException('Failed to save attribute, error is ['.$stmt->error.'], query ['.$this->record->getLastQuery().']');
1078
        }
1079
1080
        $stmt->close();
1081
1082
        $this->record->set($attribute, $value);
1083
        $this->record->set('version_num', $newVersionNumber);
1084
1085
        if ($this->record->getMaintainHistory()) {
1086
            $this->record->saveHistory();
1087
        }
1088
1089
        self::$logger->debug('<<saveAttribute');
1090
    }
1091
1092
    /**
1093
     * (non-PHPdoc).
1094
     *
1095
     * @see Alpha\Model\ActiveRecordProviderInterface::saveHistory()
1096
     */
1097
    public function saveHistory()
1098
    {
1099
        self::$logger->debug('>>saveHistory()');
1100
1101
        // get the class attributes
1102
        $reflection = new ReflectionClass(get_class($this->record));
1103
        $properties = $reflection->getProperties();
1104
1105
        $savedFieldsCount = 0;
1106
        $attributeNames = array();
1107
        $attributeValues = array();
1108
1109
        $sqlQuery = 'INSERT INTO '.$this->record->getTableName().'_history (';
1110
1111
        foreach ($properties as $propObj) {
1112
            $propName = $propObj->name;
1113
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1114
                $sqlQuery .= "$propName,";
1115
                $attributeNames[] = $propName;
1116
                $attributeValues[] = $this->record->get($propName);
1117
                ++$savedFieldsCount;
1118
            }
1119
        }
1120
1121
        if ($this->record->isTableOverloaded()) {
1122
            $sqlQuery .= 'classname,';
1123
        }
1124
1125
        $sqlQuery = rtrim($sqlQuery, ',');
1126
1127
        $sqlQuery .= ') VALUES (';
1128
1129
        for ($i = 0; $i < $savedFieldsCount; ++$i) {
1130
            $sqlQuery .= '?,';
1131
        }
1132
1133
        if ($this->record->isTableOverloaded()) {
1134
            $sqlQuery .= '?,';
1135
        }
1136
1137
        $sqlQuery = rtrim($sqlQuery, ',').')';
1138
1139
        $this->record->setLastQuery($sqlQuery);
1140
        self::$logger->debug('Query ['.$sqlQuery.']');
1141
1142
        $stmt = self::getConnection()->stmt_init();
1143
1144
        if ($stmt->prepare($sqlQuery)) {
1145
            $stmt = $this->bindParams($stmt, $attributeNames, $attributeValues);
1146
            $stmt->execute();
1147
        } else {
1148
            throw new FailedSaveException('Failed to save object history, error is ['.$stmt->error.'], query ['.$this->record->getLastQuery().']');
1149
        }
1150
    }
1151
1152
    /**
1153
     * (non-PHPdoc).
1154
     *
1155
     * @see Alpha\Model\ActiveRecordProviderInterface::delete()
1156
     */
1157
    public function delete()
1158
    {
1159
        self::$logger->debug('>>delete()');
1160
1161
        $sqlQuery = 'DELETE FROM '.$this->record->getTableName().' WHERE ID = ?;';
1162
1163
        $this->record->setLastQuery($sqlQuery);
1164
1165
        $stmt = self::getConnection()->stmt_init();
1166
1167
        if ($stmt->prepare($sqlQuery)) {
1168
            $ID = $this->record->getID();
1169
            $stmt->bind_param('i', $ID);
1170
            $stmt->execute();
1171
            self::$logger->debug('Deleted the object ['.$this->record->getID().'] of class ['.get_class($this->record).']');
1172
        } else {
1173
            throw new FailedDeleteException('Failed to delete object ['.$this->record->getID().'], error is ['.$stmt->error.'], query ['.$this->record->getLastQuery().']');
1174
        }
1175
1176
        $stmt->close();
1177
1178
        self::$logger->debug('<<delete');
1179
    }
1180
1181
    /**
1182
     * (non-PHPdoc).
1183
     *
1184
     * @see Alpha\Model\ActiveRecordProviderInterface::getVersion()
1185
     */
1186
    public function getVersion()
1187
    {
1188
        self::$logger->debug('>>getVersion()');
1189
1190
        $sqlQuery = 'SELECT version_num FROM '.$this->record->getTableName().' WHERE ID = ?;';
1191
        $this->record->setLastQuery($sqlQuery);
1192
1193
        $stmt = self::getConnection()->stmt_init();
1194
1195
        if ($stmt->prepare($sqlQuery)) {
1196
            $ID = $this->record->getID();
1197
            $stmt->bind_param('i', $ID);
1198
1199
            $stmt->execute();
1200
1201
            $result = $this->bindResult($stmt);
1202
            if (isset($result[0])) {
1203
                $row = $result[0];
1204
            }
1205
1206
            $stmt->close();
1207
        } else {
1208
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
1209
            if (!$this->record->checkTableExists()) {
1210
                $this->record->makeTable();
1211
1212
                throw new RecordNotFoundException('Failed to get the version number, table did not exist so had to create!');
1213
            }
1214
1215
            return;
1216
        }
1217
1218
        if (!isset($row['version_num']) || $row['version_num'] < 1) {
1219
            self::$logger->debug('<<getVersion [0]');
1220
1221
            return 0;
1222
        } else {
1223
            $version_num = $row['version_num'];
1224
1225
            self::$logger->debug('<<getVersion ['.$version_num.']');
1226
1227
            return $version_num;
1228
        }
1229
    }
1230
1231
    /**
1232
     * (non-PHPdoc).
1233
     *
1234
     * @see Alpha\Model\ActiveRecordProviderInterface::makeTable()
1235
     */
1236
    public function makeTable()
1237
    {
1238
        self::$logger->debug('>>makeTable()');
1239
1240
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().' (ID INT(11) ZEROFILL NOT NULL AUTO_INCREMENT,';
1241
1242
        // get the class attributes
1243
        $reflection = new ReflectionClass(get_class($this->record));
1244
        $properties = $reflection->getProperties();
1245
1246
        foreach ($properties as $propObj) {
1247
            $propName = $propObj->name;
1248
1249
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1250
                $prop = $this->record->getPropObject($propName);
1251
1252
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1253
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL,';
1254
                } elseif ($prop instanceof Integer) {
1255
                    $sqlQuery .= "$propName INT(".$prop->getSize().'),';
1256
                } elseif ($prop instanceof Double) {
1257
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).'),';
1258
                } elseif ($prop instanceof SmallText) {
1259
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8,';
1260
                } elseif ($prop instanceof Text) {
1261
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8,";
1262
                } elseif ($prop instanceof Boolean) {
1263
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0',";
1264
                } elseif ($prop instanceof Date) {
1265
                    $sqlQuery .= "$propName DATE,";
1266
                } elseif ($prop instanceof Timestamp) {
1267
                    $sqlQuery .= "$propName DATETIME,";
1268
                } elseif ($prop instanceof Enum) {
1269
                    $sqlQuery .= "$propName ENUM(";
1270
                    $enumVals = $prop->getOptions();
1271
                    foreach ($enumVals as $val) {
1272
                        $sqlQuery .= "'".$val."',";
1273
                    }
1274
                    $sqlQuery = rtrim($sqlQuery, ',');
1275
                    $sqlQuery .= ') CHARACTER SET utf8,';
1276
                } elseif ($prop instanceof DEnum) {
1277
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1278
                    $denum->saveIfNew();
1279
                    $sqlQuery .= "$propName INT(11) ZEROFILL,";
1280
                } elseif ($prop instanceof Relation) {
1281
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED,";
1282
                } else {
1283
                    $sqlQuery .= '';
1284
                }
1285
            }
1286
        }
1287
        if ($this->record->isTableOverloaded()) {
1288
            $sqlQuery .= 'classname VARCHAR(100),';
1289
        }
1290
1291
        $sqlQuery .= 'PRIMARY KEY (ID)) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;';
1292
1293
        $this->record->setLastQuery($sqlQuery);
1294
1295
        if (!$result = self::getConnection()->query($sqlQuery)) {
1296
            self::$logger->debug('<<makeTable');
1297
            throw new AlphaException('Failed to create the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getConnection()->error.']');
1298
        }
1299
1300
        // check the table indexes if any additional ones required
1301
        $this->checkIndexes();
1302
1303
        if ($this->record->getMaintainHistory()) {
1304
            $this->record->makeHistoryTable();
1305
        }
1306
1307
        self::$logger->debug('<<makeTable');
1308
    }
1309
1310
    /**
1311
     * (non-PHPdoc).
1312
     *
1313
     * @see Alpha\Model\ActiveRecordProviderInterface::makeHistoryTable()
1314
     */
1315
    public function makeHistoryTable()
1316
    {
1317
        self::$logger->debug('>>makeHistoryTable()');
1318
1319
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().'_history (ID INT(11) ZEROFILL NOT NULL,';
1320
1321
        // get the class attributes
1322
        $reflection = new ReflectionClass(get_class($this->record));
1323
        $properties = $reflection->getProperties();
1324
1325
        foreach ($properties as $propObj) {
1326
            $propName = $propObj->name;
1327
1328
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1329
                $prop = $this->record->getPropObject($propName);
1330
1331
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1332
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL,';
1333
                } elseif ($prop instanceof Integer) {
1334
                    $sqlQuery .= "$propName INT(".$prop->getSize().'),';
1335
                } elseif ($prop instanceof Double) {
1336
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).'),';
1337
                } elseif ($prop instanceof SmallText) {
1338
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8,';
1339
                } elseif ($prop instanceof Text) {
1340
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8,";
1341
                } elseif ($prop instanceof Boolean) {
1342
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0',";
1343
                } elseif ($prop instanceof Date) {
1344
                    $sqlQuery .= "$propName DATE,";
1345
                } elseif ($prop instanceof Timestamp) {
1346
                    $sqlQuery .= "$propName DATETIME,";
1347
                } elseif ($prop instanceof Enum) {
1348
                    $sqlQuery .= "$propName ENUM(";
1349
                    $enumVals = $prop->getOptions();
1350
                    foreach ($enumVals as $val) {
1351
                        $sqlQuery .= "'".$val."',";
1352
                    }
1353
                    $sqlQuery = rtrim($sqlQuery, ',');
1354
                    $sqlQuery .= ') CHARACTER SET utf8,';
1355
                } elseif ($prop instanceof DEnum) {
1356
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1357
                    $denum->saveIfNew();
1358
                    $sqlQuery .= "$propName INT(11) ZEROFILL,";
1359
                } elseif ($prop instanceof Relation) {
1360
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED,";
1361
                } else {
1362
                    $sqlQuery .= '';
1363
                }
1364
            }
1365
        }
1366
1367
        if ($this->record->isTableOverloaded()) {
1368
            $sqlQuery .= 'classname VARCHAR(100),';
1369
        }
1370
1371
        $sqlQuery .= 'PRIMARY KEY (ID, version_num)) ENGINE=MyISAM DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;';
1372
1373
        $this->record->setLastQuery($sqlQuery);
1374
1375
        if (!$result = self::getConnection()->query($sqlQuery)) {
1376
            self::$logger->debug('<<makeHistoryTable');
1377
            throw new AlphaException('Failed to create the table ['.$this->record->getTableName().'_history] for the class ['.get_class($this->record).'], database error is ['.self::getConnection()->error.']');
1378
        }
1379
1380
        self::$logger->debug('<<makeHistoryTable');
1381
    }
1382
1383
    /**
1384
     * (non-PHPdoc).
1385
     *
1386
     * @see Alpha\Model\ActiveRecordProviderInterface::rebuildTable()
1387
     */
1388
    public function rebuildTable()
1389
    {
1390
        self::$logger->debug('>>rebuildTable()');
1391
1392
        $sqlQuery = 'DROP TABLE IF EXISTS '.$this->record->getTableName().';';
1393
1394
        $this->record->setLastQuery($sqlQuery);
1395
1396
        if (!$result = self::getConnection()->query($sqlQuery)) {
1397
            self::$logger->debug('<<rebuildTable');
1398
            throw new AlphaException('Failed to drop the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getConnection()->error.']');
1399
        }
1400
1401
        $this->record->makeTable();
1402
1403
        self::$logger->debug('<<rebuildTable');
1404
    }
1405
1406
    /**
1407
     * (non-PHPdoc).
1408
     *
1409
     * @see Alpha\Model\ActiveRecordProviderInterface::dropTable()
1410
     */
1411
    public function dropTable($tableName = null)
1412
    {
1413
        self::$logger->debug('>>dropTable()');
1414
1415
        if ($tableName === null) {
1416
            $tableName = $this->record->getTableName();
1417
        }
1418
1419
        $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.';';
1420
1421
        $this->record->setLastQuery($sqlQuery);
1422
1423
        if (!$result = self::getConnection()->query($sqlQuery)) {
1424
            self::$logger->debug('<<dropTable');
1425
            throw new AlphaException('Failed to drop the table ['.$tableName.'] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1426
        }
1427
1428
        if ($this->record->getMaintainHistory()) {
1429
            $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.'_history;';
1430
1431
            $this->record->setLastQuery($sqlQuery);
1432
1433
            if (!$result = self::getConnection()->query($sqlQuery)) {
1434
                self::$logger->debug('<<dropTable');
1435
                throw new AlphaException('Failed to drop the table ['.$tableName.'_history] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1436
            }
1437
        }
1438
1439
        self::$logger->debug('<<dropTable');
1440
    }
1441
1442
    /**
1443
     * (non-PHPdoc).
1444
     *
1445
     * @see Alpha\Model\ActiveRecordProviderInterface::addProperty()
1446
     */
1447
    public function addProperty($propName)
1448
    {
1449
        self::$logger->debug('>>addProperty(propName=['.$propName.'])');
1450
1451
        $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD ';
1452
1453
        if ($this->isTableOverloaded() && $propName == 'classname') {
1454
            $sqlQuery .= 'classname VARCHAR(100)';
1455
        } else {
1456
            if (!in_array($propName, $this->record->getDefaultAttributes()) && !in_array($propName, $this->record->getTransientAttributes())) {
1457
                $prop = $this->record->getPropObject($propName);
1458
1459
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1460
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL';
1461
                } elseif ($prop instanceof Integer) {
1462
                    $sqlQuery .= "$propName INT(".$prop->getSize().')';
1463
                } elseif ($prop instanceof Double) {
1464
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).')';
1465
                } elseif ($prop instanceof SmallText) {
1466
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8';
1467
                } elseif ($prop instanceof Text) {
1468
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8";
1469
                } elseif ($prop instanceof Boolean) {
1470
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0'";
1471
                } elseif ($prop instanceof Date) {
1472
                    $sqlQuery .= "$propName DATE";
1473
                } elseif ($prop instanceof Timestamp) {
1474
                    $sqlQuery .= "$propName DATETIME";
1475
                } elseif ($prop instanceof Enum) {
1476
                    $sqlQuery .= "$propName ENUM(";
1477
                    $enumVals = $prop->getOptions();
1478
                    foreach ($enumVals as $val) {
1479
                        $sqlQuery .= "'".$val."',";
1480
                    }
1481
                    $sqlQuery = rtrim($sqlQuery, ',');
1482
                    $sqlQuery .= ') CHARACTER SET utf8';
1483
                } elseif ($prop instanceof DEnum) {
1484
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1485
                    $denum->saveIfNew();
1486
                    $sqlQuery .= "$propName INT(11) ZEROFILL";
1487
                } elseif ($prop instanceof Relation) {
1488
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED";
1489
                } else {
1490
                    $sqlQuery .= '';
1491
                }
1492
            }
1493
        }
1494
1495
        $this->record->setLastQuery($sqlQuery);
1496
1497
        if (!$result = self::getConnection()->query($sqlQuery)) {
1498
            self::$logger->debug('<<addProperty');
1499
            throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1500
        } else {
1501
            self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'] table for the class ['.get_class($this->record).']');
1502
        }
1503
1504
        if ($this->record->getMaintainHistory()) {
1505
            $sqlQuery = str_replace($this->record->getTableName(), $this->record->getTableName().'_history', $sqlQuery);
1506
1507
            if (!$result = self::getConnection()->query($sqlQuery)) {
1508
                self::$logger->debug('<<addProperty');
1509
                throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'_history], query is ['.$this->record->getLastQuery().']');
1510
            } else {
1511
                self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'_history] table for the class ['.get_class($this->record).']');
1512
            }
1513
        }
1514
1515
        self::$logger->debug('<<addProperty');
1516
    }
1517
1518
    /**
1519
     * (non-PHPdoc).
1520
     *
1521
     * @see Alpha\Model\ActiveRecordProviderInterface::getMAX()
1522
     */
1523
    public function getMAX()
1524
    {
1525
        self::$logger->debug('>>getMAX()');
1526
1527
        $sqlQuery = 'SELECT MAX(ID) AS max_ID FROM '.$this->record->getTableName();
1528
1529
        $this->record->setLastQuery($sqlQuery);
1530
1531
        try {
1532
            $result = $this->record->query($sqlQuery);
1533
1534
            $row = $result[0];
1535
1536
            if (isset($row['max_ID'])) {
1537
                self::$logger->debug('<<getMAX ['.$row['max_ID'].']');
1538
1539
                return $row['max_ID'];
1540
            } else {
1541
                throw new AlphaException('Failed to get the MAX ID for the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1542
            }
1543
        } catch (\Exception $e) {
1544
            self::$logger->debug('<<getMAX');
1545
            throw new AlphaException($e->getMessage());
1546
        }
1547
    }
1548
1549
    /**
1550
     * (non-PHPdoc).
1551
     *
1552
     * @see Alpha\Model\ActiveRecordProviderInterface::getCount()
1553
     */
1554
    public function getCount($attributes = array(), $values = array())
1555
    {
1556
        self::$logger->debug('>>getCount(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'])');
1557
1558
        if ($this->record->isTableOverloaded()) {
1559
            $whereClause = ' WHERE classname = \''.addslashes(get_class($this->record)).'\' AND';
1560
        } else {
1561
            $whereClause = ' WHERE';
1562
        }
1563
1564
        $count = count($attributes);
1565
1566
        for ($i = 0; $i < $count; ++$i) {
1567
            $whereClause .= ' '.$attributes[$i].' = \''.$values[$i].'\' AND';
1568
            self::$logger->debug($whereClause);
1569
        }
1570
        // remove the last " AND"
1571
        $whereClause = mb_substr($whereClause, 0, -4);
1572
1573
        if ($whereClause != ' WHERE') {
1574
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName().$whereClause;
1575
        } else {
1576
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName();
1577
        }
1578
1579
        $this->record->setLastQuery($sqlQuery);
1580
1581
        $result = self::getConnection()->query($sqlQuery);
1582
1583
        if ($result) {
1584
            $row = $result->fetch_array(MYSQLI_ASSOC);
1585
1586
            self::$logger->debug('<<getCount ['.$row['class_count'].']');
1587
1588
            return $row['class_count'];
1589
        } else {
1590
            self::$logger->debug('<<getCount');
1591
            throw new AlphaException('Failed to get the count for the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1592
        }
1593
    }
1594
1595
    /**
1596
     * (non-PHPdoc).
1597
     *
1598
     * @see Alpha\Model\ActiveRecordProviderInterface::getHistoryCount()
1599
     */
1600
    public function getHistoryCount()
1601
    {
1602
        self::$logger->debug('>>getHistoryCount()');
1603
1604
        if (!$this->record->getMaintainHistory()) {
1605
            throw new AlphaException('getHistoryCount method called on a DAO where no history is maintained!');
1606
        }
1607
1608
        $sqlQuery = 'SELECT COUNT(ID) AS object_count FROM '.$this->record->getTableName().'_history WHERE ID='.$this->record->getID();
1609
1610
        $this->record->setLastQuery($sqlQuery);
1611
1612
        $result = self::getConnection()->query($sqlQuery);
1613
1614
        if ($result) {
1615
            $row = $result->fetch_array(MYSQLI_ASSOC);
1616
1617
            self::$logger->debug('<<getHistoryCount ['.$row['object_count'].']');
1618
1619
            return $row['object_count'];
1620
        } else {
1621
            self::$logger->debug('<<getHistoryCount');
1622
            throw new AlphaException('Failed to get the history count for the business object ['.$this->record->getID().'] from the table ['.$this->record->getTableName().'_history], query is ['.$this->record->getLastQuery().']');
1623
        }
1624
    }
1625
1626
    /**
1627
     * (non-PHPdoc).
1628
     *
1629
     * @see Alpha\Model\ActiveRecordProviderInterface::setEnumOptions()
1630
     * @since 1.1
1631
     */
1632
    public function setEnumOptions()
1633
    {
1634
        self::$logger->debug('>>setEnumOptions()');
1635
1636
        // get the class attributes
1637
        $reflection = new ReflectionClass(get_class($this->record));
1638
        $properties = $reflection->getProperties();
1639
1640
        // flag for any database errors
1641
        $dbError = false;
1642
1643
        foreach ($properties as $propObj) {
1644
            $propName = $propObj->name;
1645
            if (!in_array($propName, $this->record->getDefaultAttributes()) && !in_array($propName, $this->record->getTransientAttributes())) {
1646
                $propClass = get_class($this->record->getPropObject($propName));
1647
                if ($propClass == 'Enum') {
1648
                    $sqlQuery = 'SHOW COLUMNS FROM '.$this->record->getTableName()." LIKE '$propName'";
1649
1650
                    $this->record->setLastQuery($sqlQuery);
1651
1652
                    $result = self::getConnection()->query($sqlQuery);
1653
1654
                    if ($result) {
1655
                        $row = $result->fetch_array(MYSQLI_NUM);
1656
                        $options = explode("','", preg_replace("/(enum|set)\('(.+?)'\)/", '\\2', $row[1]));
1657
1658
                        $this->record->getPropObject($propName)->setOptions($options);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Alpha\Model\Type\Type as the method setOptions() does only exist in the following sub-classes of Alpha\Model\Type\Type: Alpha\Model\Type\Enum. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1659
                    } else {
1660
                        $dbError = true;
1661
                        break;
1662
                    }
1663
                }
1664
            }
1665
        }
1666
1667
        if (!$dbError) {
1668
            if (method_exists($this, 'after_setEnumOptions_callback')) {
1669
                $this->{'after_setEnumOptions_callback'}();
1670
            }
1671
        } else {
1672
            throw new AlphaException('Failed to load enum options correctly for object instance of class ['.get_class($this).']');
1673
        }
1674
        self::$logger->debug('<<setEnumOptions');
1675
    }
1676
1677
    /**
1678
     * (non-PHPdoc).
1679
     *
1680
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableExists()
1681
     */
1682
    public function checkTableExists($checkHistoryTable = false)
1683
    {
1684
        self::$logger->debug('>>checkTableExists(checkHistoryTable=['.$checkHistoryTable.'])');
1685
1686
        $tableExists = false;
1687
1688
        $sqlQuery = 'SHOW TABLES;';
1689
        $this->record->setLastQuery($sqlQuery);
1690
1691
        $result = self::getConnection()->query($sqlQuery);
1692
1693
        if ($result) {
1694
            $tableName = ($checkHistoryTable ? $this->record->getTableName().'_history' : $this->record->getTableName());
1695
1696
            while ($row = $result->fetch_array(MYSQLI_NUM)) {
1697
                if (strtolower($row[0]) == mb_strtolower($tableName)) {
1698
                    $tableExists = true;
1699
                }
1700
            }
1701
1702
            self::$logger->debug('<<checkTableExists ['.$tableExists.']');
1703
1704
            return $tableExists;
1705
        } else {
1706
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1707
        }
1708
    }
1709
1710
    /**
1711
     * (non-PHPdoc).
1712
     *
1713
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordTableExists()
1714
     */
1715
    public static function checkRecordTableExists($RecordClassName, $checkHistoryTable = false)
1716
    {
1717
        if (self::$logger == null) {
1718
            self::$logger = new Logger('ActiveRecordProviderMySQL');
1719
        }
1720
        self::$logger->debug('>>checkRecordTableExists(RecordClassName=['.$RecordClassName.'], checkHistoryTable=['.$checkHistoryTable.'])');
1721
1722
        if (!class_exists($RecordClassName)) {
1723
            throw new IllegalArguementException('The classname provided ['.$checkHistoryTable.'] is not defined!');
1724
        }
1725
1726
        $tableName = $RecordClassName::TABLE_NAME;
1727
1728
        if (empty($tableName)) {
1729
            $tableName = mb_substr($RecordClassName, 0, mb_strpos($RecordClassName, '_'));
1730
        }
1731
1732
        if ($checkHistoryTable) {
1733
            $tableName .= '_history';
1734
        }
1735
1736
        $tableExists = false;
1737
1738
        $sqlQuery = 'SHOW TABLES;';
1739
1740
        $result = self::getConnection()->query($sqlQuery);
1741
1742
        while ($row = $result->fetch_array(MYSQLI_NUM)) {
1743
            if ($row[0] == $tableName) {
1744
                $tableExists = true;
1745
            }
1746
        }
1747
1748
        if ($result) {
1749
            self::$logger->debug('<<checkRecordTableExists ['.($tableExists ? 'true' : 'false').']');
1750
1751
            return $tableExists;
1752
        } else {
1753
            self::$logger->debug('<<checkRecordTableExists');
1754
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1755
        }
1756
    }
1757
1758
    /**
1759
     * (non-PHPdoc).
1760
     *
1761
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableNeedsUpdate()
1762
     */
1763
    public function checkTableNeedsUpdate()
1764
    {
1765
        self::$logger->debug('>>checkTableNeedsUpdate()');
1766
1767
        $updateRequired = false;
1768
1769
        $matchCount = 0;
1770
1771
        $query = 'SHOW COLUMNS FROM '.$this->record->getTableName();
1772
        $result = self::getConnection()->query($query);
1773
        $this->record->setLastQuery($query);
1774
1775
        // get the class attributes
1776
        $reflection = new ReflectionClass(get_class($this->record));
1777
        $properties = $reflection->getProperties();
1778
1779
        foreach ($properties as $propObj) {
1780
            $propName = $propObj->name;
1781
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1782
                $foundMatch = false;
1783
1784
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1785
                    if ($propName == $row['Field']) {
1786
                        $foundMatch = true;
1787
                        break;
1788
                    }
1789
                }
1790
1791
                if (!$foundMatch) {
1792
                    --$matchCount;
1793
                }
1794
1795
                $result->data_seek(0);
1796
            }
1797
        }
1798
1799
        // check for the "classname" field in overloaded tables
1800
        if ($this->record->isTableOverloaded()) {
1801
            $foundMatch = false;
1802
1803
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1804
                if ('classname' == $row['Field']) {
1805
                    $foundMatch = true;
1806
                    break;
1807
                }
1808
            }
1809
            if (!$foundMatch) {
1810
                --$matchCount;
1811
            }
1812
        }
1813
1814
        if ($matchCount != 0) {
1815
            $updateRequired = true;
1816
        }
1817
1818
        if ($result) {
1819
            // check the table indexes
1820
            try {
1821
                $this->checkIndexes();
1822
            } catch (AlphaException $ae) {
1823
                self::$logger->warn("Error while checking database indexes:\n\n".$ae->getMessage());
1824
            }
1825
1826
            self::$logger->debug('<<checkTableNeedsUpdate ['.$updateRequired.']');
1827
1828
            return $updateRequired;
1829
        } else {
1830
            self::$logger->debug('<<checkTableNeedsUpdate');
1831
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1832
        }
1833
    }
1834
1835
    /**
1836
     * (non-PHPdoc).
1837
     *
1838
     * @see Alpha\Model\ActiveRecordProviderInterface::findMissingFields()
1839
     */
1840
    public function findMissingFields()
1841
    {
1842
        self::$logger->debug('>>findMissingFields()');
1843
1844
        $missingFields = array();
1845
        $matchCount = 0;
1846
1847
        $sqlQuery = 'SHOW COLUMNS FROM '.$this->record->getTableName();
1848
1849
        $result = self::getConnection()->query($sqlQuery);
1850
1851
        $this->record->setLastQuery($sqlQuery);
1852
1853
        // get the class attributes
1854
        $reflection = new ReflectionClass(get_class($this->record));
1855
        $properties = $reflection->getProperties();
1856
1857
        foreach ($properties as $propObj) {
1858
            $propName = $propObj->name;
1859
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1860
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1861
                    if ($propName == $row['Field']) {
1862
                        ++$matchCount;
1863
                        break;
1864
                    }
1865
                }
1866
                $result->data_seek(0);
1867
            } else {
1868
                ++$matchCount;
1869
            }
1870
1871
            if ($matchCount == 0) {
1872
                array_push($missingFields, $propName);
1873
            } else {
1874
                $matchCount = 0;
1875
            }
1876
        }
1877
1878
        // check for the "classname" field in overloaded tables
1879
        if ($this->record->isTableOverloaded()) {
1880
            $foundMatch = false;
1881
1882
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1883
                if ('classname' == $row['Field']) {
1884
                    $foundMatch = true;
1885
                    break;
1886
                }
1887
            }
1888
            if (!$foundMatch) {
1889
                array_push($missingFields, 'classname');
1890
            }
1891
        }
1892
1893
        if (!$result) {
1894
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1895
        }
1896
1897
        self::$logger->debug('<<findMissingFields ['.var_export($missingFields, true).']');
1898
1899
        return $missingFields;
1900
    }
1901
1902
    /**
1903
     * (non-PHPdoc).
1904
     *
1905
     * @see Alpha\Model\ActiveRecordProviderInterface::getIndexes()
1906
     */
1907
    public function getIndexes()
1908
    {
1909
        self::$logger->debug('>>getIndexes()');
1910
1911
        $query = 'SHOW INDEX FROM '.$this->record->getTableName();
1912
1913
        $result = self::getConnection()->query($query);
1914
1915
        $this->record->setLastQuery($query);
1916
1917
        $indexNames = array();
1918
1919
        if (!$result) {
1920
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1921
        } else {
1922
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1923
                array_push($indexNames, $row['Key_name']);
1924
            }
1925
        }
1926
1927
        self::$logger->debug('<<getIndexes');
1928
1929
        return $indexNames;
1930
    }
1931
1932
    /**
1933
     * Checks to see if all of the indexes are in place for the record's table, creates those that are missing.
1934
     *
1935
     * @since 1.1
1936
     */
1937
    private function checkIndexes()
1938
    {
1939
        self::$logger->debug('>>checkIndexes()');
1940
1941
        $indexNames = $this->getIndexes();
1942
1943
        // process unique keys
1944
        foreach ($this->record->getUniqueAttributes() as $prop) {
1945
            // check for composite indexes
1946
            if (mb_strpos($prop, '+')) {
1947
                $attributes = explode('+', $prop);
1948
1949
                $index_exists = false;
1950
                foreach ($indexNames as $index) {
1951
                    if ($attributes[0].'_'.$attributes[1].'_unq_idx' == $index) {
1952
                        $index_exists = true;
1953
                    }
1954
                    if (count($attributes) == 3) {
1955
                        if ($attributes[0].'_'.$attributes[1].'_'.$attributes[2].'_unq_idx' == $index) {
1956
                            $index_exists = true;
1957
                        }
1958
                    }
1959
                }
1960
1961
                if (!$index_exists) {
1962
                    if (count($attributes) == 3) {
1963
                        $this->record->createUniqueIndex($attributes[0], $attributes[1], $attributes[2]);
1964
                    } else {
1965
                        $this->record->createUniqueIndex($attributes[0], $attributes[1]);
1966
                    }
1967
                }
1968
            } else {
1969
                $index_exists = false;
1970
                foreach ($indexNames as $index) {
1971
                    if ($prop.'_unq_idx' == $index) {
1972
                        $index_exists = true;
1973
                    }
1974
                }
1975
1976
                if (!$index_exists) {
1977
                    $this->createUniqueIndex($prop);
1978
                }
1979
            }
1980
        }
1981
1982
        // process foreign-key indexes
1983
        // get the class attributes
1984
        $reflection = new ReflectionClass(get_class($this->record));
1985
        $properties = $reflection->getProperties();
1986
1987
        foreach ($properties as $propObj) {
1988
            $propName = $propObj->name;
1989
            $prop = $this->record->getPropObject($propName);
1990
            if ($prop instanceof Relation) {
1991
                if ($prop->getRelationType() == 'MANY-TO-ONE') {
1992
                    $indexExists = false;
1993
                    foreach ($indexNames as $index) {
1994
                        if ($this->record->getTableName().'_'.$propName.'_fk_idx' == $index) {
1995
                            $indexExists = true;
1996
                        }
1997
                    }
1998
1999
                    if (!$indexExists) {
2000
                        $this->createForeignIndex($propName, $prop->getRelatedClass(), $prop->getRelatedClassField());
2001
                    }
2002
                }
2003
2004
                if ($prop->getRelationType() == 'MANY-TO-MANY') {
2005
                    $lookup = $prop->getLookup();
2006
2007
                    if ($lookup != null) {
2008
                        try {
2009
                            $lookupIndexNames = $lookup->getIndexes();
2010
2011
                            // handle index check/creation on left side of Relation
2012
                            $indexExists = false;
2013
                            foreach ($lookupIndexNames as $index) {
2014
                                if ($lookup->getTableName().'_leftID_fk_idx' == $index) {
2015
                                    $indexExists = true;
2016
                                }
2017
                            }
2018
2019
                            if (!$indexExists) {
2020
                                $lookup->createForeignIndex('leftID', $prop->getRelatedClass('left'), 'ID');
2021
                            }
2022
2023
                            // handle index check/creation on right side of Relation
2024
                            $indexExists = false;
2025
                            foreach ($lookupIndexNames as $index) {
2026
                                if ($lookup->getTableName().'_rightID_fk_idx' == $index) {
2027
                                    $indexExists = true;
2028
                                }
2029
                            }
2030
2031
                            if (!$indexExists) {
2032
                                $lookup->createForeignIndex('rightID', $prop->getRelatedClass('right'), 'ID');
2033
                            }
2034
                        } catch (AlphaException $e) {
2035
                            self::$logger->error($e->getMessage());
2036
                        }
2037
                    }
2038
                }
2039
            }
2040
        }
2041
2042
        self::$logger->debug('<<checkIndexes');
2043
    }
2044
2045
    /**
2046
     * (non-PHPdoc).
2047
     *
2048
     * @see Alpha\Model\ActiveRecordProviderInterface::createForeignIndex()
2049
     */
2050
    public function createForeignIndex($attributeName, $relatedClass, $relatedClassAttribute, $indexName = null)
2051
    {
2052
        self::$logger->debug('>>createForeignIndex(attributeName=['.$attributeName.'], relatedClass=['.$relatedClass.'], relatedClassAttribute=['.$relatedClassAttribute.'], indexName=['.$indexName.']');
2053
2054
        $relatedRecord = new $relatedClass();
2055
        $tableName = $relatedRecord->getTableName();
2056
2057
        $result = false;
2058
2059
        if (self::checkRecordTableExists($relatedClass)) {
2060
            $sqlQuery = '';
2061
2062
            if ($attributeName == 'leftID') {
2063
                if ($indexName === null) {
2064
                    $indexName = $this->record->getTableName().'_leftID_fk_idx';
2065
                }
2066
                $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD INDEX '.$indexName.' (leftID);';
2067
            }
2068
            if ($attributeName == 'rightID') {
2069
                if ($indexName === null) {
2070
                    $indexName = $this->record->getTableName().'_rightID_fk_idx';
2071
                }
2072
                $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD INDEX '.$indexName.' (rightID);';
2073
            }
2074
2075
            if (!empty($sqlQuery)) {
2076
                $this->record->setLastQuery($sqlQuery);
2077
2078
                $result = self::getConnection()->query($sqlQuery);
2079
2080
                if (!$result) {
2081
                    throw new FailedIndexCreateException('Failed to create an index on ['.$this->record->getTableName().'], error is ['.self::getConnection()->error.'], query ['.$this->record->getLastQuery().']');
2082
                }
2083
            }
2084
2085
            if ($indexName === null) {
2086
                $indexName = $this->record->getTableName().'_'.$attributeName.'_fk_idx';
2087
            }
2088
2089
            $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD FOREIGN KEY '.$indexName.' ('.$attributeName.') REFERENCES '.$tableName.' ('.$relatedClassAttribute.') ON DELETE SET NULL;';
2090
2091
            $this->record->setLastQuery($sqlQuery);
2092
            $result = self::getConnection()->query($sqlQuery);
2093
        }
2094
2095
        if ($result) {
2096
            self::$logger->debug('Successfully created the foreign key index ['.$indexName.']');
2097
        } else {
2098
            throw new FailedIndexCreateException('Failed to create the index ['.$indexName.'] on ['.$this->record->getTableName().'], error is ['.self::getConnection()->error.'], query ['.$this->record->getLastQuery().']');
2099
        }
2100
2101
        self::$logger->debug('<<createForeignIndex');
2102
    }
2103
2104
    /**
2105
     * (non-PHPdoc).
2106
     *
2107
     * @see Alpha\Model\ActiveRecordProviderInterface::createUniqueIndex()
2108
     */
2109
    public function createUniqueIndex($attribute1Name, $attribute2Name = '', $attribute3Name = '')
2110
    {
2111
        self::$logger->debug('>>createUniqueIndex(attribute1Name=['.$attribute1Name.'], attribute2Name=['.$attribute2Name.'], attribute3Name=['.$attribute3Name.'])');
2112
2113
        $sqlQuery = '';
2114
2115
        if ($attribute2Name != '' && $attribute3Name != '') {
2116
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_'.$attribute2Name.'_'.$attribute3Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.','.$attribute2Name.','.$attribute3Name.');';
2117
        }
2118
2119
        if ($attribute2Name != '' && $attribute3Name == '') {
2120
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_'.$attribute2Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.','.$attribute2Name.');';
2121
        }
2122
2123
        if ($attribute2Name == '' && $attribute3Name == '') {
2124
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.');';
2125
        }
2126
2127
        $this->record->setLastQuery($sqlQuery);
2128
2129
        $result = self::getConnection()->query($sqlQuery);
2130
2131
        if ($result) {
2132
            self::$logger->debug('Successfully created the unique index on ['.$this->record->getTableName().']');
2133
        } else {
2134
            throw new FailedIndexCreateException('Failed to create the unique index on ['.$this->record->getTableName().'], error is ['.self::getConnection()->error.']');
2135
        }
2136
2137
        self::$logger->debug('<<createUniqueIndex');
2138
    }
2139
2140
    /**
2141
     * (non-PHPdoc).
2142
     *
2143
     * @see Alpha\Model\ActiveRecordProviderInterface::reload()
2144
     */
2145
    public function reload()
2146
    {
2147
        self::$logger->debug('>>reload()');
2148
2149
        if (!$this->record->isTransient()) {
2150
            $this->record->load($this->record->getID());
2151
        } else {
2152
            throw new AlphaException('Cannot reload transient object from database!');
2153
        }
2154
        self::$logger->debug('<<reload');
2155
    }
2156
2157
    /**
2158
     * (non-PHPdoc).
2159
     *
2160
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordExists()
2161
     */
2162
    public function checkRecordExists($ID)
2163
    {
2164
        self::$logger->debug('>>checkRecordExists(ID=['.$ID.'])');
2165
2166
        $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE ID = ?;';
2167
2168
        $this->record->setLastQuery($sqlQuery);
2169
2170
        $stmt = self::getConnection()->stmt_init();
2171
2172
        if ($stmt->prepare($sqlQuery)) {
2173
            $stmt->bind_param('i', $ID);
2174
2175
            $stmt->execute();
2176
2177
            $result = $this->bindResult($stmt);
2178
2179
            $stmt->close();
2180
2181
            if (is_array($result)) {
2182
                if (count($result) > 0) {
2183
                    self::$logger->debug('<<checkRecordExists [true]');
2184
2185
                    return true;
2186
                } else {
2187
                    self::$logger->debug('<<checkRecordExists [false]');
2188
2189
                    return false;
2190
                }
2191
            } else {
2192
                self::$logger->debug('<<checkRecordExists');
2193
                throw new AlphaException('Failed to check for the record ['.$ID.'] on the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
2194
            }
2195
        } else {
2196
            self::$logger->debug('<<checkRecordExists');
2197
            throw new AlphaException('Failed to check for the record ['.$ID.'] on the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
2198
        }
2199
    }
2200
2201
    /**
2202
     * (non-PHPdoc).
2203
     *
2204
     * @see Alpha\Model\ActiveRecordProviderInterface::isTableOverloaded()
2205
     */
2206
    public function isTableOverloaded()
2207
    {
2208
        self::$logger->debug('>>isTableOverloaded()');
2209
2210
        $reflection = new ReflectionClass($this->record);
2211
        $classname = $reflection->getShortName();
2212
        $tablename = ucfirst($this->record->getTableName());
2213
2214
        // use reflection to check to see if we are dealing with a persistent type (e.g. DEnum) which are never overloaded
2215
        $implementedInterfaces = $reflection->getInterfaces();
2216
2217
        foreach ($implementedInterfaces as $interface) {
2218
            if ($interface->name == 'Alpha\Model\Type\TypeInterface') {
2219
                self::$logger->debug('<<isTableOverloaded [false]');
2220
2221
                return false;
2222
            }
2223
        }
2224
2225
        if ($classname != $tablename) {
2226
            // loop over all records to see if there is one using the same table as this record
2227
2228
            $Recordclasses = ActiveRecord::getRecordClassNames();
2229
2230
            foreach ($Recordclasses as $RecordclassName) {
2231
                $reflection = new ReflectionClass($RecordclassName);
2232
                $classname = $reflection->getShortName();
2233
                if ($tablename == $classname) {
2234
                    self::$logger->debug('<<isTableOverloaded [true]');
2235
2236
                    return true;
2237
                }
2238
            }
2239
2240
            self::$logger->debug('<<isTableOverloaded');
2241
            throw new BadTableNameException('The table name ['.$tablename.'] for the class ['.$classname.'] is invalid as it does not match a Record definition in the system!');
2242
        } else {
2243
            // check to see if there is already a "classname" column in the database for this record
2244
2245
            $query = 'SHOW COLUMNS FROM '.$this->record->getTableName();
2246
2247
            $result = self::getConnection()->query($query);
2248
2249
            if ($result) {
2250
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
2251
                    if ('classname' == $row['Field']) {
2252
                        self::$logger->debug('<<isTableOverloaded [true]');
2253
2254
                        return true;
2255
                    }
2256
                }
2257
            } else {
2258
                self::$logger->warn('Error during show columns ['.self::getConnection()->error.']');
2259
            }
2260
2261
            self::$logger->debug('<<isTableOverloaded [false]');
2262
2263
            return false;
2264
        }
2265
    }
2266
2267
    /**
2268
     * (non-PHPdoc).
2269
     *
2270
     * @see Alpha\Model\ActiveRecordProviderInterface::begin()
2271
     */
2272
    public static function begin()
2273
    {
2274
        if (self::$logger == null) {
2275
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2276
        }
2277
        self::$logger->debug('>>begin()');
2278
2279
        if (!self::getConnection()->autocommit(false)) {
2280
            throw new AlphaException('Error beginning a new transaction, error is ['.self::getConnection()->error.']');
2281
        }
2282
2283
        self::$logger->debug('<<begin');
2284
    }
2285
2286
    /**
2287
     * (non-PHPdoc).
2288
     *
2289
     * @see Alpha\Model\ActiveRecordProviderInterface::commit()
2290
     */
2291
    public static function commit()
2292
    {
2293
        if (self::$logger == null) {
2294
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2295
        }
2296
        self::$logger->debug('>>commit()');
2297
2298
        if (!self::getConnection()->commit()) {
2299
            throw new FailedSaveException('Error commiting a transaction, error is ['.self::getConnection()->error.']');
2300
        }
2301
2302
        self::$logger->debug('<<commit');
2303
    }
2304
2305
    /**
2306
     * (non-PHPdoc).
2307
     *
2308
     * @see Alpha\Model\ActiveRecordProviderInterface::rollback()
2309
     */
2310
    public static function rollback()
2311
    {
2312
        if (self::$logger == null) {
2313
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2314
        }
2315
        self::$logger->debug('>>rollback()');
2316
2317
        if (!self::getConnection()->rollback()) {
2318
            throw new AlphaException('Error rolling back a transaction, error is ['.self::getConnection()->error.']');
2319
        }
2320
2321
        self::$logger->debug('<<rollback');
2322
    }
2323
2324
    /**
2325
     * (non-PHPdoc).
2326
     *
2327
     * @see Alpha\Model\ActiveRecordProviderInterface::setRecord()
2328
     */
2329
    public function setRecord($Record)
2330
    {
2331
        $this->record = $Record;
2332
    }
2333
2334
    /**
2335
     * Dynamically binds all of the attributes for the current Record to the supplied prepared statement
2336
     * parameters.  If arrays of attribute names and values are provided, only those will be bound to
2337
     * the supplied statement.
2338
     *
2339
     * @param \mysqli_stmt $stmt The SQL statement to bind to.
2340
     * @param array Optional array of Record attributes.
2341
     * @param array Optional array of Record values.
2342
     *
2343
     * @return \mysqli_stmt
2344
     *
2345
     * @since 1.1
2346
     */
2347
    private function bindParams($stmt, $attributes = array(), $values = array())
2348
    {
2349
        self::$logger->debug('>>bindParams(stmt=['.var_export($stmt, true).'])');
2350
2351
        $bindingsTypes = '';
2352
        $params = array();
2353
2354
        // here we are only binding the supplied attributes
2355
        if (count($attributes) > 0 && count($attributes) == count($values)) {
2356
            $count = count($values);
2357
2358
            for ($i = 0; $i < $count; ++$i) {
2359
                if (Validator::isInteger($values[$i])) {
2360
                    $bindingsTypes .= 'i';
2361
                } else {
2362
                    $bindingsTypes .= 's';
2363
                }
2364
                array_push($params, $values[$i]);
2365
            }
2366
2367
            if ($this->record->isTableOverloaded()) {
2368
                $bindingsTypes .= 's';
2369
                array_push($params, get_class($this->record));
2370
            }
2371
        } else { // bind all attributes on the business object
2372
2373
            // get the class attributes
2374
            $reflection = new ReflectionClass(get_class($this->record));
2375
            $properties = $reflection->getProperties();
2376
2377
            foreach ($properties as $propObj) {
2378
                $propName = $propObj->name;
2379
                if (!in_array($propName, $this->record->getTransientAttributes())) {
2380
                    // Skip the ID, database auto number takes care of this.
2381
                    if ($propName != 'ID' && $propName != 'version_num') {
2382
                        if ($this->record->getPropObject($propName) instanceof Integer) {
2383
                            $bindingsTypes .= 'i';
2384
                        } else {
2385
                            $bindingsTypes .= 's';
2386
                        }
2387
                        array_push($params, $this->record->get($propName));
2388
                    }
2389
2390
                    if ($propName == 'version_num') {
2391
                        $temp = $this->record->getVersionNumber()->getValue();
2392
                        $this->record->set('version_num', $temp+1);
2393
                        $bindingsTypes .= 'i';
2394
                        array_push($params, $this->record->getVersionNumber()->getValue());
2395
                    }
2396
                }
2397
            }
2398
2399
            if ($this->record->isTableOverloaded()) {
2400
                $bindingsTypes .= 's';
2401
                array_push($params, get_class($this->record));
2402
            }
2403
2404
            // the ID may be on the WHERE clause for UPDATEs and DELETEs
2405
            if (!$this->record->isTransient()) {
2406
                $bindingsTypes .= 'i';
2407
                array_push($params, $this->record->getID());
2408
            }
2409
        }
2410
2411
        self::$logger->debug('bindingsTypes=['.$bindingsTypes.'], count: ['.mb_strlen($bindingsTypes).']');
2412
        self::$logger->debug('params ['.var_export($params, true).']');
2413
2414
        if ($params != null) {
2415
            $bind_names[] = $bindingsTypes;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$bind_names was never initialized. Although not strictly required by PHP, it is generally a good practice to add $bind_names = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
2416
2417
            $count = count($params);
2418
2419
            for ($i = 0; $i < $count; ++$i) {
2420
                $bind_name = 'bind'.$i;
2421
                $$bind_name = $params[$i];
2422
                $bind_names[] = &$$bind_name;
2423
            }
2424
2425
            call_user_func_array(array($stmt, 'bind_param'), $bind_names);
2426
        }
2427
2428
        self::$logger->debug('<<bindParams ['.var_export($stmt, true).']');
2429
2430
        return $stmt;
2431
    }
2432
2433
    /**
2434
     * Dynamically binds the result of the supplied prepared statement to a 2d array, where each element in the array is another array
2435
     * representing a database row.
2436
     *
2437
     * @param \mysqli_stmt $stmt
2438
     *
2439
     * @return array A 2D array containing the query result.
2440
     *
2441
     * @since 1.1
2442
     */
2443
    private function bindResult($stmt)
2444
    {
2445
        $result = array();
2446
2447
        $metadata = $stmt->result_metadata();
2448
        $fields = $metadata->fetch_fields();
2449
2450
        while (true) {
2451
            $pointers = array();
2452
            $row = array();
2453
2454
            $pointers[] = $stmt;
2455
            foreach ($fields as $field) {
2456
                $fieldname = $field->name;
2457
                $pointers[] = &$row[$fieldname];
2458
            }
2459
2460
            call_user_func_array('mysqli_stmt_bind_result', $pointers);
2461
2462
            if (!$stmt->fetch()) {
2463
                break;
2464
            }
2465
2466
            $result[] = $row;
2467
        }
2468
2469
        $metadata->free();
2470
2471
        return $result;
2472
    }
2473
2474
    /**
2475
     * Parses a MySQL error for the value that violated a unique constraint.
2476
     *
2477
     * @param string $error The MySQL error string.
2478
     *
2479
     * @since 1.1
2480
     */
2481
    private function findOffendingValue($error)
2482
    {
2483
        self::$logger->debug('>>findOffendingValue(error=['.$error.'])');
2484
2485
        $singleQuote1 = mb_strpos($error, "'");
2486
        $singleQuote2 = mb_strrpos($error, "'");
2487
2488
        $value = mb_substr($error, $singleQuote1, ($singleQuote2-$singleQuote1)+1);
2489
        self::$logger->debug('<<findOffendingValue ['.$value.'])');
2490
2491
        return $value;
2492
    }
2493
2494
    /**
2495
     * (non-PHPdoc).
2496
     *
2497
     * @see Alpha\Model\ActiveRecordProviderInterface::checkDatabaseExists()
2498
     */
2499
    public static function checkDatabaseExists()
2500
    {
2501
        $config = ConfigProvider::getInstance();
2502
2503
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2504
2505
        $result = $connection->query('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \''.$config->get('db.name').'\'');
2506
2507
        if (count($result) > 0) {
2508
            return true;
2509
        } else {
2510
            return false;
2511
        }
2512
    }
2513
2514
    /**
2515
     * (non-PHPdoc).
2516
     *
2517
     * @see Alpha\Model\ActiveRecordProviderInterface::createDatabase()
2518
     */
2519
    public static function createDatabase()
2520
    {
2521
        $config = ConfigProvider::getInstance();
2522
2523
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2524
2525
        $connection->query('CREATE DATABASE '.$config->get('db.name'));
2526
    }
2527
2528
    /**
2529
     * (non-PHPdoc).
2530
     *
2531
     * @see Alpha\Model\ActiveRecordProviderInterface::dropDatabase()
2532
     */
2533
    public static function dropDatabase()
2534
    {
2535
        $config = ConfigProvider::getInstance();
2536
2537
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2538
2539
        $connection->query('DROP DATABASE '.$config->get('db.name'));
2540
    }
2541
2542
    /**
2543
     * (non-PHPdoc).
2544
     *
2545
     * @see Alpha\Model\ActiveRecordProviderInterface::backupDatabase()
2546
     */
2547
    public static function backupDatabase($targetFile)
2548
    {
2549
        $config = ConfigProvider::getInstance();
2550
2551
        exec('mysqldump  --host="'.$config->get('db.hostname').'" --user="'.$config->get('db.username').'" --password="'.$config->get('db.password').'" --opt '.$config->get('db.name').' 2>&1 >'.$targetFile);
2552
    }
2553
}
2554