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

ActiveRecordProviderMySQL::loadAllByAttribute()   F

Complexity

Conditions 17
Paths 440

Size

Total Lines 102
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 102
rs 3.4777
c 0
b 0
f 0
cc 17
eloc 70
nc 440
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($checkIndexes = true)
1237
    {
1238
        self::$logger->debug('>>makeTable()');
1239
1240
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().' (ID INT(11) ZEROFILL NOT NULL AUTO_INCREMENT,';
1241
1242
        // get the class attributes
1243
        $reflection = new ReflectionClass(get_class($this->record));
1244
        $properties = $reflection->getProperties();
1245
1246
        foreach ($properties as $propObj) {
1247
            $propName = $propObj->name;
1248
1249
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1250
                $prop = $this->record->getPropObject($propName);
1251
1252
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1253
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL,';
1254
                } elseif ($prop instanceof Integer) {
1255
                    $sqlQuery .= "$propName INT(".$prop->getSize().'),';
1256
                } elseif ($prop instanceof Double) {
1257
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).'),';
1258
                } elseif ($prop instanceof SmallText) {
1259
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8,';
1260
                } elseif ($prop instanceof Text) {
1261
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8,";
1262
                } elseif ($prop instanceof Boolean) {
1263
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0',";
1264
                } elseif ($prop instanceof Date) {
1265
                    $sqlQuery .= "$propName DATE,";
1266
                } elseif ($prop instanceof Timestamp) {
1267
                    $sqlQuery .= "$propName DATETIME,";
1268
                } elseif ($prop instanceof Enum) {
1269
                    $sqlQuery .= "$propName ENUM(";
1270
                    $enumVals = $prop->getOptions();
1271
                    foreach ($enumVals as $val) {
1272
                        $sqlQuery .= "'".$val."',";
1273
                    }
1274
                    $sqlQuery = rtrim($sqlQuery, ',');
1275
                    $sqlQuery .= ') CHARACTER SET utf8,';
1276
                } elseif ($prop instanceof DEnum) {
1277
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1278
                    $denum->saveIfNew();
1279
                    $sqlQuery .= "$propName INT(11) ZEROFILL,";
1280
                } elseif ($prop instanceof Relation) {
1281
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED,";
1282
                } else {
1283
                    $sqlQuery .= '';
1284
                }
1285
            }
1286
        }
1287
        if ($this->record->isTableOverloaded()) {
1288
            $sqlQuery .= 'classname VARCHAR(100),';
1289
        }
1290
1291
        $sqlQuery .= 'PRIMARY KEY (ID)) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;';
1292
1293
        $this->record->setLastQuery($sqlQuery);
1294
1295
        if (!$result = self::getConnection()->query($sqlQuery)) {
1296
            self::$logger->debug('<<makeTable');
1297
            throw new AlphaException('Failed to create the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getConnection()->error.']');
1298
        }
1299
1300
        // check the table indexes if any additional ones required
1301
        if ($checkIndexes) {
1302
            $this->checkIndexes();
1303
        }
1304
1305
        if ($this->record->getMaintainHistory()) {
1306
            $this->record->makeHistoryTable();
1307
        }
1308
1309
        self::$logger->debug('<<makeTable');
1310
    }
1311
1312
    /**
1313
     * (non-PHPdoc).
1314
     *
1315
     * @see Alpha\Model\ActiveRecordProviderInterface::makeHistoryTable()
1316
     */
1317
    public function makeHistoryTable()
1318
    {
1319
        self::$logger->debug('>>makeHistoryTable()');
1320
1321
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().'_history (ID INT(11) ZEROFILL NOT NULL,';
1322
1323
        // get the class attributes
1324
        $reflection = new ReflectionClass(get_class($this->record));
1325
        $properties = $reflection->getProperties();
1326
1327
        foreach ($properties as $propObj) {
1328
            $propName = $propObj->name;
1329
1330
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1331
                $prop = $this->record->getPropObject($propName);
1332
1333
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1334
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL,';
1335
                } elseif ($prop instanceof Integer) {
1336
                    $sqlQuery .= "$propName INT(".$prop->getSize().'),';
1337
                } elseif ($prop instanceof Double) {
1338
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).'),';
1339
                } elseif ($prop instanceof SmallText) {
1340
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8,';
1341
                } elseif ($prop instanceof Text) {
1342
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8,";
1343
                } elseif ($prop instanceof Boolean) {
1344
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0',";
1345
                } elseif ($prop instanceof Date) {
1346
                    $sqlQuery .= "$propName DATE,";
1347
                } elseif ($prop instanceof Timestamp) {
1348
                    $sqlQuery .= "$propName DATETIME,";
1349
                } elseif ($prop instanceof Enum) {
1350
                    $sqlQuery .= "$propName ENUM(";
1351
                    $enumVals = $prop->getOptions();
1352
                    foreach ($enumVals as $val) {
1353
                        $sqlQuery .= "'".$val."',";
1354
                    }
1355
                    $sqlQuery = rtrim($sqlQuery, ',');
1356
                    $sqlQuery .= ') CHARACTER SET utf8,';
1357
                } elseif ($prop instanceof DEnum) {
1358
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1359
                    $denum->saveIfNew();
1360
                    $sqlQuery .= "$propName INT(11) ZEROFILL,";
1361
                } elseif ($prop instanceof Relation) {
1362
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED,";
1363
                } else {
1364
                    $sqlQuery .= '';
1365
                }
1366
            }
1367
        }
1368
1369
        if ($this->record->isTableOverloaded()) {
1370
            $sqlQuery .= 'classname VARCHAR(100),';
1371
        }
1372
1373
        $sqlQuery .= 'PRIMARY KEY (ID, version_num)) ENGINE=MyISAM DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;';
1374
1375
        $this->record->setLastQuery($sqlQuery);
1376
1377
        if (!$result = self::getConnection()->query($sqlQuery)) {
1378
            self::$logger->debug('<<makeHistoryTable');
1379
            throw new AlphaException('Failed to create the table ['.$this->record->getTableName().'_history] for the class ['.get_class($this->record).'], database error is ['.self::getConnection()->error.']');
1380
        }
1381
1382
        self::$logger->debug('<<makeHistoryTable');
1383
    }
1384
1385
    /**
1386
     * (non-PHPdoc).
1387
     *
1388
     * @see Alpha\Model\ActiveRecordProviderInterface::rebuildTable()
1389
     */
1390
    public function rebuildTable()
1391
    {
1392
        self::$logger->debug('>>rebuildTable()');
1393
1394
        $sqlQuery = 'DROP TABLE IF EXISTS '.$this->record->getTableName().';';
1395
1396
        $this->record->setLastQuery($sqlQuery);
1397
1398
        if (!$result = self::getConnection()->query($sqlQuery)) {
1399
            self::$logger->debug('<<rebuildTable');
1400
            throw new AlphaException('Failed to drop the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getConnection()->error.']');
1401
        }
1402
1403
        $this->record->makeTable();
1404
1405
        self::$logger->debug('<<rebuildTable');
1406
    }
1407
1408
    /**
1409
     * (non-PHPdoc).
1410
     *
1411
     * @see Alpha\Model\ActiveRecordProviderInterface::dropTable()
1412
     */
1413
    public function dropTable($tableName = null)
1414
    {
1415
        self::$logger->debug('>>dropTable()');
1416
1417
        if ($tableName === null) {
1418
            $tableName = $this->record->getTableName();
1419
        }
1420
1421
        $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.';';
1422
1423
        $this->record->setLastQuery($sqlQuery);
1424
1425
        if (!$result = self::getConnection()->query($sqlQuery)) {
1426
            self::$logger->debug('<<dropTable');
1427
            throw new AlphaException('Failed to drop the table ['.$tableName.'] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1428
        }
1429
1430
        if ($this->record->getMaintainHistory()) {
1431
            $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.'_history;';
1432
1433
            $this->record->setLastQuery($sqlQuery);
1434
1435
            if (!$result = self::getConnection()->query($sqlQuery)) {
1436
                self::$logger->debug('<<dropTable');
1437
                throw new AlphaException('Failed to drop the table ['.$tableName.'_history] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1438
            }
1439
        }
1440
1441
        self::$logger->debug('<<dropTable');
1442
    }
1443
1444
    /**
1445
     * (non-PHPdoc).
1446
     *
1447
     * @see Alpha\Model\ActiveRecordProviderInterface::addProperty()
1448
     */
1449
    public function addProperty($propName)
1450
    {
1451
        self::$logger->debug('>>addProperty(propName=['.$propName.'])');
1452
1453
        $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD ';
1454
1455
        if ($this->isTableOverloaded() && $propName == 'classname') {
1456
            $sqlQuery .= 'classname VARCHAR(100)';
1457
        } else {
1458
            if (!in_array($propName, $this->record->getDefaultAttributes()) && !in_array($propName, $this->record->getTransientAttributes())) {
1459
                $prop = $this->record->getPropObject($propName);
1460
1461
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1462
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL';
1463
                } elseif ($prop instanceof Integer) {
1464
                    $sqlQuery .= "$propName INT(".$prop->getSize().')';
1465
                } elseif ($prop instanceof Double) {
1466
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).')';
1467
                } elseif ($prop instanceof SmallText) {
1468
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8';
1469
                } elseif ($prop instanceof Text) {
1470
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8";
1471
                } elseif ($prop instanceof Boolean) {
1472
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0'";
1473
                } elseif ($prop instanceof Date) {
1474
                    $sqlQuery .= "$propName DATE";
1475
                } elseif ($prop instanceof Timestamp) {
1476
                    $sqlQuery .= "$propName DATETIME";
1477
                } elseif ($prop instanceof Enum) {
1478
                    $sqlQuery .= "$propName ENUM(";
1479
                    $enumVals = $prop->getOptions();
1480
                    foreach ($enumVals as $val) {
1481
                        $sqlQuery .= "'".$val."',";
1482
                    }
1483
                    $sqlQuery = rtrim($sqlQuery, ',');
1484
                    $sqlQuery .= ') CHARACTER SET utf8';
1485
                } elseif ($prop instanceof DEnum) {
1486
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1487
                    $denum->saveIfNew();
1488
                    $sqlQuery .= "$propName INT(11) ZEROFILL";
1489
                } elseif ($prop instanceof Relation) {
1490
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED";
1491
                } else {
1492
                    $sqlQuery .= '';
1493
                }
1494
            }
1495
        }
1496
1497
        $this->record->setLastQuery($sqlQuery);
1498
1499
        if (!$result = self::getConnection()->query($sqlQuery)) {
1500
            self::$logger->debug('<<addProperty');
1501
            throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1502
        } else {
1503
            self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'] table for the class ['.get_class($this->record).']');
1504
        }
1505
1506
        if ($this->record->getMaintainHistory()) {
1507
            $sqlQuery = str_replace($this->record->getTableName(), $this->record->getTableName().'_history', $sqlQuery);
1508
1509
            if (!$result = self::getConnection()->query($sqlQuery)) {
1510
                self::$logger->debug('<<addProperty');
1511
                throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'_history], query is ['.$this->record->getLastQuery().']');
1512
            } else {
1513
                self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'_history] table for the class ['.get_class($this->record).']');
1514
            }
1515
        }
1516
1517
        self::$logger->debug('<<addProperty');
1518
    }
1519
1520
    /**
1521
     * (non-PHPdoc).
1522
     *
1523
     * @see Alpha\Model\ActiveRecordProviderInterface::getMAX()
1524
     */
1525
    public function getMAX()
1526
    {
1527
        self::$logger->debug('>>getMAX()');
1528
1529
        $sqlQuery = 'SELECT MAX(ID) AS max_ID FROM '.$this->record->getTableName();
1530
1531
        $this->record->setLastQuery($sqlQuery);
1532
1533
        try {
1534
            $result = $this->record->query($sqlQuery);
1535
1536
            $row = $result[0];
1537
1538
            if (isset($row['max_ID'])) {
1539
                self::$logger->debug('<<getMAX ['.$row['max_ID'].']');
1540
1541
                return $row['max_ID'];
1542
            } else {
1543
                throw new AlphaException('Failed to get the MAX ID for the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1544
            }
1545
        } catch (\Exception $e) {
1546
            self::$logger->debug('<<getMAX');
1547
            throw new AlphaException($e->getMessage());
1548
        }
1549
    }
1550
1551
    /**
1552
     * (non-PHPdoc).
1553
     *
1554
     * @see Alpha\Model\ActiveRecordProviderInterface::getCount()
1555
     */
1556
    public function getCount($attributes = array(), $values = array())
1557
    {
1558
        self::$logger->debug('>>getCount(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'])');
1559
1560
        if ($this->record->isTableOverloaded()) {
1561
            $whereClause = ' WHERE classname = \''.addslashes(get_class($this->record)).'\' AND';
1562
        } else {
1563
            $whereClause = ' WHERE';
1564
        }
1565
1566
        $count = count($attributes);
1567
1568
        for ($i = 0; $i < $count; ++$i) {
1569
            $whereClause .= ' '.$attributes[$i].' = \''.$values[$i].'\' AND';
1570
            self::$logger->debug($whereClause);
1571
        }
1572
        // remove the last " AND"
1573
        $whereClause = mb_substr($whereClause, 0, -4);
1574
1575
        if ($whereClause != ' WHERE') {
1576
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName().$whereClause;
1577
        } else {
1578
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName();
1579
        }
1580
1581
        $this->record->setLastQuery($sqlQuery);
1582
1583
        $result = self::getConnection()->query($sqlQuery);
1584
1585
        if ($result) {
1586
            $row = $result->fetch_array(MYSQLI_ASSOC);
1587
1588
            self::$logger->debug('<<getCount ['.$row['class_count'].']');
1589
1590
            return $row['class_count'];
1591
        } else {
1592
            self::$logger->debug('<<getCount');
1593
            throw new AlphaException('Failed to get the count for the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1594
        }
1595
    }
1596
1597
    /**
1598
     * (non-PHPdoc).
1599
     *
1600
     * @see Alpha\Model\ActiveRecordProviderInterface::getHistoryCount()
1601
     */
1602
    public function getHistoryCount()
1603
    {
1604
        self::$logger->debug('>>getHistoryCount()');
1605
1606
        if (!$this->record->getMaintainHistory()) {
1607
            throw new AlphaException('getHistoryCount method called on a DAO where no history is maintained!');
1608
        }
1609
1610
        $sqlQuery = 'SELECT COUNT(ID) AS object_count FROM '.$this->record->getTableName().'_history WHERE ID='.$this->record->getID();
1611
1612
        $this->record->setLastQuery($sqlQuery);
1613
1614
        $result = self::getConnection()->query($sqlQuery);
1615
1616
        if ($result) {
1617
            $row = $result->fetch_array(MYSQLI_ASSOC);
1618
1619
            self::$logger->debug('<<getHistoryCount ['.$row['object_count'].']');
1620
1621
            return $row['object_count'];
1622
        } else {
1623
            self::$logger->debug('<<getHistoryCount');
1624
            throw new AlphaException('Failed to get the history count for the business object ['.$this->record->getID().'] from the table ['.$this->record->getTableName().'_history], query is ['.$this->record->getLastQuery().']');
1625
        }
1626
    }
1627
1628
    /**
1629
     * (non-PHPdoc).
1630
     *
1631
     * @see Alpha\Model\ActiveRecordProviderInterface::setEnumOptions()
1632
     * @since 1.1
1633
     */
1634
    public function setEnumOptions()
1635
    {
1636
        self::$logger->debug('>>setEnumOptions()');
1637
1638
        // get the class attributes
1639
        $reflection = new ReflectionClass(get_class($this->record));
1640
        $properties = $reflection->getProperties();
1641
1642
        // flag for any database errors
1643
        $dbError = false;
1644
1645
        foreach ($properties as $propObj) {
1646
            $propName = $propObj->name;
1647
            if (!in_array($propName, $this->record->getDefaultAttributes()) && !in_array($propName, $this->record->getTransientAttributes())) {
1648
                $propClass = get_class($this->record->getPropObject($propName));
1649
                if ($propClass == 'Enum') {
1650
                    $sqlQuery = 'SHOW COLUMNS FROM '.$this->record->getTableName()." LIKE '$propName'";
1651
1652
                    $this->record->setLastQuery($sqlQuery);
1653
1654
                    $result = self::getConnection()->query($sqlQuery);
1655
1656
                    if ($result) {
1657
                        $row = $result->fetch_array(MYSQLI_NUM);
1658
                        $options = explode("','", preg_replace("/(enum|set)\('(.+?)'\)/", '\\2', $row[1]));
1659
1660
                        $this->record->getPropObject($propName)->setOptions($options);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Alpha\Model\Type\Type as the method setOptions() does only exist in the following sub-classes of Alpha\Model\Type\Type: Alpha\Model\Type\Enum. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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