Completed
Push — develop ( 67789b...2bd525 )
by John
03:25
created

ActiveRecordProviderMySQL::makeTable()   D

Complexity

Conditions 22
Paths 150

Size

Total Lines 75
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 75
c 0
b 0
f 0
rs 4.8882
cc 22
eloc 52
nc 150
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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($checkIndexes = true)
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
        if ($checkIndexes) {
1302
            $this->checkIndexes();
1303
        }
1304
1305
        if ($this->record->getMaintainHistory()) {
1306
            $this->record->makeHistoryTable();
1307
        }
1308
1309
        self::$logger->debug('<<makeTable');
1310
    }
1311
1312
    /**
1313
     * (non-PHPdoc).
1314
     *
1315
     * @see Alpha\Model\ActiveRecordProviderInterface::makeHistoryTable()
1316
     */
1317
    public function makeHistoryTable()
1318
    {
1319
        self::$logger->debug('>>makeHistoryTable()');
1320
1321
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().'_history (ID INT(11) ZEROFILL NOT NULL,';
1322
1323
        // get the class attributes
1324
        $reflection = new ReflectionClass(get_class($this->record));
1325
        $properties = $reflection->getProperties();
1326
1327
        foreach ($properties as $propObj) {
1328
            $propName = $propObj->name;
1329
1330
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1331
                $prop = $this->record->getPropObject($propName);
1332
1333
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1334
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL,';
1335
                } elseif ($prop instanceof Integer) {
1336
                    $sqlQuery .= "$propName INT(".$prop->getSize().'),';
1337
                } elseif ($prop instanceof Double) {
1338
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).'),';
1339
                } elseif ($prop instanceof SmallText) {
1340
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8,';
1341
                } elseif ($prop instanceof Text) {
1342
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8,";
1343
                } elseif ($prop instanceof Boolean) {
1344
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0',";
1345
                } elseif ($prop instanceof Date) {
1346
                    $sqlQuery .= "$propName DATE,";
1347
                } elseif ($prop instanceof Timestamp) {
1348
                    $sqlQuery .= "$propName DATETIME,";
1349
                } elseif ($prop instanceof Enum) {
1350
                    $sqlQuery .= "$propName ENUM(";
1351
                    $enumVals = $prop->getOptions();
1352
                    foreach ($enumVals as $val) {
1353
                        $sqlQuery .= "'".$val."',";
1354
                    }
1355
                    $sqlQuery = rtrim($sqlQuery, ',');
1356
                    $sqlQuery .= ') CHARACTER SET utf8,';
1357
                } elseif ($prop instanceof DEnum) {
1358
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1359
                    $denum->saveIfNew();
1360
                    $sqlQuery .= "$propName INT(11) ZEROFILL,";
1361
                } elseif ($prop instanceof Relation) {
1362
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED,";
1363
                } else {
1364
                    $sqlQuery .= '';
1365
                }
1366
            }
1367
        }
1368
1369
        if ($this->record->isTableOverloaded()) {
1370
            $sqlQuery .= 'classname VARCHAR(100),';
1371
        }
1372
1373
        $sqlQuery .= 'PRIMARY KEY (ID, version_num)) ENGINE=MyISAM DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;';
1374
1375
        $this->record->setLastQuery($sqlQuery);
1376
1377
        if (!$result = self::getConnection()->query($sqlQuery)) {
1378
            self::$logger->debug('<<makeHistoryTable');
1379
            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.']');
1380
        }
1381
1382
        self::$logger->debug('<<makeHistoryTable');
1383
    }
1384
1385
    /**
1386
     * (non-PHPdoc).
1387
     *
1388
     * @see Alpha\Model\ActiveRecordProviderInterface::rebuildTable()
1389
     */
1390
    public function rebuildTable()
1391
    {
1392
        self::$logger->debug('>>rebuildTable()');
1393
1394
        $sqlQuery = 'DROP TABLE IF EXISTS '.$this->record->getTableName().';';
1395
1396
        $this->record->setLastQuery($sqlQuery);
1397
1398
        if (!$result = self::getConnection()->query($sqlQuery)) {
1399
            self::$logger->debug('<<rebuildTable');
1400
            throw new AlphaException('Failed to drop the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getConnection()->error.']');
1401
        }
1402
1403
        $this->record->makeTable();
1404
1405
        self::$logger->debug('<<rebuildTable');
1406
    }
1407
1408
    /**
1409
     * (non-PHPdoc).
1410
     *
1411
     * @see Alpha\Model\ActiveRecordProviderInterface::dropTable()
1412
     */
1413
    public function dropTable($tableName = null)
1414
    {
1415
        self::$logger->debug('>>dropTable()');
1416
1417
        if ($tableName === null) {
1418
            $tableName = $this->record->getTableName();
1419
        }
1420
1421
        $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.';';
1422
1423
        $this->record->setLastQuery($sqlQuery);
1424
1425
        if (!$result = self::getConnection()->query($sqlQuery)) {
1426
            self::$logger->debug('<<dropTable');
1427
            throw new AlphaException('Failed to drop the table ['.$tableName.'] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1428
        }
1429
1430
        if ($this->record->getMaintainHistory()) {
1431
            $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.'_history;';
1432
1433
            $this->record->setLastQuery($sqlQuery);
1434
1435
            if (!$result = self::getConnection()->query($sqlQuery)) {
1436
                self::$logger->debug('<<dropTable');
1437
                throw new AlphaException('Failed to drop the table ['.$tableName.'_history] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1438
            }
1439
        }
1440
1441
        self::$logger->debug('<<dropTable');
1442
    }
1443
1444
    /**
1445
     * (non-PHPdoc).
1446
     *
1447
     * @see Alpha\Model\ActiveRecordProviderInterface::addProperty()
1448
     */
1449
    public function addProperty($propName)
1450
    {
1451
        self::$logger->debug('>>addProperty(propName=['.$propName.'])');
1452
1453
        $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD ';
1454
1455
        if ($this->isTableOverloaded() && $propName == 'classname') {
1456
            $sqlQuery .= 'classname VARCHAR(100)';
1457
        } else {
1458
            if (!in_array($propName, $this->record->getDefaultAttributes()) && !in_array($propName, $this->record->getTransientAttributes())) {
1459
                $prop = $this->record->getPropObject($propName);
1460
1461
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1462
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL';
1463
                } elseif ($prop instanceof Integer) {
1464
                    $sqlQuery .= "$propName INT(".$prop->getSize().')';
1465
                } elseif ($prop instanceof Double) {
1466
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).')';
1467
                } elseif ($prop instanceof SmallText) {
1468
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8';
1469
                } elseif ($prop instanceof Text) {
1470
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8";
1471
                } elseif ($prop instanceof Boolean) {
1472
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0'";
1473
                } elseif ($prop instanceof Date) {
1474
                    $sqlQuery .= "$propName DATE";
1475
                } elseif ($prop instanceof Timestamp) {
1476
                    $sqlQuery .= "$propName DATETIME";
1477
                } elseif ($prop instanceof Enum) {
1478
                    $sqlQuery .= "$propName ENUM(";
1479
                    $enumVals = $prop->getOptions();
1480
                    foreach ($enumVals as $val) {
1481
                        $sqlQuery .= "'".$val."',";
1482
                    }
1483
                    $sqlQuery = rtrim($sqlQuery, ',');
1484
                    $sqlQuery .= ') CHARACTER SET utf8';
1485
                } elseif ($prop instanceof DEnum) {
1486
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1487
                    $denum->saveIfNew();
1488
                    $sqlQuery .= "$propName INT(11) ZEROFILL";
1489
                } elseif ($prop instanceof Relation) {
1490
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED";
1491
                } else {
1492
                    $sqlQuery .= '';
1493
                }
1494
            }
1495
        }
1496
1497
        $this->record->setLastQuery($sqlQuery);
1498
1499
        if (!$result = self::getConnection()->query($sqlQuery)) {
1500
            self::$logger->debug('<<addProperty');
1501
            throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1502
        } else {
1503
            self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'] table for the class ['.get_class($this->record).']');
1504
        }
1505
1506
        if ($this->record->getMaintainHistory()) {
1507
            $sqlQuery = str_replace($this->record->getTableName(), $this->record->getTableName().'_history', $sqlQuery);
1508
1509
            if (!$result = self::getConnection()->query($sqlQuery)) {
1510
                self::$logger->debug('<<addProperty');
1511
                throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'_history], query is ['.$this->record->getLastQuery().']');
1512
            } else {
1513
                self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'_history] table for the class ['.get_class($this->record).']');
1514
            }
1515
        }
1516
1517
        self::$logger->debug('<<addProperty');
1518
    }
1519
1520
    /**
1521
     * (non-PHPdoc).
1522
     *
1523
     * @see Alpha\Model\ActiveRecordProviderInterface::getMAX()
1524
     */
1525
    public function getMAX()
1526
    {
1527
        self::$logger->debug('>>getMAX()');
1528
1529
        $sqlQuery = 'SELECT MAX(ID) AS max_ID FROM '.$this->record->getTableName();
1530
1531
        $this->record->setLastQuery($sqlQuery);
1532
1533
        try {
1534
            $result = $this->record->query($sqlQuery);
1535
1536
            $row = $result[0];
1537
1538
            if (isset($row['max_ID'])) {
1539
                self::$logger->debug('<<getMAX ['.$row['max_ID'].']');
1540
1541
                return $row['max_ID'];
1542
            } else {
1543
                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().']');
1544
            }
1545
        } catch (\Exception $e) {
1546
            self::$logger->debug('<<getMAX');
1547
            throw new AlphaException($e->getMessage());
1548
        }
1549
    }
1550
1551
    /**
1552
     * (non-PHPdoc).
1553
     *
1554
     * @see Alpha\Model\ActiveRecordProviderInterface::getCount()
1555
     */
1556
    public function getCount($attributes = array(), $values = array())
1557
    {
1558
        self::$logger->debug('>>getCount(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'])');
1559
1560
        if ($this->record->isTableOverloaded()) {
1561
            $whereClause = ' WHERE classname = \''.addslashes(get_class($this->record)).'\' AND';
1562
        } else {
1563
            $whereClause = ' WHERE';
1564
        }
1565
1566
        $count = count($attributes);
1567
1568
        for ($i = 0; $i < $count; ++$i) {
1569
            $whereClause .= ' '.$attributes[$i].' = \''.$values[$i].'\' AND';
1570
            self::$logger->debug($whereClause);
1571
        }
1572
        // remove the last " AND"
1573
        $whereClause = mb_substr($whereClause, 0, -4);
1574
1575
        if ($whereClause != ' WHERE') {
1576
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName().$whereClause;
1577
        } else {
1578
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName();
1579
        }
1580
1581
        $this->record->setLastQuery($sqlQuery);
1582
1583
        $result = self::getConnection()->query($sqlQuery);
1584
1585
        if ($result) {
1586
            $row = $result->fetch_array(MYSQLI_ASSOC);
1587
1588
            self::$logger->debug('<<getCount ['.$row['class_count'].']');
1589
1590
            return $row['class_count'];
1591
        } else {
1592
            self::$logger->debug('<<getCount');
1593
            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().']');
1594
        }
1595
    }
1596
1597
    /**
1598
     * (non-PHPdoc).
1599
     *
1600
     * @see Alpha\Model\ActiveRecordProviderInterface::getHistoryCount()
1601
     */
1602
    public function getHistoryCount()
1603
    {
1604
        self::$logger->debug('>>getHistoryCount()');
1605
1606
        if (!$this->record->getMaintainHistory()) {
1607
            throw new AlphaException('getHistoryCount method called on a DAO where no history is maintained!');
1608
        }
1609
1610
        $sqlQuery = 'SELECT COUNT(ID) AS object_count FROM '.$this->record->getTableName().'_history WHERE ID='.$this->record->getID();
1611
1612
        $this->record->setLastQuery($sqlQuery);
1613
1614
        $result = self::getConnection()->query($sqlQuery);
1615
1616
        if ($result) {
1617
            $row = $result->fetch_array(MYSQLI_ASSOC);
1618
1619
            self::$logger->debug('<<getHistoryCount ['.$row['object_count'].']');
1620
1621
            return $row['object_count'];
1622
        } else {
1623
            self::$logger->debug('<<getHistoryCount');
1624
            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().']');
1625
        }
1626
    }
1627
1628
    /**
1629
     * (non-PHPdoc).
1630
     *
1631
     * @see Alpha\Model\ActiveRecordProviderInterface::setEnumOptions()
1632
     * @since 1.1
1633
     */
1634
    public function setEnumOptions()
1635
    {
1636
        self::$logger->debug('>>setEnumOptions()');
1637
1638
        // get the class attributes
1639
        $reflection = new ReflectionClass(get_class($this->record));
1640
        $properties = $reflection->getProperties();
1641
1642
        // flag for any database errors
1643
        $dbError = false;
1644
1645
        foreach ($properties as $propObj) {
1646
            $propName = $propObj->name;
1647
            if (!in_array($propName, $this->record->getDefaultAttributes()) && !in_array($propName, $this->record->getTransientAttributes())) {
1648
                $propClass = get_class($this->record->getPropObject($propName));
1649
                if ($propClass == 'Enum') {
1650
                    $sqlQuery = 'SHOW COLUMNS FROM '.$this->record->getTableName()." LIKE '$propName'";
1651
1652
                    $this->record->setLastQuery($sqlQuery);
1653
1654
                    $result = self::getConnection()->query($sqlQuery);
1655
1656
                    if ($result) {
1657
                        $row = $result->fetch_array(MYSQLI_NUM);
1658
                        $options = explode("','", preg_replace("/(enum|set)\('(.+?)'\)/", '\\2', $row[1]));
1659
1660
                        $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...
1661
                    } else {
1662
                        $dbError = true;
1663
                        break;
1664
                    }
1665
                }
1666
            }
1667
        }
1668
1669
        if (!$dbError) {
1670
            if (method_exists($this, 'after_setEnumOptions_callback')) {
1671
                $this->{'after_setEnumOptions_callback'}();
1672
            }
1673
        } else {
1674
            throw new AlphaException('Failed to load enum options correctly for object instance of class ['.get_class($this).']');
1675
        }
1676
        self::$logger->debug('<<setEnumOptions');
1677
    }
1678
1679
    /**
1680
     * (non-PHPdoc).
1681
     *
1682
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableExists()
1683
     */
1684
    public function checkTableExists($checkHistoryTable = false)
1685
    {
1686
        self::$logger->debug('>>checkTableExists(checkHistoryTable=['.$checkHistoryTable.'])');
1687
1688
        $tableExists = false;
1689
1690
        $sqlQuery = 'SHOW TABLES;';
1691
        $this->record->setLastQuery($sqlQuery);
1692
1693
        $result = self::getConnection()->query($sqlQuery);
1694
1695
        if ($result) {
1696
            $tableName = ($checkHistoryTable ? $this->record->getTableName().'_history' : $this->record->getTableName());
1697
1698
            while ($row = $result->fetch_array(MYSQLI_NUM)) {
1699
                if (strtolower($row[0]) == mb_strtolower($tableName)) {
1700
                    $tableExists = true;
1701
                }
1702
            }
1703
1704
            self::$logger->debug('<<checkTableExists ['.$tableExists.']');
1705
1706
            return $tableExists;
1707
        } else {
1708
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1709
        }
1710
    }
1711
1712
    /**
1713
     * (non-PHPdoc).
1714
     *
1715
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordTableExists()
1716
     */
1717
    public static function checkRecordTableExists($RecordClassName, $checkHistoryTable = false)
1718
    {
1719
        if (self::$logger == null) {
1720
            self::$logger = new Logger('ActiveRecordProviderMySQL');
1721
        }
1722
        self::$logger->debug('>>checkRecordTableExists(RecordClassName=['.$RecordClassName.'], checkHistoryTable=['.$checkHistoryTable.'])');
1723
1724
        if (!class_exists($RecordClassName)) {
1725
            throw new IllegalArguementException('The classname provided ['.$checkHistoryTable.'] is not defined!');
1726
        }
1727
1728
        $tableName = $RecordClassName::TABLE_NAME;
1729
1730
        if (empty($tableName)) {
1731
            $tableName = mb_substr($RecordClassName, 0, mb_strpos($RecordClassName, '_'));
1732
        }
1733
1734
        if ($checkHistoryTable) {
1735
            $tableName .= '_history';
1736
        }
1737
1738
        $tableExists = false;
1739
1740
        $sqlQuery = 'SHOW TABLES;';
1741
1742
        $result = self::getConnection()->query($sqlQuery);
1743
1744
        while ($row = $result->fetch_array(MYSQLI_NUM)) {
1745
            if ($row[0] == $tableName) {
1746
                $tableExists = true;
1747
            }
1748
        }
1749
1750
        if ($result) {
1751
            self::$logger->debug('<<checkRecordTableExists ['.($tableExists ? 'true' : 'false').']');
1752
1753
            return $tableExists;
1754
        } else {
1755
            self::$logger->debug('<<checkRecordTableExists');
1756
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1757
        }
1758
    }
1759
1760
    /**
1761
     * (non-PHPdoc).
1762
     *
1763
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableNeedsUpdate()
1764
     */
1765
    public function checkTableNeedsUpdate()
1766
    {
1767
        self::$logger->debug('>>checkTableNeedsUpdate()');
1768
1769
        $updateRequired = false;
1770
1771
        $matchCount = 0;
1772
1773
        $query = 'SHOW COLUMNS FROM '.$this->record->getTableName();
1774
        $result = self::getConnection()->query($query);
1775
        $this->record->setLastQuery($query);
1776
1777
        // get the class attributes
1778
        $reflection = new ReflectionClass(get_class($this->record));
1779
        $properties = $reflection->getProperties();
1780
1781
        foreach ($properties as $propObj) {
1782
            $propName = $propObj->name;
1783
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1784
                $foundMatch = false;
1785
1786
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1787
                    if ($propName == $row['Field']) {
1788
                        $foundMatch = true;
1789
                        break;
1790
                    }
1791
                }
1792
1793
                if (!$foundMatch) {
1794
                    --$matchCount;
1795
                }
1796
1797
                $result->data_seek(0);
1798
            }
1799
        }
1800
1801
        // check for the "classname" field in overloaded tables
1802
        if ($this->record->isTableOverloaded()) {
1803
            $foundMatch = false;
1804
1805
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1806
                if ('classname' == $row['Field']) {
1807
                    $foundMatch = true;
1808
                    break;
1809
                }
1810
            }
1811
            if (!$foundMatch) {
1812
                --$matchCount;
1813
            }
1814
        }
1815
1816
        if ($matchCount != 0) {
1817
            $updateRequired = true;
1818
        }
1819
1820
        if ($result) {
1821
            // check the table indexes
1822
            try {
1823
                $this->checkIndexes();
1824
            } catch (AlphaException $ae) {
1825
                self::$logger->warn("Error while checking database indexes:\n\n".$ae->getMessage());
1826
            }
1827
1828
            self::$logger->debug('<<checkTableNeedsUpdate ['.$updateRequired.']');
1829
1830
            return $updateRequired;
1831
        } else {
1832
            self::$logger->debug('<<checkTableNeedsUpdate');
1833
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1834
        }
1835
    }
1836
1837
    /**
1838
     * (non-PHPdoc).
1839
     *
1840
     * @see Alpha\Model\ActiveRecordProviderInterface::findMissingFields()
1841
     */
1842
    public function findMissingFields()
1843
    {
1844
        self::$logger->debug('>>findMissingFields()');
1845
1846
        $missingFields = array();
1847
        $matchCount = 0;
1848
1849
        $sqlQuery = 'SHOW COLUMNS FROM '.$this->record->getTableName();
1850
1851
        $result = self::getConnection()->query($sqlQuery);
1852
1853
        $this->record->setLastQuery($sqlQuery);
1854
1855
        // get the class attributes
1856
        $reflection = new ReflectionClass(get_class($this->record));
1857
        $properties = $reflection->getProperties();
1858
1859
        foreach ($properties as $propObj) {
1860
            $propName = $propObj->name;
1861
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1862
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1863
                    if ($propName == $row['Field']) {
1864
                        ++$matchCount;
1865
                        break;
1866
                    }
1867
                }
1868
                $result->data_seek(0);
1869
            } else {
1870
                ++$matchCount;
1871
            }
1872
1873
            if ($matchCount == 0) {
1874
                array_push($missingFields, $propName);
1875
            } else {
1876
                $matchCount = 0;
1877
            }
1878
        }
1879
1880
        // check for the "classname" field in overloaded tables
1881
        if ($this->record->isTableOverloaded()) {
1882
            $foundMatch = false;
1883
1884
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1885
                if ('classname' == $row['Field']) {
1886
                    $foundMatch = true;
1887
                    break;
1888
                }
1889
            }
1890
            if (!$foundMatch) {
1891
                array_push($missingFields, 'classname');
1892
            }
1893
        }
1894
1895
        if (!$result) {
1896
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1897
        }
1898
1899
        self::$logger->debug('<<findMissingFields ['.var_export($missingFields, true).']');
1900
1901
        return $missingFields;
1902
    }
1903
1904
    /**
1905
     * (non-PHPdoc).
1906
     *
1907
     * @see Alpha\Model\ActiveRecordProviderInterface::getIndexes()
1908
     */
1909
    public function getIndexes()
1910
    {
1911
        self::$logger->debug('>>getIndexes()');
1912
1913
        $query = 'SHOW INDEX FROM '.$this->record->getTableName();
1914
1915
        $result = self::getConnection()->query($query);
1916
1917
        $this->record->setLastQuery($query);
1918
1919
        $indexNames = array();
1920
1921
        if (!$result) {
1922
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1923
        } else {
1924
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1925
                array_push($indexNames, $row['Key_name']);
1926
            }
1927
        }
1928
1929
        self::$logger->debug('<<getIndexes');
1930
1931
        return $indexNames;
1932
    }
1933
1934
    /**
1935
     * Checks to see if all of the indexes are in place for the record's table, creates those that are missing.
1936
     *
1937
     * @since 1.1
1938
     */
1939
    private function checkIndexes()
1940
    {
1941
        self::$logger->debug('>>checkIndexes()');
1942
1943
        $indexNames = $this->getIndexes();
1944
1945
        // process unique keys
1946
        foreach ($this->record->getUniqueAttributes() as $prop) {
1947
            // check for composite indexes
1948
            if (mb_strpos($prop, '+')) {
1949
                $attributes = explode('+', $prop);
1950
1951
                $index_exists = false;
1952
                foreach ($indexNames as $index) {
1953
                    if ($attributes[0].'_'.$attributes[1].'_unq_idx' == $index) {
1954
                        $index_exists = true;
1955
                    }
1956
                    if (count($attributes) == 3) {
1957
                        if ($attributes[0].'_'.$attributes[1].'_'.$attributes[2].'_unq_idx' == $index) {
1958
                            $index_exists = true;
1959
                        }
1960
                    }
1961
                }
1962
1963
                if (!$index_exists) {
1964
                    if (count($attributes) == 3) {
1965
                        $this->record->createUniqueIndex($attributes[0], $attributes[1], $attributes[2]);
1966
                    } else {
1967
                        $this->record->createUniqueIndex($attributes[0], $attributes[1]);
1968
                    }
1969
                }
1970
            } else {
1971
                $index_exists = false;
1972
                foreach ($indexNames as $index) {
1973
                    if ($prop.'_unq_idx' == $index) {
1974
                        $index_exists = true;
1975
                    }
1976
                }
1977
1978
                if (!$index_exists) {
1979
                    $this->createUniqueIndex($prop);
1980
                }
1981
            }
1982
        }
1983
1984
        // process foreign-key indexes
1985
        // get the class attributes
1986
        $reflection = new ReflectionClass(get_class($this->record));
1987
        $properties = $reflection->getProperties();
1988
1989
        foreach ($properties as $propObj) {
1990
            $propName = $propObj->name;
1991
            $prop = $this->record->getPropObject($propName);
1992
            if ($prop instanceof Relation) {
1993
                if ($prop->getRelationType() == 'MANY-TO-ONE') {
1994
                    $indexExists = false;
1995
                    foreach ($indexNames as $index) {
1996
                        if ($this->record->getTableName().'_'.$propName.'_fk_idx' == $index) {
1997
                            $indexExists = true;
1998
                        }
1999
                    }
2000
2001
                    if (!$indexExists) {
2002
                        $this->createForeignIndex($propName, $prop->getRelatedClass(), $prop->getRelatedClassField());
2003
                    }
2004
                }
2005
2006
                if ($prop->getRelationType() == 'MANY-TO-MANY') {
2007
                    $lookup = $prop->getLookup();
2008
2009
                    if ($lookup != null) {
2010
                        try {
2011
                            $lookupIndexNames = $lookup->getIndexes();
2012
2013
                            // handle index check/creation on left side of Relation
2014
                            $indexExists = false;
2015
                            foreach ($lookupIndexNames as $index) {
2016
                                if ($lookup->getTableName().'_leftID_fk_idx' == $index) {
2017
                                    $indexExists = true;
2018
                                }
2019
                            }
2020
2021
                            if (!$indexExists) {
2022
                                $lookup->createForeignIndex('leftID', $prop->getRelatedClass('left'), 'ID');
2023
                            }
2024
2025
                            // handle index check/creation on right side of Relation
2026
                            $indexExists = false;
2027
                            foreach ($lookupIndexNames as $index) {
2028
                                if ($lookup->getTableName().'_rightID_fk_idx' == $index) {
2029
                                    $indexExists = true;
2030
                                }
2031
                            }
2032
2033
                            if (!$indexExists) {
2034
                                $lookup->createForeignIndex('rightID', $prop->getRelatedClass('right'), 'ID');
2035
                            }
2036
                        } catch (AlphaException $e) {
2037
                            self::$logger->error($e->getMessage());
2038
                        }
2039
                    }
2040
                }
2041
            }
2042
        }
2043
2044
        self::$logger->debug('<<checkIndexes');
2045
    }
2046
2047
    /**
2048
     * (non-PHPdoc).
2049
     *
2050
     * @see Alpha\Model\ActiveRecordProviderInterface::createForeignIndex()
2051
     */
2052
    public function createForeignIndex($attributeName, $relatedClass, $relatedClassAttribute, $indexName = null)
2053
    {
2054
        self::$logger->debug('>>createForeignIndex(attributeName=['.$attributeName.'], relatedClass=['.$relatedClass.'], relatedClassAttribute=['.$relatedClassAttribute.'], indexName=['.$indexName.']');
2055
2056
        $relatedRecord = new $relatedClass();
2057
        $tableName = $relatedRecord->getTableName();
2058
2059
        $result = false;
2060
2061
        if (self::checkRecordTableExists($relatedClass)) {
2062
            $sqlQuery = '';
2063
2064
            if ($attributeName == 'leftID') {
2065
                if ($indexName === null) {
2066
                    $indexName = $this->record->getTableName().'_leftID_fk_idx';
2067
                }
2068
                $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD INDEX '.$indexName.' (leftID);';
2069
            }
2070
            if ($attributeName == 'rightID') {
2071
                if ($indexName === null) {
2072
                    $indexName = $this->record->getTableName().'_rightID_fk_idx';
2073
                }
2074
                $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD INDEX '.$indexName.' (rightID);';
2075
            }
2076
2077
            if (!empty($sqlQuery)) {
2078
                $this->record->setLastQuery($sqlQuery);
2079
2080
                $result = self::getConnection()->query($sqlQuery);
2081
2082
                if (!$result) {
2083
                    throw new FailedIndexCreateException('Failed to create an index on ['.$this->record->getTableName().'], error is ['.self::getConnection()->error.'], query ['.$this->record->getLastQuery().']');
2084
                }
2085
            }
2086
2087
            if ($indexName === null) {
2088
                $indexName = $this->record->getTableName().'_'.$attributeName.'_fk_idx';
2089
            }
2090
2091
            $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD FOREIGN KEY '.$indexName.' ('.$attributeName.') REFERENCES '.$tableName.' ('.$relatedClassAttribute.') ON DELETE SET NULL;';
2092
2093
            $this->record->setLastQuery($sqlQuery);
2094
            $result = self::getConnection()->query($sqlQuery);
2095
        }
2096
2097
        if ($result) {
2098
            self::$logger->debug('Successfully created the foreign key index ['.$indexName.']');
2099
        } else {
2100
            throw new FailedIndexCreateException('Failed to create the index ['.$indexName.'] on ['.$this->record->getTableName().'], error is ['.self::getConnection()->error.'], query ['.$this->record->getLastQuery().']');
2101
        }
2102
2103
        self::$logger->debug('<<createForeignIndex');
2104
    }
2105
2106
    /**
2107
     * (non-PHPdoc).
2108
     *
2109
     * @see Alpha\Model\ActiveRecordProviderInterface::createUniqueIndex()
2110
     */
2111
    public function createUniqueIndex($attribute1Name, $attribute2Name = '', $attribute3Name = '')
2112
    {
2113
        self::$logger->debug('>>createUniqueIndex(attribute1Name=['.$attribute1Name.'], attribute2Name=['.$attribute2Name.'], attribute3Name=['.$attribute3Name.'])');
2114
2115
        $sqlQuery = '';
2116
2117
        if ($attribute2Name != '' && $attribute3Name != '') {
2118
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_'.$attribute2Name.'_'.$attribute3Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.','.$attribute2Name.','.$attribute3Name.');';
2119
        }
2120
2121
        if ($attribute2Name != '' && $attribute3Name == '') {
2122
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_'.$attribute2Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.','.$attribute2Name.');';
2123
        }
2124
2125
        if ($attribute2Name == '' && $attribute3Name == '') {
2126
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.');';
2127
        }
2128
2129
        $this->record->setLastQuery($sqlQuery);
2130
2131
        $result = self::getConnection()->query($sqlQuery);
2132
2133
        if ($result) {
2134
            self::$logger->debug('Successfully created the unique index on ['.$this->record->getTableName().']');
2135
        } else {
2136
            throw new FailedIndexCreateException('Failed to create the unique index on ['.$this->record->getTableName().'], error is ['.self::getConnection()->error.']');
2137
        }
2138
2139
        self::$logger->debug('<<createUniqueIndex');
2140
    }
2141
2142
    /**
2143
     * (non-PHPdoc).
2144
     *
2145
     * @see Alpha\Model\ActiveRecordProviderInterface::reload()
2146
     */
2147
    public function reload()
2148
    {
2149
        self::$logger->debug('>>reload()');
2150
2151
        if (!$this->record->isTransient()) {
2152
            $this->record->load($this->record->getID());
2153
        } else {
2154
            throw new AlphaException('Cannot reload transient object from database!');
2155
        }
2156
        self::$logger->debug('<<reload');
2157
    }
2158
2159
    /**
2160
     * (non-PHPdoc).
2161
     *
2162
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordExists()
2163
     */
2164
    public function checkRecordExists($ID)
2165
    {
2166
        self::$logger->debug('>>checkRecordExists(ID=['.$ID.'])');
2167
2168
        $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE ID = ?;';
2169
2170
        $this->record->setLastQuery($sqlQuery);
2171
2172
        $stmt = self::getConnection()->stmt_init();
2173
2174
        if ($stmt->prepare($sqlQuery)) {
2175
            $stmt->bind_param('i', $ID);
2176
2177
            $stmt->execute();
2178
2179
            $result = $this->bindResult($stmt);
2180
2181
            $stmt->close();
2182
2183
            if (is_array($result)) {
2184
                if (count($result) > 0) {
2185
                    self::$logger->debug('<<checkRecordExists [true]');
2186
2187
                    return true;
2188
                } else {
2189
                    self::$logger->debug('<<checkRecordExists [false]');
2190
2191
                    return false;
2192
                }
2193
            } else {
2194
                self::$logger->debug('<<checkRecordExists');
2195
                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().']');
2196
            }
2197
        } else {
2198
            self::$logger->debug('<<checkRecordExists');
2199
            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().']');
2200
        }
2201
    }
2202
2203
    /**
2204
     * (non-PHPdoc).
2205
     *
2206
     * @see Alpha\Model\ActiveRecordProviderInterface::isTableOverloaded()
2207
     */
2208
    public function isTableOverloaded()
2209
    {
2210
        self::$logger->debug('>>isTableOverloaded()');
2211
2212
        $reflection = new ReflectionClass($this->record);
2213
        $classname = $reflection->getShortName();
2214
        $tablename = ucfirst($this->record->getTableName());
2215
2216
        // use reflection to check to see if we are dealing with a persistent type (e.g. DEnum) which are never overloaded
2217
        $implementedInterfaces = $reflection->getInterfaces();
2218
2219
        foreach ($implementedInterfaces as $interface) {
2220
            if ($interface->name == 'Alpha\Model\Type\TypeInterface') {
2221
                self::$logger->debug('<<isTableOverloaded [false]');
2222
2223
                return false;
2224
            }
2225
        }
2226
2227
        if ($classname != $tablename) {
2228
            // loop over all records to see if there is one using the same table as this record
2229
2230
            $Recordclasses = ActiveRecord::getRecordClassNames();
2231
2232
            foreach ($Recordclasses as $RecordclassName) {
2233
                $reflection = new ReflectionClass($RecordclassName);
2234
                $classname = $reflection->getShortName();
2235
                if ($tablename == $classname) {
2236
                    self::$logger->debug('<<isTableOverloaded [true]');
2237
2238
                    return true;
2239
                }
2240
            }
2241
2242
            self::$logger->debug('<<isTableOverloaded');
2243
            throw new BadTableNameException('The table name ['.$tablename.'] for the class ['.$classname.'] is invalid as it does not match a Record definition in the system!');
2244
        } else {
2245
            // check to see if there is already a "classname" column in the database for this record
2246
2247
            $query = 'SHOW COLUMNS FROM '.$this->record->getTableName();
2248
2249
            $result = self::getConnection()->query($query);
2250
2251
            if ($result) {
2252
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
2253
                    if ('classname' == $row['Field']) {
2254
                        self::$logger->debug('<<isTableOverloaded [true]');
2255
2256
                        return true;
2257
                    }
2258
                }
2259
            } else {
2260
                self::$logger->warn('Error during show columns ['.self::getConnection()->error.']');
2261
            }
2262
2263
            self::$logger->debug('<<isTableOverloaded [false]');
2264
2265
            return false;
2266
        }
2267
    }
2268
2269
    /**
2270
     * (non-PHPdoc).
2271
     *
2272
     * @see Alpha\Model\ActiveRecordProviderInterface::begin()
2273
     */
2274
    public static function begin()
2275
    {
2276
        if (self::$logger == null) {
2277
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2278
        }
2279
        self::$logger->debug('>>begin()');
2280
2281
        if (!self::getConnection()->autocommit(false)) {
2282
            throw new AlphaException('Error beginning a new transaction, error is ['.self::getConnection()->error.']');
2283
        }
2284
2285
        self::$logger->debug('<<begin');
2286
    }
2287
2288
    /**
2289
     * (non-PHPdoc).
2290
     *
2291
     * @see Alpha\Model\ActiveRecordProviderInterface::commit()
2292
     */
2293
    public static function commit()
2294
    {
2295
        if (self::$logger == null) {
2296
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2297
        }
2298
        self::$logger->debug('>>commit()');
2299
2300
        if (!self::getConnection()->commit()) {
2301
            throw new FailedSaveException('Error commiting a transaction, error is ['.self::getConnection()->error.']');
2302
        }
2303
2304
        self::$logger->debug('<<commit');
2305
    }
2306
2307
    /**
2308
     * (non-PHPdoc).
2309
     *
2310
     * @see Alpha\Model\ActiveRecordProviderInterface::rollback()
2311
     */
2312
    public static function rollback()
2313
    {
2314
        if (self::$logger == null) {
2315
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2316
        }
2317
        self::$logger->debug('>>rollback()');
2318
2319
        if (!self::getConnection()->rollback()) {
2320
            throw new AlphaException('Error rolling back a transaction, error is ['.self::getConnection()->error.']');
2321
        }
2322
2323
        self::$logger->debug('<<rollback');
2324
    }
2325
2326
    /**
2327
     * (non-PHPdoc).
2328
     *
2329
     * @see Alpha\Model\ActiveRecordProviderInterface::setRecord()
2330
     */
2331
    public function setRecord($Record)
2332
    {
2333
        $this->record = $Record;
2334
    }
2335
2336
    /**
2337
     * Dynamically binds all of the attributes for the current Record to the supplied prepared statement
2338
     * parameters.  If arrays of attribute names and values are provided, only those will be bound to
2339
     * the supplied statement.
2340
     *
2341
     * @param \mysqli_stmt $stmt The SQL statement to bind to.
2342
     * @param array Optional array of Record attributes.
2343
     * @param array Optional array of Record values.
2344
     *
2345
     * @return \mysqli_stmt
2346
     *
2347
     * @since 1.1
2348
     */
2349
    private function bindParams($stmt, $attributes = array(), $values = array())
2350
    {
2351
        self::$logger->debug('>>bindParams(stmt=['.var_export($stmt, true).'])');
2352
2353
        $bindingsTypes = '';
2354
        $params = array();
2355
2356
        // here we are only binding the supplied attributes
2357
        if (count($attributes) > 0 && count($attributes) == count($values)) {
2358
            $count = count($values);
2359
2360
            for ($i = 0; $i < $count; ++$i) {
2361
                if (Validator::isInteger($values[$i])) {
2362
                    $bindingsTypes .= 'i';
2363
                } else {
2364
                    $bindingsTypes .= 's';
2365
                }
2366
                array_push($params, $values[$i]);
2367
            }
2368
2369
            if ($this->record->isTableOverloaded()) {
2370
                $bindingsTypes .= 's';
2371
                array_push($params, get_class($this->record));
2372
            }
2373
        } else { // bind all attributes on the business object
2374
2375
            // get the class attributes
2376
            $reflection = new ReflectionClass(get_class($this->record));
2377
            $properties = $reflection->getProperties();
2378
2379
            foreach ($properties as $propObj) {
2380
                $propName = $propObj->name;
2381
                if (!in_array($propName, $this->record->getTransientAttributes())) {
2382
                    // Skip the ID, database auto number takes care of this.
2383
                    if ($propName != 'ID' && $propName != 'version_num') {
2384
                        if ($this->record->getPropObject($propName) instanceof Integer) {
2385
                            $bindingsTypes .= 'i';
2386
                        } else {
2387
                            $bindingsTypes .= 's';
2388
                        }
2389
                        array_push($params, $this->record->get($propName));
2390
                    }
2391
2392
                    if ($propName == 'version_num') {
2393
                        $temp = $this->record->getVersionNumber()->getValue();
2394
                        $this->record->set('version_num', $temp+1);
2395
                        $bindingsTypes .= 'i';
2396
                        array_push($params, $this->record->getVersionNumber()->getValue());
2397
                    }
2398
                }
2399
            }
2400
2401
            if ($this->record->isTableOverloaded()) {
2402
                $bindingsTypes .= 's';
2403
                array_push($params, get_class($this->record));
2404
            }
2405
2406
            // the ID may be on the WHERE clause for UPDATEs and DELETEs
2407
            if (!$this->record->isTransient()) {
2408
                $bindingsTypes .= 'i';
2409
                array_push($params, $this->record->getID());
2410
            }
2411
        }
2412
2413
        self::$logger->debug('bindingsTypes=['.$bindingsTypes.'], count: ['.mb_strlen($bindingsTypes).']');
2414
        self::$logger->debug('params ['.var_export($params, true).']');
2415
2416
        if ($params != null) {
2417
            $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...
2418
2419
            $count = count($params);
2420
2421
            for ($i = 0; $i < $count; ++$i) {
2422
                $bind_name = 'bind'.$i;
2423
                $$bind_name = $params[$i];
2424
                $bind_names[] = &$$bind_name;
2425
            }
2426
2427
            call_user_func_array(array($stmt, 'bind_param'), $bind_names);
2428
        }
2429
2430
        self::$logger->debug('<<bindParams ['.var_export($stmt, true).']');
2431
2432
        return $stmt;
2433
    }
2434
2435
    /**
2436
     * Dynamically binds the result of the supplied prepared statement to a 2d array, where each element in the array is another array
2437
     * representing a database row.
2438
     *
2439
     * @param \mysqli_stmt $stmt
2440
     *
2441
     * @return array A 2D array containing the query result.
2442
     *
2443
     * @since 1.1
2444
     */
2445
    private function bindResult($stmt)
2446
    {
2447
        $result = array();
2448
2449
        $metadata = $stmt->result_metadata();
2450
        $fields = $metadata->fetch_fields();
2451
2452
        while (true) {
2453
            $pointers = array();
2454
            $row = array();
2455
2456
            $pointers[] = $stmt;
2457
            foreach ($fields as $field) {
2458
                $fieldname = $field->name;
2459
                $pointers[] = &$row[$fieldname];
2460
            }
2461
2462
            call_user_func_array('mysqli_stmt_bind_result', $pointers);
2463
2464
            if (!$stmt->fetch()) {
2465
                break;
2466
            }
2467
2468
            $result[] = $row;
2469
        }
2470
2471
        $metadata->free();
2472
2473
        return $result;
2474
    }
2475
2476
    /**
2477
     * Parses a MySQL error for the value that violated a unique constraint.
2478
     *
2479
     * @param string $error The MySQL error string.
2480
     *
2481
     * @since 1.1
2482
     */
2483
    private function findOffendingValue($error)
2484
    {
2485
        self::$logger->debug('>>findOffendingValue(error=['.$error.'])');
2486
2487
        $singleQuote1 = mb_strpos($error, "'");
2488
        $singleQuote2 = mb_strrpos($error, "'");
2489
2490
        $value = mb_substr($error, $singleQuote1, ($singleQuote2-$singleQuote1)+1);
2491
        self::$logger->debug('<<findOffendingValue ['.$value.'])');
2492
2493
        return $value;
2494
    }
2495
2496
    /**
2497
     * (non-PHPdoc).
2498
     *
2499
     * @see Alpha\Model\ActiveRecordProviderInterface::checkDatabaseExists()
2500
     */
2501
    public static function checkDatabaseExists()
2502
    {
2503
        $config = ConfigProvider::getInstance();
2504
2505
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2506
2507
        $result = $connection->query('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \''.$config->get('db.name').'\'');
2508
2509
        if (count($result) > 0) {
2510
            return true;
2511
        } else {
2512
            return false;
2513
        }
2514
    }
2515
2516
    /**
2517
     * (non-PHPdoc).
2518
     *
2519
     * @see Alpha\Model\ActiveRecordProviderInterface::createDatabase()
2520
     */
2521
    public static function createDatabase()
2522
    {
2523
        $config = ConfigProvider::getInstance();
2524
2525
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2526
2527
        $connection->query('CREATE DATABASE '.$config->get('db.name'));
2528
    }
2529
2530
    /**
2531
     * (non-PHPdoc).
2532
     *
2533
     * @see Alpha\Model\ActiveRecordProviderInterface::dropDatabase()
2534
     */
2535
    public static function dropDatabase()
2536
    {
2537
        $config = ConfigProvider::getInstance();
2538
2539
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2540
2541
        $connection->query('DROP DATABASE '.$config->get('db.name'));
2542
    }
2543
2544
    /**
2545
     * (non-PHPdoc).
2546
     *
2547
     * @see Alpha\Model\ActiveRecordProviderInterface::backupDatabase()
2548
     */
2549
    public static function backupDatabase($targetFile)
2550
    {
2551
        $config = ConfigProvider::getInstance();
2552
2553
        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);
2554
    }
2555
}
2556