Completed
Push — develop ( 2ea776...7fcc0f )
by John
03:31
created

ActiveRecordProviderMySQL::addProperty()   C

Complexity

Conditions 22
Paths 56

Size

Total Lines 69
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 69
rs 5.5836
c 0
b 0
f 0
cc 22
eloc 52
nc 56
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace Alpha\Model;
4
5
use Alpha\Model\Type\Integer;
6
use Alpha\Model\Type\Timestamp;
7
use Alpha\Model\Type\DEnum;
8
use Alpha\Model\Type\Relation;
9
use Alpha\Model\Type\RelationLookup;
10
use Alpha\Model\Type\Double;
11
use Alpha\Model\Type\Text;
12
use Alpha\Model\Type\SmallText;
13
use Alpha\Model\Type\Date;
14
use Alpha\Model\Type\Enum;
15
use Alpha\Model\Type\Boolean;
16
use Alpha\Util\Config\ConfigProvider;
17
use Alpha\Util\Logging\Logger;
18
use Alpha\Util\Helper\Validator;
19
use Alpha\Util\Http\Session\SessionProviderFactory;
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 $BO;
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->BO->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($OID, $version = 0)
201
    {
202
        self::$logger->debug('>>load(OID=['.$OID.'], version=['.$version.'])');
203
204
        $attributes = $this->BO->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->BO->getTableName().'_history WHERE OID = ? AND version_num = ? LIMIT 1;';
213
        } else {
214
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->BO->getTableName().' WHERE OID = ? LIMIT 1;';
215
        }
216
        $this->BO->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', $OID, $version);
224
            } else {
225
                $stmt->bind_param('i', $OID);
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.'], OID is ['.print_r($OID, true).'], MySql error is ['.self::getConnection()->error.']');
238
            if (!$this->BO->checkTableExists()) {
239
                $this->BO->makeTable();
240
241
                throw new RecordNotFoundException('Failed to load object of OID ['.$OID.'], table ['.$this->BO->getTableName().'] did not exist so had to create!');
242
            }
243
244
            return;
245
        }
246
247
        if (!isset($row['OID']) || $row['OID'] < 1) {
248
            self::$logger->debug('<<load');
249
            throw new RecordNotFoundException('Failed to load object of OID ['.$OID.'] not found in database.');
250
        }
251
252
        // get the class attributes
253
        $reflection = new ReflectionClass(get_class($this->BO));
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->BO->getTransientAttributes())) {
262
                    $this->BO->set($propName, $row[$propName]);
263
                } elseif (!$propObj->isPrivate() && $this->BO->getPropObject($propName) instanceof Relation) {
264
                    $prop = $this->BO->getPropObject($propName);
265
266
                    // handle the setting of ONE-TO-MANY relation values
267
                    if ($prop->getRelationType() == 'ONE-TO-MANY') {
268
                        $this->BO->set($propObj->name, $this->BO->getOID());
269
                    }
270
271
                    // handle the setting of MANY-TO-ONE relation values
272
                    if ($prop->getRelationType() == 'MANY-TO-ONE' && isset($row[$propName])) {
273
                        $this->BO->set($propObj->name, $row[$propName]);
274
                    }
275
                }
276
            }
277
        } catch (IllegalArguementException $e) {
278
            self::$logger->warn('Bad data stored in the table ['.$this->BO->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->BO->checkTableNeedsUpdate()) {
282
                $missingFields = $this->BO->findMissingFields();
283
284
                $count = count($missingFields);
285
286
                for ($i = 0; $i < $count; ++$i) {
287
                    $this->BO->addProperty($missingFields[$i]);
288
                }
289
290
                self::$logger->warn('<<load');
291
                throw new RecordNotFoundException('Failed to load object of OID ['.$OID.'], table ['.$this->BO->getTableName().'] was out of sync with the database so had to be updated!');
292
            }
293
        }
294
295
        self::$logger->debug('<<load ['.$OID.']');
296
    }
297
298
    /**
299
     * (non-PHPdoc).
300
     *
301
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllOldVersions()
302
     */
303
    public function loadAllOldVersions($OID)
304
    {
305
        self::$logger->debug('>>loadAllOldVersions(OID=['.$OID.'])');
306
307
        if (!$this->BO->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->BO->getTableName().'_history WHERE OID = \''.$OID.'\' ORDER BY version_num;';
312
313
        $this->BO->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->BO->getLastQuery().']');
318
        }
319
320
        // now build an array of objects to be returned
321
        $objects = array();
322
        $count = 0;
323
        $RecordClass = get_class($this->BO);
324
325
        while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
326
            try {
327
                $obj = new $RecordClass();
328
                $obj->load($OID, $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->BO->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->BO->isTableOverloaded()) {
364
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->BO->getTableName().' WHERE '.$attribute.' = ? AND classname = ? LIMIT 1;';
365
        } else {
366
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->BO->getTableName().' WHERE '.$attribute.' = ? LIMIT 1;';
367
        }
368
369
        self::$logger->debug('Query=['.$sqlQuery.']');
370
371
        $this->BO->setLastQuery($sqlQuery);
372
        $stmt = self::getConnection()->stmt_init();
373
374
        $row = array();
375
376
        if ($stmt->prepare($sqlQuery)) {
377
            if ($this->BO->getPropObject($attribute) instanceof Integer) {
378
                if (!$ignoreClassType && $this->BO->isTableOverloaded()) {
379
                    $classname = get_class($this->BO);
380
                    $stmt->bind_param('is', $value, $classname);
381
                } else {
382
                    $stmt->bind_param('i', $value);
383
                }
384
            } else {
385
                if (!$ignoreClassType && $this->BO->isTableOverloaded()) {
386
                    $classname = get_class($this->BO);
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->BO->checkTableExists()) {
405
                $this->BO->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['OID']) || $row['OID'] < 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->BO->setOID($row['OID']);
419
420
        // get the class attributes
421
        $reflection = new ReflectionClass(get_class($this->BO));
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->BO->getTransientAttributes())) {
431
                        $this->BO->set($propName, $row[$propName]);
432
                    } elseif (!$propObj->isPrivate() && $this->BO->get($propName) != '' && $this->BO->getPropObject($propName) instanceof Relation) {
433
                        $prop = $this->BO->getPropObject($propName);
434
435
                        // handle the setting of ONE-TO-MANY relation values
436
                        if ($prop->getRelationType() == 'ONE-TO-MANY') {
437
                            $this->BO->set($propObj->name, $this->BO->getOID());
438
                        }
439
                    }
440
                }
441
            }
442
        } catch (IllegalArguementException $e) {
443
            self::$logger->warn('Bad data stored in the table ['.$this->BO->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->BO->checkTableNeedsUpdate()) {
447
                $missingFields = $this->BO->findMissingFields();
448
449
                $count = count($missingFields);
450
451
                for ($i = 0; $i < $count; ++$i) {
452
                    $this->BO->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->BO->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 = 'OID', $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->BO->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->BO).']');
477
        }
478
479
        if (!$ignoreClassType && $this->BO->isTableOverloaded()) {
480
            if ($limit == 0) {
481
                $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName().' WHERE classname = \''.addslashes(get_class($this->BO)).'\' ORDER BY '.$orderBy.' '.$order.';';
482
            } else {
483
                $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName().' WHERE classname = \''.addslashes(get_class($this->BO)).'\' ORDER BY '.$orderBy.' '.$order.' LIMIT '.
484
                    $start.', '.$limit.';';
485
            }
486
        } else {
487
            if ($limit == 0) {
488
                $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName().' ORDER BY '.$orderBy.' '.$order.';';
489
            } else {
490
                $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName().' ORDER BY '.$orderBy.' '.$order.' LIMIT '.$start.', '.$limit.';';
491
            }
492
        }
493
494
        $this->BO->setLastQuery($sqlQuery);
495
496
        if (!$result = self::getConnection()->query($sqlQuery)) {
497
            self::$logger->debug('<<loadAll [0]');
498
            throw new RecordNotFoundException('Failed to load object OIDs, MySql error is ['.self::getConnection()->error.'], query ['.$this->BO->getLastQuery().']');
499
        }
500
501
        // now build an array of objects to be returned
502
        $objects = array();
503
        $count = 0;
504
        $RecordClass = get_class($this->BO);
505
506
        while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
507
            try {
508
                $obj = new $RecordClass();
509
                $obj->load($row['OID']);
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 = 'OID', $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->BO->isTableOverloaded()) {
538
            $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName()." WHERE $attribute = ? AND classname = ? ORDER BY ".$orderBy.' '.$order.$limit;
539
        } else {
540
            $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName()." WHERE $attribute = ? ORDER BY ".$orderBy.' '.$order.$limit;
541
        }
542
543
        $this->BO->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->BO->getPropObject($attribute) instanceof Integer) {
552
                if ($this->BO->isTableOverloaded()) {
553
                    $classname = get_class($this->BO);
554
                    $stmt->bind_param('is', $value, $classname);
555
                } else {
556
                    $stmt->bind_param('i', $value);
557
                }
558
            } else {
559
                if ($this->BO->isTableOverloaded()) {
560
                    $classname = get_class($this->BO);
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->BO->checkTableExists()) {
575
                $this->BO->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->BO);
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['OID']);
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 = 'OID', $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->BO->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 OID FROM '.$this->BO->getTableName().$whereClause.' ORDER BY '.$orderBy.' '.$order.$limit;
663
664
        $this->BO->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);
0 ignored issues
show
Documentation introduced by
$stmt is of type object<mysqli_stmt>, but the function expects a object<Alpha\Model\mysqli_stmt>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
672
            } else {
673
                // we'll still need to bind the "classname" for overloaded BOs...
674
                if ($this->BO->isTableOverloaded()) {
675
                    $classname = get_class($this->BO);
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->BO->checkTableExists()) {
688
                $this->BO->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->BO);
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['OID']);
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 = 'OID', $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->BO->isTableOverloaded()) {
761
            $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName()." WHERE updated_ts >= '".$date." 00:00:00' AND updated_ts <= '".$date." 23:59:59' AND classname = '".addslashes(get_class($this->BO))."' ORDER BY ".$orderBy.' '.$order.$limit;
762
        } else {
763
            $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName()." WHERE updated_ts >= '".$date." 00:00:00' AND updated_ts <= '".$date." 23:59:59' ORDER BY ".$orderBy.' '.$order.$limit;
764
        }
765
766
        $this->BO->setLastQuery($sqlQuery);
767
768
        if (!$result = self::getConnection()->query($sqlQuery)) {
769
            self::$logger->debug('<<loadAllByDayUpdated []');
770
            throw new RecordNotFoundException('Failed to load object OIDs, MySql error is ['.self::getConnection()->error.'], query ['.$this->BO->getLastQuery().']');
771
        }
772
773
        // now build an array of objects to be returned
774
        $objects = array();
775
        $count = 0;
776
        $RecordClass = get_class($this->BO);
777
778
        while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
779
            $obj = new $RecordClass();
780
            $obj->load($row['OID']);
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->BO->isTableOverloaded()) {
800
            $sqlQuery = 'SELECT '.$returnAttribute.' FROM '.$this->BO->getTableName()." WHERE $attribute = '$value' AND classname = '".addslashes(get_class($this->BO))."' ORDER BY OID ".$order.';';
801
        } else {
802
            $sqlQuery = 'SELECT '.$returnAttribute.' FROM '.$this->BO->getTableName()." WHERE $attribute = '$value' ORDER BY OID ".$order.';';
803
        }
804
805
        $this->BO->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->BO->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 = SessionProviderFactory::getInstance($sessionProvider);
840
841
        // get the class attributes
842
        $reflection = new ReflectionClass(get_class($this->BO));
843
        $properties = $reflection->getProperties();
844
845
        if ($this->BO->getVersion() != $this->BO->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->BO->set('updated_by', $session->get('currentUser')->getOID());
852
        }
853
854
        $this->BO->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->BO->isTransient()) {
858
            $savedFieldsCount = 0;
859
            $sqlQuery = 'INSERT INTO '.$this->BO->getTableName().' (';
860
861
            foreach ($properties as $propObj) {
862
                $propName = $propObj->name;
863
                if (!in_array($propName, $this->BO->getTransientAttributes())) {
864
                    // Skip the OID, database auto number takes care of this.
865
                    if ($propName != 'OID' && $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->BO->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->BO->isTableOverloaded()) {
890
                $sqlQuery .= '?,';
891
            }
892
893
            $sqlQuery = rtrim($sqlQuery, ',').')';
894
895
            $this->BO->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);
0 ignored issues
show
Documentation introduced by
$stmt is of type object<mysqli_stmt>, but the function expects a object<Alpha\Model\mysqli_stmt>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
902
                $stmt->execute();
903
            } else {
904
                throw new FailedSaveException('Failed to save object, error is ['.$stmt->error.'], query ['.$this->BO->getLastQuery().']');
905
            }
906
        } else {
907
            // assume that it is a persistent object that needs to be updated
908
            $savedFieldsCount = 0;
909
            $sqlQuery = 'UPDATE '.$this->BO->getTableName().' SET ';
910
911
            foreach ($properties as $propObj) {
912
                $propName = $propObj->name;
913
                if (!in_array($propName, $this->BO->getTransientAttributes())) {
914
                    // Skip the OID, database auto number takes care of this.
915
                    if ($propName != 'OID' && $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->BO->isTableOverloaded()) {
928
                $sqlQuery .= 'classname = ?,';
929
            }
930
931
            $sqlQuery = rtrim($sqlQuery, ',');
932
933
            $sqlQuery .= ' WHERE OID=?;';
934
935
            $this->BO->setLastQuery($sqlQuery);
936
            $stmt = self::getConnection()->stmt_init();
937
938
            if ($stmt->prepare($sqlQuery)) {
939
                $this->bindParams($stmt);
0 ignored issues
show
Documentation introduced by
$stmt is of type object<mysqli_stmt>, but the function expects a object<Alpha\Model\mysqli_stmt>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
940
                $stmt->execute();
941
            } else {
942
                throw new FailedSaveException('Failed to save object, error is ['.$stmt->error.'], query ['.$this->BO->getLastQuery().']');
943
            }
944
        }
945
946
        if ($stmt != null && $stmt->error == '') {
947
            // populate the updated OID in case we just done an insert
948
            if ($this->BO->isTransient()) {
949
                $this->BO->setOID(self::getConnection()->insert_id);
950
            }
951
952
            try {
953
                foreach ($properties as $propObj) {
954
                    $propName = $propObj->name;
955
956
                    if ($this->BO->getPropObject($propName) instanceof Relation) {
957
                        $prop = $this->BO->getPropObject($propName);
958
959
                        // handle the saving of MANY-TO-MANY relation values
960
                        if ($prop->getRelationType() == 'MANY-TO-MANY' && count($prop->getRelatedOIDs()) > 0) {
961
                            try {
962
                                try {
963
                                    // check to see if the rel is on this class
964
                                    $side = $prop->getSide(get_class($this->BO));
965
                                } catch (IllegalArguementException $iae) {
966
                                    $side = $prop->getSide(get_parent_class($this->BO));
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->BO->getOID());
975
                                    } else {
976
                                        $lookUp->deleteAllByAttribute('rightID', $this->BO->getOID());
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
                                $OIDs = $prop->getRelatedOIDs();
983
984
                                if (isset($OIDs) && !empty($OIDs[0])) {
985
                                    // now for each posted OID, create a new RelationLookup record and save
986
                                    foreach ($OIDs as $oid) {
987
                                        $newLookUp = new RelationLookup($lookUp->get('leftClassName'), $lookUp->get('rightClassName'));
988
                                        if ($side == 'left') {
989
                                            $newLookUp->set('leftID', $this->BO->getOID());
990
                                            $newLookUp->set('rightID', $oid);
991
                                        } else {
992
                                            $newLookUp->set('rightID', $this->BO->getOID());
993
                                            $newLookUp->set('leftID', $oid);
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->BO->getOID());
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->BO->getVersionNumber()->getValue();
1017
            $this->BO->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->BO->getLastQuery().']');
1024
            }
1025
        }
1026
1027
        if ($this->BO->getMaintainHistory()) {
1028
            $this->BO->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 = SessionProviderFactory::getInstance($sessionProvider);
1044
1045
        // get the class attributes
1046
        $reflection = new ReflectionClass(get_class($this->BO));
1047
        $properties = $reflection->getProperties();
0 ignored issues
show
Unused Code introduced by
$properties is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1048
1049
        if ($this->BO->getVersion() != $this->BO->getVersionNumber()->getValue()) {
1050
            throw new LockingException('Could not save the object as it has been updated by another user.  Please try saving again.');
1051
        }
1052
1053
        // set the "updated by" fields, we can only set the user id if someone is logged in
1054
        if ($session->get('currentUser') != null) {
1055
            $this->BO->set('updated_by', $session->get('currentUser')->getOID());
1056
        }
1057
1058
        $this->BO->set('updated_ts', new Timestamp(date('Y-m-d H:i:s')));
1059
1060
        // assume that it is a persistent object that needs to be updated
1061
        $sqlQuery = 'UPDATE '.$this->BO->getTableName().' SET '.$attribute.' = ?, version_num = ? , updated_by = ?, updated_ts = ? WHERE OID = ?;';
1062
1063
        $this->BO->setLastQuery($sqlQuery);
1064
        $stmt = self::getConnection()->stmt_init();
1065
1066
        $newVersionNumber = $this->BO->getVersionNumber()->getValue() + 1;
1067
1068
        if ($stmt->prepare($sqlQuery)) {
1069
            if ($this->BO->getPropObject($attribute) instanceof Integer) {
1070
                $bindingsType = 'i';
1071
            } else {
1072
                $bindingsType = 's';
1073
            }
1074
            $OID = $this->BO->getOID();
1075
            $updatedBy = $this->BO->get('updated_by');
1076
            $updatedTS = $this->BO->get('updated_ts');
1077
            $stmt->bind_param($bindingsType.'iisi', $value, $newVersionNumber, $updatedBy, $updatedTS, $OID);
1078
            self::$logger->debug('Binding params ['.$bindingsType.'iisi, '.$value.', '.$newVersionNumber.', '.$updatedBy.', '.$updatedTS.', '.$OID.']');
1079
            $stmt->execute();
1080
        } else {
1081
            throw new FailedSaveException('Failed to save attribute, error is ['.$stmt->error.'], query ['.$this->BO->getLastQuery().']');
1082
        }
1083
1084
        $stmt->close();
1085
1086
        $this->BO->set($attribute, $value);
1087
        $this->BO->set('version_num', $newVersionNumber);
1088
1089
        if ($this->BO->getMaintainHistory()) {
1090
            $this->BO->saveHistory();
1091
        }
1092
1093
        self::$logger->debug('<<saveAttribute');
1094
    }
1095
1096
    /**
1097
     * (non-PHPdoc).
1098
     *
1099
     * @see Alpha\Model\ActiveRecordProviderInterface::saveHistory()
1100
     */
1101
    public function saveHistory()
1102
    {
1103
        self::$logger->debug('>>saveHistory()');
1104
1105
        // get the class attributes
1106
        $reflection = new ReflectionClass(get_class($this->BO));
1107
        $properties = $reflection->getProperties();
1108
1109
        $savedFieldsCount = 0;
1110
        $attributeNames = array();
1111
        $attributeValues = array();
1112
1113
        $sqlQuery = 'INSERT INTO '.$this->BO->getTableName().'_history (';
1114
1115
        foreach ($properties as $propObj) {
1116
            $propName = $propObj->name;
1117
            if (!in_array($propName, $this->BO->getTransientAttributes())) {
1118
                $sqlQuery .= "$propName,";
1119
                $attributeNames[] = $propName;
1120
                $attributeValues[] = $this->BO->get($propName);
1121
                ++$savedFieldsCount;
1122
            }
1123
        }
1124
1125
        if ($this->BO->isTableOverloaded()) {
1126
            $sqlQuery .= 'classname,';
1127
        }
1128
1129
        $sqlQuery = rtrim($sqlQuery, ',');
1130
1131
        $sqlQuery .= ') VALUES (';
1132
1133
        for ($i = 0; $i < $savedFieldsCount; ++$i) {
1134
            $sqlQuery .= '?,';
1135
        }
1136
1137
        if ($this->BO->isTableOverloaded()) {
1138
            $sqlQuery .= '?,';
1139
        }
1140
1141
        $sqlQuery = rtrim($sqlQuery, ',').')';
1142
1143
        $this->BO->setLastQuery($sqlQuery);
1144
        self::$logger->debug('Query ['.$sqlQuery.']');
1145
1146
        $stmt = self::getConnection()->stmt_init();
1147
1148
        if ($stmt->prepare($sqlQuery)) {
1149
            $stmt = $this->bindParams($stmt, $attributeNames, $attributeValues);
0 ignored issues
show
Documentation introduced by
$stmt is of type object<mysqli_stmt>, but the function expects a object<Alpha\Model\mysqli_stmt>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1150
            $stmt->execute();
1151
        } else {
1152
            throw new FailedSaveException('Failed to save object history, error is ['.$stmt->error.'], query ['.$this->BO->getLastQuery().']');
1153
        }
1154
    }
1155
1156
    /**
1157
     * (non-PHPdoc).
1158
     *
1159
     * @see Alpha\Model\ActiveRecordProviderInterface::delete()
1160
     */
1161
    public function delete()
1162
    {
1163
        self::$logger->debug('>>delete()');
1164
1165
        $sqlQuery = 'DELETE FROM '.$this->BO->getTableName().' WHERE OID = ?;';
1166
1167
        $this->BO->setLastQuery($sqlQuery);
1168
1169
        $stmt = self::getConnection()->stmt_init();
1170
1171
        if ($stmt->prepare($sqlQuery)) {
1172
            $OID = $this->BO->getOID();
1173
            $stmt->bind_param('i', $OID);
1174
            $stmt->execute();
1175
            self::$logger->debug('Deleted the object ['.$this->BO->getOID().'] of class ['.get_class($this->BO).']');
1176
        } else {
1177
            throw new FailedDeleteException('Failed to delete object ['.$this->BO->getOID().'], error is ['.$stmt->error.'], query ['.$this->BO->getLastQuery().']');
1178
        }
1179
1180
        $stmt->close();
1181
1182
        self::$logger->debug('<<delete');
1183
    }
1184
1185
    /**
1186
     * (non-PHPdoc).
1187
     *
1188
     * @see Alpha\Model\ActiveRecordProviderInterface::getVersion()
1189
     */
1190
    public function getVersion()
1191
    {
1192
        self::$logger->debug('>>getVersion()');
1193
1194
        $sqlQuery = 'SELECT version_num FROM '.$this->BO->getTableName().' WHERE OID = ?;';
1195
        $this->BO->setLastQuery($sqlQuery);
1196
1197
        $stmt = self::getConnection()->stmt_init();
1198
1199
        if ($stmt->prepare($sqlQuery)) {
1200
            $OID = $this->BO->getOID();
1201
            $stmt->bind_param('i', $OID);
1202
1203
            $stmt->execute();
1204
1205
            $result = $this->bindResult($stmt);
1206
            if (isset($result[0])) {
1207
                $row = $result[0];
1208
            }
1209
1210
            $stmt->close();
1211
        } else {
1212
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
1213
            if (!$this->BO->checkTableExists()) {
1214
                $this->BO->makeTable();
1215
1216
                throw new RecordNotFoundException('Failed to get the version number, table did not exist so had to create!');
1217
            }
1218
1219
            return;
1220
        }
1221
1222
        if (!isset($row['version_num']) || $row['version_num'] < 1) {
1223
            self::$logger->debug('<<getVersion [0]');
1224
1225
            return 0;
1226
        } else {
1227
            $version_num = $row['version_num'];
1228
1229
            self::$logger->debug('<<getVersion ['.$version_num.']');
1230
1231
            return $version_num;
1232
        }
1233
    }
1234
1235
    /**
1236
     * (non-PHPdoc).
1237
     *
1238
     * @see Alpha\Model\ActiveRecordProviderInterface::makeTable()
1239
     */
1240
    public function makeTable()
1241
    {
1242
        self::$logger->debug('>>makeTable()');
1243
1244
        $sqlQuery = 'CREATE TABLE '.$this->BO->getTableName().' (OID INT(11) ZEROFILL NOT NULL AUTO_INCREMENT,';
1245
1246
        // get the class attributes
1247
        $reflection = new ReflectionClass(get_class($this->BO));
1248
        $properties = $reflection->getProperties();
1249
1250
        foreach ($properties as $propObj) {
1251
            $propName = $propObj->name;
1252
1253
            if (!in_array($propName, $this->BO->getTransientAttributes()) && $propName != 'OID') {
1254
                $prop = $this->BO->getPropObject($propName);
1255
1256
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1257
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL,';
1258
                } elseif ($prop instanceof Integer) {
1259
                    $sqlQuery .= "$propName INT(".$prop->getSize().'),';
1260
                } elseif ($prop instanceof Double) {
1261
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).'),';
1262
                } elseif ($prop instanceof SmallText) {
1263
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8,';
1264
                } elseif ($prop instanceof Text) {
1265
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8,";
1266
                } elseif ($prop instanceof Boolean) {
1267
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0',";
1268
                } elseif ($prop instanceof Date) {
1269
                    $sqlQuery .= "$propName DATE,";
1270
                } elseif ($prop instanceof Timestamp) {
1271
                    $sqlQuery .= "$propName DATETIME,";
1272
                } elseif ($prop instanceof Enum) {
1273
                    $sqlQuery .= "$propName ENUM(";
1274
                    $enumVals = $prop->getOptions();
1275
                    foreach ($enumVals as $val) {
1276
                        $sqlQuery .= "'".$val."',";
1277
                    }
1278
                    $sqlQuery = rtrim($sqlQuery, ',');
1279
                    $sqlQuery .= ') CHARACTER SET utf8,';
1280
                } elseif ($prop instanceof DEnum) {
1281
                    $tmp = new DEnum(get_class($this->BO).'::'.$propName);
0 ignored issues
show
Unused Code introduced by
$tmp is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1282
                    $sqlQuery .= "$propName INT(11) ZEROFILL,";
1283
                } elseif ($prop instanceof Relation) {
1284
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED,";
1285
                } else {
1286
                    $sqlQuery .= '';
1287
                }
1288
            }
1289
        }
1290
        if ($this->BO->isTableOverloaded()) {
1291
            $sqlQuery .= 'classname VARCHAR(100),';
1292
        }
1293
1294
        $sqlQuery .= 'PRIMARY KEY (OID)) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;';
1295
1296
        $this->BO->setLastQuery($sqlQuery);
1297
1298
        if (!$result = self::getConnection()->query($sqlQuery)) {
1299
            self::$logger->debug('<<makeTable');
1300
            throw new AlphaException('Failed to create the table ['.$this->BO->getTableName().'] for the class ['.get_class($this->BO).'], database error is ['.self::getConnection()->error.']');
1301
        }
1302
1303
        // check the table indexes if any additional ones required
1304
        $this->checkIndexes();
1305
1306
        if ($this->BO->getMaintainHistory()) {
1307
            $this->BO->makeHistoryTable();
1308
        }
1309
1310
        self::$logger->debug('<<makeTable');
1311
    }
1312
1313
    /**
1314
     * (non-PHPdoc).
1315
     *
1316
     * @see Alpha\Model\ActiveRecordProviderInterface::makeHistoryTable()
1317
     */
1318
    public function makeHistoryTable()
1319
    {
1320
        self::$logger->debug('>>makeHistoryTable()');
1321
1322
        $sqlQuery = 'CREATE TABLE '.$this->BO->getTableName().'_history (OID INT(11) ZEROFILL NOT NULL,';
1323
1324
        // get the class attributes
1325
        $reflection = new ReflectionClass(get_class($this->BO));
1326
        $properties = $reflection->getProperties();
1327
1328
        foreach ($properties as $propObj) {
1329
            $propName = $propObj->name;
1330
1331
            if (!in_array($propName, $this->BO->getTransientAttributes()) && $propName != 'OID') {
1332
                $prop = $this->BO->getPropObject($propName);
1333
1334
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1335
                    $sqlQuery .= "$propName INT(".$prop->getSize().') ZEROFILL NOT NULL,';
1336
                } elseif ($prop instanceof Integer) {
1337
                    $sqlQuery .= "$propName INT(".$prop->getSize().'),';
1338
                } elseif ($prop instanceof Double) {
1339
                    $sqlQuery .= "$propName DOUBLE(".$prop->getSize(true).'),';
1340
                } elseif ($prop instanceof SmallText) {
1341
                    $sqlQuery .= "$propName VARCHAR(".$prop->getSize().') CHARACTER SET utf8,';
1342
                } elseif ($prop instanceof Text) {
1343
                    $sqlQuery .= "$propName TEXT CHARACTER SET utf8,";
1344
                } elseif ($prop instanceof Boolean) {
1345
                    $sqlQuery .= "$propName CHAR(1) DEFAULT '0',";
1346
                } elseif ($prop instanceof Date) {
1347
                    $sqlQuery .= "$propName DATE,";
1348
                } elseif ($prop instanceof Timestamp) {
1349
                    $sqlQuery .= "$propName DATETIME,";
1350
                } elseif ($prop instanceof Enum) {
1351
                    $sqlQuery .= "$propName ENUM(";
1352
                    $enumVals = $prop->getOptions();
1353
                    foreach ($enumVals as $val) {
1354
                        $sqlQuery .= "'".$val."',";
1355
                    }
1356
                    $sqlQuery = rtrim($sqlQuery, ',');
1357
                    $sqlQuery .= ') CHARACTER SET utf8,';
1358
                } elseif ($prop instanceof DEnum) {
1359
                    $tmp = new DEnum(get_class($this->BO).'::'.$propName);
0 ignored issues
show
Unused Code introduced by
$tmp is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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->BO->isTableOverloaded()) {
1370
            $sqlQuery .= 'classname VARCHAR(100),';
1371
        }
1372
1373
        $sqlQuery .= 'PRIMARY KEY (OID, version_num)) ENGINE=MyISAM DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;';
1374
1375
        $this->BO->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->BO->getTableName().'_history] for the class ['.get_class($this->BO).'], 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->BO->getTableName().';';
1395
1396
        $this->BO->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->BO->getTableName().'] for the class ['.get_class($this->BO).'], database error is ['.self::getConnection()->error.']');
1401
        }
1402
1403
        $this->BO->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->BO->getTableName();
1419
        }
1420
1421
        $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.';';
1422
1423
        $this->BO->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->BO).'], query is ['.$this->BO->getLastQuery().']');
1428
        }
1429
1430
        if ($this->BO->getMaintainHistory()) {
1431
            $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.'_history;';
1432
1433
            $this->BO->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->BO).'], query is ['.$this->BO->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->BO->getTableName().' ADD ';
1454
1455
        if ($this->isTableOverloaded() && $propName == 'classname') {
1456
            $sqlQuery .= 'classname VARCHAR(100)';
1457
        } else {
1458
            if (!in_array($propName, $this->BO->getDefaultAttributes()) && !in_array($propName, $this->BO->getTransientAttributes())) {
1459
                $prop = $this->BO->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
                    $tmp = new DEnum(get_class($this->BO).'::'.$propName);
0 ignored issues
show
Unused Code introduced by
$tmp is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1487
                    $sqlQuery .= "$propName INT(11) ZEROFILL";
1488
                } elseif ($prop instanceof Relation) {
1489
                    $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED";
1490
                } else {
1491
                    $sqlQuery .= '';
1492
                }
1493
            }
1494
        }
1495
1496
        $this->BO->setLastQuery($sqlQuery);
1497
1498
        if (!$result = self::getConnection()->query($sqlQuery)) {
1499
            self::$logger->debug('<<addProperty');
1500
            throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->BO->getTableName().'], query is ['.$this->BO->getLastQuery().']');
1501
        } else {
1502
            self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->BO->getTableName().'] table for the class ['.get_class($this->BO).']');
1503
        }
1504
1505
        if ($this->BO->getMaintainHistory()) {
1506
            $sqlQuery = str_replace($this->BO->getTableName(), $this->BO->getTableName().'_history', $sqlQuery);
1507
1508
            if (!$result = self::getConnection()->query($sqlQuery)) {
1509
                self::$logger->debug('<<addProperty');
1510
                throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->BO->getTableName().'_history], query is ['.$this->BO->getLastQuery().']');
1511
            } else {
1512
                self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->BO->getTableName().'_history] table for the class ['.get_class($this->BO).']');
1513
            }
1514
        }
1515
1516
        self::$logger->debug('<<addProperty');
1517
    }
1518
1519
    /**
1520
     * (non-PHPdoc).
1521
     *
1522
     * @see Alpha\Model\ActiveRecordProviderInterface::getMAX()
1523
     */
1524
    public function getMAX()
1525
    {
1526
        self::$logger->debug('>>getMAX()');
1527
1528
        $sqlQuery = 'SELECT MAX(OID) AS max_OID FROM '.$this->BO->getTableName();
1529
1530
        $this->BO->setLastQuery($sqlQuery);
1531
1532
        try {
1533
            $result = $this->BO->query($sqlQuery);
1534
1535
            $row = $result[0];
1536
1537
            if (isset($row['max_OID'])) {
1538
                self::$logger->debug('<<getMAX ['.$row['max_OID'].']');
1539
1540
                return $row['max_OID'];
1541
            } else {
1542
                throw new AlphaException('Failed to get the MAX ID for the class ['.get_class($this->BO).'] from the table ['.$this->BO->getTableName().'], query is ['.$this->BO->getLastQuery().']');
1543
            }
1544
        } catch (\Exception $e) {
1545
            self::$logger->debug('<<getMAX');
1546
            throw new AlphaException($e->getMessage());
1547
        }
1548
    }
1549
1550
    /**
1551
     * (non-PHPdoc).
1552
     *
1553
     * @see Alpha\Model\ActiveRecordProviderInterface::getCount()
1554
     */
1555
    public function getCount($attributes = array(), $values = array())
1556
    {
1557
        self::$logger->debug('>>getCount(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'])');
1558
1559
        if ($this->BO->isTableOverloaded()) {
1560
            $whereClause = ' WHERE classname = \''.addslashes(get_class($this->BO)).'\' AND';
1561
        } else {
1562
            $whereClause = ' WHERE';
1563
        }
1564
1565
        $count = count($attributes);
1566
1567
        for ($i = 0; $i < $count; ++$i) {
1568
            $whereClause .= ' '.$attributes[$i].' = \''.$values[$i].'\' AND';
1569
            self::$logger->debug($whereClause);
1570
        }
1571
        // remove the last " AND"
1572
        $whereClause = mb_substr($whereClause, 0, -4);
1573
1574
        if ($whereClause != ' WHERE') {
1575
            $sqlQuery = 'SELECT COUNT(OID) AS class_count FROM '.$this->BO->getTableName().$whereClause;
1576
        } else {
1577
            $sqlQuery = 'SELECT COUNT(OID) AS class_count FROM '.$this->BO->getTableName();
1578
        }
1579
1580
        $this->BO->setLastQuery($sqlQuery);
1581
1582
        $result = self::getConnection()->query($sqlQuery);
1583
1584
        if ($result) {
1585
            $row = $result->fetch_array(MYSQLI_ASSOC);
1586
1587
            self::$logger->debug('<<getCount ['.$row['class_count'].']');
1588
1589
            return $row['class_count'];
1590
        } else {
1591
            self::$logger->debug('<<getCount');
1592
            throw new AlphaException('Failed to get the count for the class ['.get_class($this->BO).'] from the table ['.$this->BO->getTableName().'], query is ['.$this->BO->getLastQuery().']');
1593
        }
1594
    }
1595
1596
    /**
1597
     * (non-PHPdoc).
1598
     *
1599
     * @see Alpha\Model\ActiveRecordProviderInterface::getHistoryCount()
1600
     */
1601
    public function getHistoryCount()
1602
    {
1603
        self::$logger->debug('>>getHistoryCount()');
1604
1605
        if (!$this->BO->getMaintainHistory()) {
1606
            throw new AlphaException('getHistoryCount method called on a DAO where no history is maintained!');
1607
        }
1608
1609
        $sqlQuery = 'SELECT COUNT(OID) AS object_count FROM '.$this->BO->getTableName().'_history WHERE OID='.$this->BO->getOID();
1610
1611
        $this->BO->setLastQuery($sqlQuery);
1612
1613
        $result = self::getConnection()->query($sqlQuery);
1614
1615
        if ($result) {
1616
            $row = $result->fetch_array(MYSQLI_ASSOC);
1617
1618
            self::$logger->debug('<<getHistoryCount ['.$row['object_count'].']');
1619
1620
            return $row['object_count'];
1621
        } else {
1622
            self::$logger->debug('<<getHistoryCount');
1623
            throw new AlphaException('Failed to get the history count for the business object ['.$this->BO->getOID().'] from the table ['.$this->BO->getTableName().'_history], query is ['.$this->BO->getLastQuery().']');
1624
        }
1625
    }
1626
1627
    /**
1628
     * (non-PHPdoc).
1629
     *
1630
     * @see Alpha\Model\ActiveRecordProviderInterface::setEnumOptions()
1631
     * @since 1.1
1632
     */
1633
    public function setEnumOptions()
1634
    {
1635
        self::$logger->debug('>>setEnumOptions()');
1636
1637
        // get the class attributes
1638
        $reflection = new ReflectionClass(get_class($this->BO));
1639
        $properties = $reflection->getProperties();
1640
1641
        // flag for any database errors
1642
        $dbError = false;
1643
1644
        foreach ($properties as $propObj) {
1645
            $propName = $propObj->name;
1646
            if (!in_array($propName, $this->BO->getDefaultAttributes()) && !in_array($propName, $this->BO->getTransientAttributes())) {
1647
                $propClass = get_class($this->BO->getPropObject($propName));
1648
                if ($propClass == 'Enum') {
1649
                    $sqlQuery = 'SHOW COLUMNS FROM '.$this->BO->getTableName()." LIKE '$propName'";
1650
1651
                    $this->BO->setLastQuery($sqlQuery);
1652
1653
                    $result = self::getConnection()->query($sqlQuery);
1654
1655
                    if ($result) {
1656
                        $row = $result->fetch_array(MYSQLI_NUM);
1657
                        $options = explode("','", preg_replace("/(enum|set)\('(.+?)'\)/", '\\2', $row[1]));
1658
1659
                        $this->BO->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...
1660
                    } else {
1661
                        $dbError = true;
1662
                        break;
1663
                    }
1664
                }
1665
            }
1666
        }
1667
1668
        if (!$dbError) {
1669
            if (method_exists($this, 'after_setEnumOptions_callback')) {
1670
                $this->{'after_setEnumOptions_callback'}();
1671
            }
1672
        } else {
1673
            throw new AlphaException('Failed to load enum options correctly for object instance of class ['.get_class($this).']');
1674
        }
1675
        self::$logger->debug('<<setEnumOptions');
1676
    }
1677
1678
    /**
1679
     * (non-PHPdoc).
1680
     *
1681
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableExists()
1682
     */
1683
    public function checkTableExists($checkHistoryTable = false)
1684
    {
1685
        self::$logger->debug('>>checkTableExists(checkHistoryTable=['.$checkHistoryTable.'])');
1686
1687
        $tableExists = false;
1688
1689
        $sqlQuery = 'SHOW TABLES;';
1690
        $this->BO->setLastQuery($sqlQuery);
1691
1692
        $result = self::getConnection()->query($sqlQuery);
1693
1694
        if ($result) {
1695
            $tableName = ($checkHistoryTable ? $this->BO->getTableName().'_history' : $this->BO->getTableName());
1696
1697
            while ($row = $result->fetch_array(MYSQLI_NUM)) {
1698
                if (strtolower($row[0]) == mb_strtolower($tableName)) {
1699
                    $tableExists = true;
1700
                }
1701
            }
1702
1703
            self::$logger->debug('<<checkTableExists ['.$tableExists.']');
1704
1705
            return $tableExists;
1706
        } else {
1707
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1708
            self::$logger->debug('<<checkTableExists [false]');
0 ignored issues
show
Unused Code introduced by
self::$logger->debug('<<...kTableExists [false]'); does not seem to be reachable.

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

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

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

    return false;
}

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

Loading history...
1709
1710
            return false;
1711
        }
1712
    }
1713
1714
    /**
1715
     * (non-PHPdoc).
1716
     *
1717
     * @see Alpha\Model\ActiveRecordProviderInterface::checkBOTableExists()
1718
     */
1719
    public static function checkBOTableExists($BOClassName, $checkHistoryTable = false)
1720
    {
1721
        if (self::$logger == null) {
1722
            self::$logger = new Logger('ActiveRecordProviderMySQL');
1723
        }
1724
        self::$logger->debug('>>checkBOTableExists(BOClassName=['.$BOClassName.'], checkHistoryTable=['.$checkHistoryTable.'])');
1725
1726
        if (!class_exists($BOClassName)) {
1727
            throw new IllegalArguementException('The classname provided ['.$checkHistoryTable.'] is not defined!');
1728
        }
1729
1730
        $tableName = $BOClassName::TABLE_NAME;
1731
1732
        if (empty($tableName)) {
1733
            $tableName = mb_substr($BOClassName, 0, mb_strpos($BOClassName, '_'));
1734
        }
1735
1736
        if ($checkHistoryTable) {
1737
            $tableName .= '_history';
1738
        }
1739
1740
        $tableExists = false;
1741
1742
        $sqlQuery = 'SHOW TABLES;';
1743
1744
        $result = self::getConnection()->query($sqlQuery);
1745
1746
        while ($row = $result->fetch_array(MYSQLI_NUM)) {
1747
            if ($row[0] == $tableName) {
1748
                $tableExists = true;
1749
            }
1750
        }
1751
1752
        if ($result) {
1753
            self::$logger->debug('<<checkBOTableExists ['.($tableExists ? 'true' : 'false').']');
1754
1755
            return $tableExists;
1756
        } else {
1757
            self::$logger->debug('<<checkBOTableExists');
1758
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1759
        }
1760
    }
1761
1762
    /**
1763
     * (non-PHPdoc).
1764
     *
1765
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableNeedsUpdate()
1766
     */
1767
    public function checkTableNeedsUpdate()
1768
    {
1769
        self::$logger->debug('>>checkTableNeedsUpdate()');
1770
1771
        $updateRequired = false;
1772
1773
        $matchCount = 0;
1774
1775
        $query = 'SHOW COLUMNS FROM '.$this->BO->getTableName();
1776
        $result = self::getConnection()->query($query);
1777
        $this->BO->setLastQuery($query);
1778
1779
        // get the class attributes
1780
        $reflection = new ReflectionClass(get_class($this->BO));
1781
        $properties = $reflection->getProperties();
1782
1783
        foreach ($properties as $propObj) {
1784
            $propName = $propObj->name;
1785
            if (!in_array($propName, $this->BO->getTransientAttributes())) {
1786
                $foundMatch = false;
1787
1788
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1789
                    if ($propName == $row['Field']) {
1790
                        $foundMatch = true;
1791
                        break;
1792
                    }
1793
                }
1794
1795
                if (!$foundMatch) {
1796
                    --$matchCount;
1797
                }
1798
1799
                $result->data_seek(0);
1800
            }
1801
        }
1802
1803
        // check for the "classname" field in overloaded tables
1804
        if ($this->BO->isTableOverloaded()) {
1805
            $foundMatch = false;
1806
1807
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1808
                if ('classname' == $row['Field']) {
1809
                    $foundMatch = true;
1810
                    break;
1811
                }
1812
            }
1813
            if (!$foundMatch) {
1814
                --$matchCount;
1815
            }
1816
        }
1817
1818
        if ($matchCount != 0) {
1819
            $updateRequired = true;
1820
        }
1821
1822
        if ($result) {
1823
            // check the table indexes
1824
            try {
1825
                $this->checkIndexes();
1826
            } catch (AlphaException $ae) {
1827
                self::$logger->warn("Error while checking database indexes:\n\n".$ae->getMessage());
1828
            }
1829
1830
            self::$logger->debug('<<checkTableNeedsUpdate ['.$updateRequired.']');
1831
1832
            return $updateRequired;
1833
        } else {
1834
            self::$logger->debug('<<checkTableNeedsUpdate');
1835
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1836
        }
1837
    }
1838
1839
    /**
1840
     * (non-PHPdoc).
1841
     *
1842
     * @see Alpha\Model\ActiveRecordProviderInterface::findMissingFields()
1843
     */
1844
    public function findMissingFields()
1845
    {
1846
        self::$logger->debug('>>findMissingFields()');
1847
1848
        $missingFields = array();
1849
        $matchCount = 0;
1850
1851
        $sqlQuery = 'SHOW COLUMNS FROM '.$this->BO->getTableName();
1852
1853
        $result = self::getConnection()->query($sqlQuery);
1854
1855
        $this->BO->setLastQuery($sqlQuery);
1856
1857
        // get the class attributes
1858
        $reflection = new ReflectionClass(get_class($this->BO));
1859
        $properties = $reflection->getProperties();
1860
1861
        foreach ($properties as $propObj) {
1862
            $propName = $propObj->name;
1863
            if (!in_array($propName, $this->BO->getTransientAttributes())) {
1864
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1865
                    if ($propName == $row['Field']) {
1866
                        ++$matchCount;
1867
                        break;
1868
                    }
1869
                }
1870
                $result->data_seek(0);
1871
            } else {
1872
                ++$matchCount;
1873
            }
1874
1875
            if ($matchCount == 0) {
1876
                array_push($missingFields, $propName);
1877
            } else {
1878
                $matchCount = 0;
1879
            }
1880
        }
1881
1882
        // check for the "classname" field in overloaded tables
1883
        if ($this->BO->isTableOverloaded()) {
1884
            $foundMatch = false;
1885
1886
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1887
                if ('classname' == $row['Field']) {
1888
                    $foundMatch = true;
1889
                    break;
1890
                }
1891
            }
1892
            if (!$foundMatch) {
1893
                array_push($missingFields, 'classname');
1894
            }
1895
        }
1896
1897
        if (!$result) {
1898
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1899
        }
1900
1901
        self::$logger->debug('<<findMissingFields ['.var_export($missingFields, true).']');
1902
1903
        return $missingFields;
1904
    }
1905
1906
    /**
1907
     * (non-PHPdoc).
1908
     *
1909
     * @see Alpha\Model\ActiveRecordProviderInterface::getIndexes()
1910
     */
1911
    public function getIndexes()
1912
    {
1913
        self::$logger->debug('>>getIndexes()');
1914
1915
        $query = 'SHOW INDEX FROM '.$this->BO->getTableName();
1916
1917
        $result = self::getConnection()->query($query);
1918
1919
        $this->BO->setLastQuery($query);
1920
1921
        $indexNames = array();
1922
1923
        if (!$result) {
1924
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getConnection()->error.']');
1925
        } else {
1926
            while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
1927
                array_push($indexNames, $row['Key_name']);
1928
            }
1929
        }
1930
1931
        self::$logger->debug('<<getIndexes');
1932
1933
        return $indexNames;
1934
    }
1935
1936
    /**
1937
     * Checks to see if all of the indexes are in place for the BO's table, creates those that are missing.
1938
     *
1939
     * @since 1.1
1940
     */
1941
    private function checkIndexes()
1942
    {
1943
        self::$logger->debug('>>checkIndexes()');
1944
1945
        $indexNames = $this->getIndexes();
1946
1947
        // process unique keys
1948
        foreach ($this->BO->getUniqueAttributes() as $prop) {
1949
            // check for composite indexes
1950
            if (mb_strpos($prop, '+')) {
1951
                $attributes = explode('+', $prop);
1952
1953
                $index_exists = false;
1954
                foreach ($indexNames as $index) {
1955
                    if ($attributes[0].'_'.$attributes[1].'_unq_idx' == $index) {
1956
                        $index_exists = true;
1957
                    }
1958
                    if (count($attributes) == 3) {
1959
                        if ($attributes[0].'_'.$attributes[1].'_'.$attributes[2].'_unq_idx' == $index) {
1960
                            $index_exists = true;
1961
                        }
1962
                    }
1963
                }
1964
1965
                if (!$index_exists) {
1966
                    if (count($attributes) == 3) {
1967
                        $this->BO->createUniqueIndex($attributes[0], $attributes[1], $attributes[2]);
1968
                    } else {
1969
                        $this->BO->createUniqueIndex($attributes[0], $attributes[1]);
1970
                    }
1971
                }
1972
            } else {
1973
                $index_exists = false;
1974
                foreach ($indexNames as $index) {
1975
                    if ($prop.'_unq_idx' == $index) {
1976
                        $index_exists = true;
1977
                    }
1978
                }
1979
1980
                if (!$index_exists) {
1981
                    $this->createUniqueIndex($prop);
1982
                }
1983
            }
1984
        }
1985
1986
        // process foreign-key indexes
1987
        // get the class attributes
1988
        $reflection = new ReflectionClass(get_class($this->BO));
1989
        $properties = $reflection->getProperties();
1990
1991
        foreach ($properties as $propObj) {
1992
            $propName = $propObj->name;
1993
            $prop = $this->BO->getPropObject($propName);
1994
            if ($prop instanceof Relation) {
1995
                if ($prop->getRelationType() == 'MANY-TO-ONE') {
1996
                    $indexExists = false;
1997
                    foreach ($indexNames as $index) {
1998
                        if ($this->BO->getTableName().'_'.$propName.'_fk_idx' == $index) {
1999
                            $indexExists = true;
2000
                        }
2001
                    }
2002
2003
                    if (!$indexExists) {
2004
                        $this->createForeignIndex($propName, $prop->getRelatedClass(), $prop->getRelatedClassField());
2005
                    }
2006
                }
2007
2008
                if ($prop->getRelationType() == 'MANY-TO-MANY') {
2009
                    $lookup = $prop->getLookup();
2010
2011
                    if ($lookup != null) {
2012
                        try {
2013
                            $lookupIndexNames = $lookup->getIndexes();
2014
2015
                            // handle index check/creation on left side of Relation
2016
                            $indexExists = false;
2017
                            foreach ($lookupIndexNames as $index) {
2018
                                if ($lookup->getTableName().'_leftID_fk_idx' == $index) {
2019
                                    $indexExists = true;
2020
                                }
2021
                            }
2022
2023
                            if (!$indexExists) {
2024
                                $lookup->createForeignIndex('leftID', $prop->getRelatedClass('left'), 'OID');
2025
                            }
2026
2027
                            // handle index check/creation on right side of Relation
2028
                            $indexExists = false;
2029
                            foreach ($lookupIndexNames as $index) {
2030
                                if ($lookup->getTableName().'_rightID_fk_idx' == $index) {
2031
                                    $indexExists = true;
2032
                                }
2033
                            }
2034
2035
                            if (!$indexExists) {
2036
                                $lookup->createForeignIndex('rightID', $prop->getRelatedClass('right'), 'OID');
2037
                            }
2038
                        } catch (AlphaException $e) {
2039
                            self::$logger->error($e->getMessage());
2040
                        }
2041
                    }
2042
                }
2043
            }
2044
        }
2045
2046
        self::$logger->debug('<<checkIndexes');
2047
    }
2048
2049
    /**
2050
     * (non-PHPdoc).
2051
     *
2052
     * @see Alpha\Model\ActiveRecordProviderInterface::createForeignIndex()
2053
     */
2054
    public function createForeignIndex($attributeName, $relatedClass, $relatedClassAttribute, $indexName = null)
2055
    {
2056
        self::$logger->debug('>>createForeignIndex(attributeName=['.$attributeName.'], relatedClass=['.$relatedClass.'], relatedClassAttribute=['.$relatedClassAttribute.'], indexName=['.$indexName.']');
2057
2058
        $relatedBO = new $relatedClass();
2059
        $tableName = $relatedBO->getTableName();
2060
2061
        $result = false;
2062
2063
        if (self::checkBOTableExists($relatedClass)) {
2064
            $sqlQuery = '';
2065
2066
            if ($attributeName == 'leftID') {
2067
                if ($indexName === null) {
2068
                    $indexName = $this->BO->getTableName().'_leftID_fk_idx';
2069
                }
2070
                $sqlQuery = 'ALTER TABLE '.$this->BO->getTableName().' ADD INDEX '.$indexName.' (leftID);';
2071
            }
2072
            if ($attributeName == 'rightID') {
2073
                if ($indexName === null) {
2074
                    $indexName = $this->BO->getTableName().'_rightID_fk_idx';
2075
                }
2076
                $sqlQuery = 'ALTER TABLE '.$this->BO->getTableName().' ADD INDEX '.$indexName.' (rightID);';
2077
            }
2078
2079
            if (!empty($sqlQuery)) {
2080
                $this->BO->setLastQuery($sqlQuery);
2081
2082
                $result = self::getConnection()->query($sqlQuery);
2083
2084
                if (!$result) {
2085
                    throw new FailedIndexCreateException('Failed to create an index on ['.$this->BO->getTableName().'], error is ['.self::getConnection()->error.'], query ['.$this->BO->getLastQuery().']');
2086
                }
2087
            }
2088
2089
            if ($indexName === null) {
2090
                $indexName = $this->BO->getTableName().'_'.$attributeName.'_fk_idx';
2091
            }
2092
2093
            $sqlQuery = 'ALTER TABLE '.$this->BO->getTableName().' ADD FOREIGN KEY '.$indexName.' ('.$attributeName.') REFERENCES '.$tableName.' ('.$relatedClassAttribute.') ON DELETE SET NULL;';
2094
2095
            $this->BO->setLastQuery($sqlQuery);
2096
            $result = self::getConnection()->query($sqlQuery);
2097
        }
2098
2099
        if ($result) {
2100
            self::$logger->debug('Successfully created the foreign key index ['.$indexName.']');
2101
        } else {
2102
            throw new FailedIndexCreateException('Failed to create the index ['.$indexName.'] on ['.$this->BO->getTableName().'], error is ['.self::getConnection()->error.'], query ['.$this->BO->getLastQuery().']');
2103
        }
2104
2105
        self::$logger->debug('<<createForeignIndex');
2106
    }
2107
2108
    /**
2109
     * (non-PHPdoc).
2110
     *
2111
     * @see Alpha\Model\ActiveRecordProviderInterface::createUniqueIndex()
2112
     */
2113
    public function createUniqueIndex($attribute1Name, $attribute2Name = '', $attribute3Name = '')
2114
    {
2115
        self::$logger->debug('>>createUniqueIndex(attribute1Name=['.$attribute1Name.'], attribute2Name=['.$attribute2Name.'], attribute3Name=['.$attribute3Name.'])');
2116
2117
        $sqlQuery = '';
2118
2119
        if ($attribute2Name != '' && $attribute3Name != '') {
2120
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_'.$attribute2Name.'_'.$attribute3Name.'_unq_idx ON '.$this->BO->getTableName().' ('.$attribute1Name.','.$attribute2Name.','.$attribute3Name.');';
2121
        }
2122
2123
        if ($attribute2Name != '' && $attribute3Name == '') {
2124
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_'.$attribute2Name.'_unq_idx ON '.$this->BO->getTableName().' ('.$attribute1Name.','.$attribute2Name.');';
2125
        }
2126
2127
        if ($attribute2Name == '' && $attribute3Name == '') {
2128
            $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_unq_idx ON '.$this->BO->getTableName().' ('.$attribute1Name.');';
2129
        }
2130
2131
        $this->BO->setLastQuery($sqlQuery);
2132
2133
        $result = self::getConnection()->query($sqlQuery);
2134
2135
        if ($result) {
2136
            self::$logger->debug('Successfully created the unique index on ['.$this->BO->getTableName().']');
2137
        } else {
2138
            throw new FailedIndexCreateException('Failed to create the unique index on ['.$this->BO->getTableName().'], error is ['.self::getConnection()->error.']');
2139
        }
2140
2141
        self::$logger->debug('<<createUniqueIndex');
2142
    }
2143
2144
    /**
2145
     * (non-PHPdoc).
2146
     *
2147
     * @see Alpha\Model\ActiveRecordProviderInterface::reload()
2148
     */
2149
    public function reload()
2150
    {
2151
        self::$logger->debug('>>reload()');
2152
2153
        if (!$this->BO->isTransient()) {
2154
            $this->BO->load($this->BO->getOID());
2155
        } else {
2156
            throw new AlphaException('Cannot reload transient object from database!');
2157
        }
2158
        self::$logger->debug('<<reload');
2159
    }
2160
2161
    /**
2162
     * (non-PHPdoc).
2163
     *
2164
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordExists()
2165
     */
2166
    public function checkRecordExists($OID)
2167
    {
2168
        self::$logger->debug('>>checkRecordExists(OID=['.$OID.'])');
2169
2170
        $sqlQuery = 'SELECT OID FROM '.$this->BO->getTableName().' WHERE OID = ?;';
2171
2172
        $this->BO->setLastQuery($sqlQuery);
2173
2174
        $stmt = self::getConnection()->stmt_init();
2175
2176
        if ($stmt->prepare($sqlQuery)) {
2177
            $stmt->bind_param('i', $OID);
2178
2179
            $stmt->execute();
2180
2181
            $result = $this->bindResult($stmt);
2182
2183
            $stmt->close();
2184
2185
            if (is_array($result)) {
2186
                if (count($result) > 0) {
2187
                    self::$logger->debug('<<checkRecordExists [true]');
2188
2189
                    return true;
2190
                } else {
2191
                    self::$logger->debug('<<checkRecordExists [false]');
2192
2193
                    return false;
2194
                }
2195
            } else {
2196
                self::$logger->debug('<<checkRecordExists');
2197
                throw new AlphaException('Failed to check for the record ['.$OID.'] on the class ['.get_class($this->BO).'] from the table ['.$this->BO->getTableName().'], query is ['.$this->BO->getLastQuery().']');
2198
            }
2199
        } else {
2200
            self::$logger->debug('<<checkRecordExists');
2201
            throw new AlphaException('Failed to check for the record ['.$OID.'] on the class ['.get_class($this->BO).'] from the table ['.$this->BO->getTableName().'], query is ['.$this->BO->getLastQuery().']');
2202
        }
2203
    }
2204
2205
    /**
2206
     * (non-PHPdoc).
2207
     *
2208
     * @see Alpha\Model\ActiveRecordProviderInterface::isTableOverloaded()
2209
     */
2210
    public function isTableOverloaded()
2211
    {
2212
        self::$logger->debug('>>isTableOverloaded()');
2213
2214
        $reflection = new ReflectionClass($this->BO);
2215
        $classname = $reflection->getShortName();
2216
        $tablename = ucfirst($this->BO->getTableName());
2217
2218
        // use reflection to check to see if we are dealing with a persistent type (e.g. DEnum) which are never overloaded
2219
        $implementedInterfaces = $reflection->getInterfaces();
2220
2221
        foreach ($implementedInterfaces as $interface) {
2222
            if ($interface->name == 'Alpha\Model\Type\TypeInterface') {
2223
                self::$logger->debug('<<isTableOverloaded [false]');
2224
2225
                return false;
2226
            }
2227
        }
2228
2229
        if ($classname != $tablename) {
2230
            // loop over all BOs to see if there is one using the same table as this BO
2231
2232
            $BOclasses = ActiveRecord::getBOClassNames();
2233
2234
            foreach ($BOclasses as $BOclassName) {
2235
                $reflection = new ReflectionClass($BOclassName);
2236
                $classname = $reflection->getShortName();
2237
                if ($tablename == $classname) {
2238
                    self::$logger->debug('<<isTableOverloaded [true]');
2239
2240
                    return true;
2241
                }
2242
            }
2243
2244
            self::$logger->debug('<<isTableOverloaded');
2245
            throw new BadTableNameException('The table name ['.$tablename.'] for the class ['.$classname.'] is invalid as it does not match a BO definition in the system!');
2246
        } else {
2247
            // check to see if there is already a "classname" column in the database for this BO
2248
2249
            $query = 'SHOW COLUMNS FROM '.$this->BO->getTableName();
2250
2251
            $result = self::getConnection()->query($query);
2252
2253
            if ($result) {
2254
                while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
2255
                    if ('classname' == $row['Field']) {
2256
                        self::$logger->debug('<<isTableOverloaded [true]');
2257
2258
                        return true;
2259
                    }
2260
                }
2261
            } else {
2262
                self::$logger->warn('Error during show columns ['.self::getConnection()->error.']');
2263
            }
2264
2265
            self::$logger->debug('<<isTableOverloaded [false]');
2266
2267
            return false;
2268
        }
2269
    }
2270
2271
    /**
2272
     * (non-PHPdoc).
2273
     *
2274
     * @see Alpha\Model\ActiveRecordProviderInterface::begin()
2275
     */
2276
    public static function begin()
2277
    {
2278
        if (self::$logger == null) {
2279
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2280
        }
2281
        self::$logger->debug('>>begin()');
2282
2283
        if (!self::getConnection()->autocommit(false)) {
2284
            throw new AlphaException('Error beginning a new transaction, error is ['.self::getConnection()->error.']');
2285
        }
2286
2287
        self::$logger->debug('<<begin');
2288
    }
2289
2290
    /**
2291
     * (non-PHPdoc).
2292
     *
2293
     * @see Alpha\Model\ActiveRecordProviderInterface::commit()
2294
     */
2295
    public static function commit()
2296
    {
2297
        if (self::$logger == null) {
2298
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2299
        }
2300
        self::$logger->debug('>>commit()');
2301
2302
        if (!self::getConnection()->commit()) {
2303
            throw new FailedSaveException('Error commiting a transaction, error is ['.self::getConnection()->error.']');
2304
        }
2305
2306
        self::$logger->debug('<<commit');
2307
    }
2308
2309
    /**
2310
     * (non-PHPdoc).
2311
     *
2312
     * @see Alpha\Model\ActiveRecordProviderInterface::rollback()
2313
     */
2314
    public static function rollback()
2315
    {
2316
        if (self::$logger == null) {
2317
            self::$logger = new Logger('ActiveRecordProviderMySQL');
2318
        }
2319
        self::$logger->debug('>>rollback()');
2320
2321
        if (!self::getConnection()->rollback()) {
2322
            throw new AlphaException('Error rolling back a transaction, error is ['.self::getConnection()->error.']');
2323
        }
2324
2325
        self::$logger->debug('<<rollback');
2326
    }
2327
2328
    /**
2329
     * (non-PHPdoc).
2330
     *
2331
     * @see Alpha\Model\ActiveRecordProviderInterface::setBO()
2332
     */
2333
    public function setBO($BO)
2334
    {
2335
        $this->BO = $BO;
2336
    }
2337
2338
    /**
2339
     * Dynamically binds all of the attributes for the current BO to the supplied prepared statement
2340
     * parameters.  If arrays of attribute names and values are provided, only those will be bound to
2341
     * the supplied statement.
2342
     *
2343
     * @param mysqli_stmt $stmt The SQL statement to bind to.
2344
     * @param array Optional array of BO attributes.
2345
     * @param array Optional array of BO values.
2346
     *
2347
     * @return mysqli_stmt
2348
     *
2349
     * @since 1.1
2350
     */
2351
    private function bindParams($stmt, $attributes = array(), $values = array())
2352
    {
2353
        self::$logger->debug('>>bindParams(stmt=['.var_export($stmt, true).'])');
2354
2355
        $bindingsTypes = '';
2356
        $params = array();
2357
2358
        // here we are only binding the supplied attributes
2359
        if (count($attributes) > 0 && count($attributes) == count($values)) {
2360
            $count = count($values);
2361
2362
            for ($i = 0; $i < $count; ++$i) {
2363
                if (Validator::isInteger($values[$i])) {
2364
                    $bindingsTypes .= 'i';
2365
                } else {
2366
                    $bindingsTypes .= 's';
2367
                }
2368
                array_push($params, $values[$i]);
2369
            }
2370
2371
            if ($this->BO->isTableOverloaded()) {
2372
                $bindingsTypes .= 's';
2373
                array_push($params, get_class($this->BO));
2374
            }
2375
        } else { // bind all attributes on the business object
2376
2377
            // get the class attributes
2378
            $reflection = new ReflectionClass(get_class($this->BO));
2379
            $properties = $reflection->getProperties();
2380
2381
            foreach ($properties as $propObj) {
2382
                $propName = $propObj->name;
2383
                if (!in_array($propName, $this->BO->getTransientAttributes())) {
2384
                    // Skip the OID, database auto number takes care of this.
2385
                    if ($propName != 'OID' && $propName != 'version_num') {
2386
                        if ($this->BO->getPropObject($propName) instanceof Integer) {
2387
                            $bindingsTypes .= 'i';
2388
                        } else {
2389
                            $bindingsTypes .= 's';
2390
                        }
2391
                        array_push($params, $this->BO->get($propName));
2392
                    }
2393
2394
                    if ($propName == 'version_num') {
2395
                        $temp = $this->BO->getVersionNumber()->getValue();
2396
                        $this->BO->set('version_num', $temp + 1);
2397
                        $bindingsTypes .= 'i';
2398
                        array_push($params, $this->BO->getVersionNumber()->getValue());
2399
                    }
2400
                }
2401
            }
2402
2403
            if ($this->BO->isTableOverloaded()) {
2404
                $bindingsTypes .= 's';
2405
                array_push($params, get_class($this->BO));
2406
            }
2407
2408
            // the OID may be on the WHERE clause for UPDATEs and DELETEs
2409
            if (!$this->BO->isTransient()) {
2410
                $bindingsTypes .= 'i';
2411
                array_push($params, $this->BO->getOID());
2412
            }
2413
        }
2414
2415
        self::$logger->debug('bindingsTypes=['.$bindingsTypes.'], count: ['.mb_strlen($bindingsTypes).']');
2416
        self::$logger->debug('params ['.var_export($params, true).']');
2417
2418
        if ($params != null) {
2419
            $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...
2420
2421
            $count = count($params);
2422
2423
            for ($i = 0; $i < $count; ++$i) {
2424
                $bind_name = 'bind'.$i;
2425
                $$bind_name = $params[$i];
2426
                $bind_names[] = &$$bind_name;
2427
            }
2428
2429
            call_user_func_array(array($stmt, 'bind_param'), $bind_names);
2430
        }
2431
2432
        self::$logger->debug('<<bindParams ['.var_export($stmt, true).']');
2433
2434
        return $stmt;
2435
    }
2436
2437
    /**
2438
     * Dynamically binds the result of the supplied prepared statement to a 2d array, where each element in the array is another array
2439
     * representing a database row.
2440
     *
2441
     * @param \mysqli_stmt $stmt
2442
     *
2443
     * @return array A 2D array containing the query result.
2444
     *
2445
     * @since 1.1
2446
     */
2447
    private function bindResult($stmt)
2448
    {
2449
        $result = array();
2450
2451
        $metadata = $stmt->result_metadata();
2452
        $fields = $metadata->fetch_fields();
2453
2454
        while (true) {
2455
            $pointers = array();
2456
            $row = array();
2457
2458
            $pointers[] = $stmt;
2459
            foreach ($fields as $field) {
2460
                $fieldname = $field->name;
2461
                $pointers[] = &$row[$fieldname];
2462
            }
2463
2464
            call_user_func_array('mysqli_stmt_bind_result', $pointers);
2465
2466
            if (!$stmt->fetch()) {
2467
                break;
2468
            }
2469
2470
            $result[] = $row;
2471
        }
2472
2473
        $metadata->free();
2474
2475
        return $result;
2476
    }
2477
2478
    /**
2479
     * Parses a MySQL error for the value that violated a unique constraint.
2480
     *
2481
     * @param string $error The MySQL error string.
2482
     *
2483
     * @since 1.1
2484
     */
2485
    private function findOffendingValue($error)
2486
    {
2487
        self::$logger->debug('>>findOffendingValue(error=['.$error.'])');
2488
2489
        $singleQuote1 = mb_strpos($error, "'");
2490
        $singleQuote2 = mb_strrpos($error, "'");
2491
2492
        $value = mb_substr($error, $singleQuote1, ($singleQuote2 - $singleQuote1) + 1);
2493
        self::$logger->debug('<<findOffendingValue ['.$value.'])');
2494
2495
        return $value;
2496
    }
2497
2498
    /**
2499
     * (non-PHPdoc).
2500
     *
2501
     * @see Alpha\Model\ActiveRecordProviderInterface::checkDatabaseExists()
2502
     */
2503
    public static function checkDatabaseExists()
2504
    {
2505
        $config = ConfigProvider::getInstance();
2506
2507
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2508
2509
        $result = $connection->query('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \''.$config->get('db.name').'\'');
2510
2511
        if (count($result) > 0) {
2512
            return true;
2513
        } else {
2514
            return false;
2515
        }
2516
    }
2517
2518
    /**
2519
     * (non-PHPdoc).
2520
     *
2521
     * @see Alpha\Model\ActiveRecordProviderInterface::createDatabase()
2522
     */
2523
    public static function createDatabase()
2524
    {
2525
        $config = ConfigProvider::getInstance();
2526
2527
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2528
2529
        $connection->query('CREATE DATABASE '.$config->get('db.name'));
2530
    }
2531
2532
    /**
2533
     * (non-PHPdoc).
2534
     *
2535
     * @see Alpha\Model\ActiveRecordProviderInterface::dropDatabase()
2536
     */
2537
    public static function dropDatabase()
2538
    {
2539
        $config = ConfigProvider::getInstance();
2540
2541
        $connection = new Mysqli($config->get('db.hostname'), $config->get('db.username'), $config->get('db.password'));
2542
2543
        $connection->query('DROP DATABASE '.$config->get('db.name'));
2544
    }
2545
}
2546