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

ActiveRecordProviderMySQL::loadAllByAttributes()   F

Complexity

Conditions 18
Paths 664

Size

Total Lines 109
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 109
rs 2.3577
cc 18
eloc 69
nc 664
nop 8

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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