ActiveRecordProviderSQLite::checkRecordExists()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 32
rs 9.408
cc 3
nc 3
nop 1
1
<?php
2
3
namespace Alpha\Model;
4
5
use Alpha\Model\Type\Integer;
6
use Alpha\Model\Type\Timestamp;
7
use Alpha\Model\Type\DEnum;
8
use Alpha\Model\Type\Relation;
9
use Alpha\Model\Type\RelationLookup;
10
use Alpha\Model\Type\Double;
11
use Alpha\Model\Type\Text;
12
use Alpha\Model\Type\LargeText;
13
use Alpha\Model\Type\HugeText;
14
use Alpha\Model\Type\SmallText;
15
use Alpha\Model\Type\Date;
16
use Alpha\Model\Type\Enum;
17
use Alpha\Model\Type\Boolean;
18
use Alpha\Util\Config\ConfigProvider;
19
use Alpha\Util\Logging\Logger;
20
use Alpha\Util\Service\ServiceFactory;
21
use Alpha\Exception\AlphaException;
22
use Alpha\Exception\FailedSaveException;
23
use Alpha\Exception\FailedDeleteException;
24
use Alpha\Exception\FailedIndexCreateException;
25
use Alpha\Exception\LockingException;
26
use Alpha\Exception\ValidationException;
27
use Alpha\Exception\CustomQueryException;
28
use Alpha\Exception\RecordNotFoundException;
29
use Alpha\Exception\BadTableNameException;
30
use Alpha\Exception\NotImplementedException;
31
use Alpha\Exception\PHPException;
32
use Alpha\Exception\ResourceNotAllowedException;
33
use Alpha\Exception\IllegalArguementException;
34
use Exception;
35
use SQLite3Stmt;
36
use SQLite3;
37
use ReflectionClass;
38
39
/**
40
 * SQLite active record provider (uses the SQLite3 native API in PHP).
41
 *
42
 * @since 1.2
43
 *
44
 * @author John Collins <[email protected]>
45
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
46
 * @copyright Copyright (c) 2018, John Collins (founder of Alpha Framework).
47
 * All rights reserved.
48
 *
49
 * <pre>
50
 * Redistribution and use in source and binary forms, with or
51
 * without modification, are permitted provided that the
52
 * following conditions are met:
53
 *
54
 * * Redistributions of source code must retain the above
55
 *   copyright notice, this list of conditions and the
56
 *   following disclaimer.
57
 * * Redistributions in binary form must reproduce the above
58
 *   copyright notice, this list of conditions and the
59
 *   following disclaimer in the documentation and/or other
60
 *   materials provided with the distribution.
61
 * * Neither the name of the Alpha Framework nor the names
62
 *   of its contributors may be used to endorse or promote
63
 *   products derived from this software without specific
64
 *   prior written permission.
65
 *
66
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
67
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
68
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
69
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
70
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
71
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
72
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
73
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
74
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
75
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
76
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
77
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
78
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
79
 * </pre>
80
 */
81
class ActiveRecordProviderSQLite implements ActiveRecordProviderInterface
82
{
83
    /**
84
     * Trace logger.
85
     *
86
     * @var \Alpha\Util\Logging\Logger
87
     *
88
     * @since 1.2
89
     */
90
    private static $logger = null;
91
92
    /**
93
     * Database connection.
94
     *
95
     * @var SQLite3
96
     *
97
     * @since 1.2
98
     */
99
    private static $connection;
100
101
    /**
102
     * The business object that we are mapping back to.
103
     *
104
     * @var \Alpha\Model\ActiveRecord
105
     *
106
     * @since 1.2
107
     */
108
    private $record;
109
110
    /**
111
     * An array of new foreign keys that need to be created.
112
     *
113
     * @var array
114
     *
115
     * @since 2.0.1
116
     */
117
    private $foreignKeys = array();
118
119
    /**
120
     * The constructor.
121
     *
122
     * @since 1.2
123
     */
124
    public function __construct()
125
    {
126
        self::$logger = new Logger('ActiveRecordProviderSQLite');
127
        self::$logger->debug('>>__construct()');
128
129
        self::$logger->debug('<<__construct');
130
    }
131
132
    /**
133
     * (non-PHPdoc).
134
     *
135
     * @see Alpha\Model\ActiveRecordProviderInterface::getConnection()
136
     */
137
    public static function getConnection()
138
    {
139
        $config = ConfigProvider::getInstance();
140
141
        if (!isset(self::$connection)) {
142
            try {
143
                self::$connection = new SQLite3($config->get('db.file.path'));
144
            } catch (\Exception $e) {
145
                self::$logger->fatal('Could not open SQLite database: ['.$e->getMessage().']');
146
            }
147
        }
148
149
        return self::$connection;
150
    }
151
152
    /**
153
     * (non-PHPdoc).
154
     *
155
     * @see Alpha\Model\ActiveRecordProviderInterface::disconnect()
156
     */
157
    public static function disconnect()
158
    {
159
        if (isset(self::$connection)) {
160
            self::$connection->close();
161
            self::$connection = null;
162
        }
163
    }
164
165
    /**
166
     * (non-PHPdoc).
167
     *
168
     * @see Alpha\Model\ActiveRecordProviderInterface::getLastDatabaseError()
169
     */
170
    public static function getLastDatabaseError()
171
    {
172
        self::$connection->lastErrorMsg();
173
    }
174
175
    /**
176
     * (non-PHPdoc).
177
     *
178
     * @see Alpha\Model\ActiveRecordProviderInterface::query()
179
     */
180
    public function query($sqlQuery)
181
    {
182
        $this->record->setLastQuery($sqlQuery);
183
184
        $resultArray = array();
185
186
        if (!$result = self::getConnection()->query($sqlQuery)) {
187
            throw new CustomQueryException('Failed to run the custom query, SQLite error is ['.self::getLastDatabaseError().'], query ['.$sqlQuery.']');
188
        } else {
189
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
190
                array_push($resultArray, $row);
191
            }
192
193
            return $resultArray;
194
        }
195
    }
196
197
    /**
198
     * (non-PHPdoc).
199
     *
200
     * @see Alpha\Model\ActiveRecordProviderInterface::load()
201
     */
202
    public function load($ID, $version = 0)
203
    {
204
        self::$logger->debug('>>load(ID=['.$ID.'], version=['.$version.'])');
205
206
        $attributes = $this->record->getPersistentAttributes();
207
        $fields = '';
208
        foreach ($attributes as $att) {
209
            $fields .= $att.',';
210
        }
211
        $fields = mb_substr($fields, 0, -1);
212
213
        if ($version > 0) {
214
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().'_history WHERE ID = :ID AND version_num = :version LIMIT 1;';
215
        } else {
216
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE ID = :ID LIMIT 1;';
217
        }
218
        $this->record->setLastQuery($sqlQuery);
219
220
        try {
221
            $stmt = self::getConnection()->prepare($sqlQuery);
222
223
            $row = array();
224
225
            if ($version > 0) {
226
                $stmt->bindValue(':version', $version, SQLITE3_INTEGER);
227
            }
228
229
            $stmt->bindValue(':ID', $ID, SQLITE3_INTEGER);
230
231
            $result = $stmt->execute();
232
233
            // there should only ever be one (or none)
234
            $row = $result->fetchArray(SQLITE3_ASSOC);
235
236
            $stmt->close();
237
        } catch (PHPException $e) {
238
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
239
            if (!$this->record->checkTableExists()) {
240
                $this->record->makeTable();
241
242
                throw new RecordNotFoundException('Failed to load object of ID ['.$ID.'], table ['.$this->record->getTableName().'] did not exist so had to create!');
243
            }
244
245
            return;
246
        }
247
248
        if (!isset($row['ID']) || $row['ID'] < 1) {
249
            self::$logger->debug('<<load');
250
            throw new RecordNotFoundException('Failed to load object of ID ['.$ID.'] not found in database.');
251
        }
252
253
        // get the class attributes
254
        $reflection = new ReflectionClass(get_class($this->record));
255
        $properties = $reflection->getProperties();
256
257
        try {
258
            foreach ($properties as $propObj) {
259
                $propName = $propObj->name;
260
261
                // filter transient attributes
262
                if (!in_array($propName, $this->record->getTransientAttributes())) {
263
                    $this->record->set($propName, $row[$propName]);
264
                } elseif (!$propObj->isPrivate() && $this->record->getPropObject($propName) instanceof Relation) {
265
                    $prop = $this->record->getPropObject($propName);
266
267
                    // handle the setting of ONE-TO-MANY relation values
268
                    if ($prop->getRelationType() == 'ONE-TO-MANY') {
269
                        $this->record->set($propObj->name, $this->record->getID());
270
                    }
271
272
                    // handle the setting of MANY-TO-ONE relation values
273
                    if ($prop->getRelationType() == 'MANY-TO-ONE' && isset($row[$propName])) {
274
                        $this->record->set($propObj->name, $row[$propName]);
275
                    }
276
                }
277
            }
278
        } catch (IllegalArguementException $e) {
279
            self::$logger->warn('Bad data stored in the table ['.$this->record->getTableName().'], field ['.$propObj->name.'] bad value['.$row[$propObj->name].'], exception ['.$e->getMessage().']');
280
        } catch (PHPException $e) {
281
            // it is possible that the load failed due to the table not being up-to-date
282
            if ($this->record->checkTableNeedsUpdate()) {
283
                $missingFields = $this->record->findMissingFields();
284
285
                $count = count($missingFields);
286
287
                for ($i = 0; $i < $count; ++$i) {
288
                    $this->record->addProperty($missingFields[$i]);
289
                }
290
291
                self::$logger->debug('<<load');
292
                throw new RecordFoundException('Failed to load object of ID ['.$ID.'], table ['.$this->record->getTableName().'] was out of sync with the database so had to be updated!');
293
            }
294
        }
295
296
        self::$logger->debug('<<load');
297
    }
298
299
    /**
300
     * (non-PHPdoc).
301
     *
302
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllOldVersions()
303
     */
304
    public function loadAllOldVersions($ID)
305
    {
306
        self::$logger->debug('>>loadAllOldVersions(ID=['.$ID.'])');
307
308
        if (!$this->record->getMaintainHistory()) {
309
            throw new RecordFoundException('loadAllOldVersions method called on an active record where no history is maintained!');
310
        }
311
312
        $sqlQuery = 'SELECT version_num FROM '.$this->record->getTableName().'_history WHERE ID = \''.$ID.'\' ORDER BY version_num;';
313
314
        $this->record->setLastQuery($sqlQuery);
315
316
        if (!$result = self::getConnection()->query($sqlQuery)) {
317
            self::$logger->debug('<<loadAllOldVersions');
318
            throw new RecordNotFoundException('Failed to load object versions, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
319
        }
320
321
        // now build an array of objects to be returned
322
        $objects = array();
323
        $count = 0;
324
        $RecordClass = get_class($this->record);
325
326
        while ($row = $result->fetchArray()) {
327
            try {
328
                $obj = new $RecordClass();
329
                $obj->load($ID, $row['version_num']);
330
                $objects[$count] = $obj;
331
                ++$count;
332
            } catch (ResourceNotAllowedException $e) {
333
                // the resource not allowed will be absent from the list
334
            }
335
        }
336
337
        self::$logger->warn('<<loadAllOldVersions ['.count($objects).']');
338
339
        return $objects;
340
    }
341
342
    /**
343
     * (non-PHPdoc).
344
     *
345
     * @see Alpha\Model\ActiveRecordProviderInterface::loadByAttribute()
346
     */
347
    public function loadByAttribute($attribute, $value, $ignoreClassType = false, $loadAttributes = array())
348
    {
349
        self::$logger->debug('>>loadByAttribute(attribute=['.$attribute.'], value=['.$value.'], ignoreClassType=['.$ignoreClassType.'], 
350
            loadAttributes=['.var_export($loadAttributes, true).'])');
351
352
        if (count($loadAttributes) == 0) {
353
            $attributes = $this->record->getPersistentAttributes();
354
        } else {
355
            $attributes = $loadAttributes;
356
        }
357
358
        $fields = '';
359
        foreach ($attributes as $att) {
360
            $fields .= $att.',';
361
        }
362
        $fields = mb_substr($fields, 0, -1);
363
364
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
365
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE '.$attribute.' = :attribute AND classname = :classname LIMIT 1;';
366
        } else {
367
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE '.$attribute.' = :attribute LIMIT 1;';
368
        }
369
370
        self::$logger->debug('Query=['.$sqlQuery.']');
371
372
        $this->record->setLastQuery($sqlQuery);
373
        $stmt = self::getConnection()->prepare($sqlQuery);
374
375
        if ($stmt instanceof SQLite3Stmt) {
376
            if ($this->record->getPropObject($attribute) instanceof Integer) {
377
                if (!$ignoreClassType && $this->record->isTableOverloaded()) {
378
                    $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
379
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
380
                } else {
381
                    $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
382
                }
383
            } else {
384
                if (!$ignoreClassType && $this->record->isTableOverloaded()) {
385
                    $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
386
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
387
                } else {
388
                    $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
389
                }
390
            }
391
392
            $result = $stmt->execute();
393
394
            // there should only ever be one (or none)
395
            $row = $result->fetchArray(SQLITE3_ASSOC);
396
397
            $stmt->close();
398
        } else {
399
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
400
            if (!$this->record->checkTableExists()) {
401
                $this->record->makeTable();
402
403
                throw new RecordNotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], table did not exist so had to create!');
404
            }
405
406
            return;
407
        }
408
409
        if (!isset($row['ID']) || $row['ID'] < 1) {
410
            self::$logger->debug('<<loadByAttribute');
411
            throw new RecordNotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], not found in database.');
412
        }
413
414
        $this->record->setID($row['ID']);
415
416
        // get the class attributes
417
        $reflection = new ReflectionClass(get_class($this->record));
418
        $properties = $reflection->getProperties();
419
420
        try {
421
            foreach ($properties as $propObj) {
422
                $propName = $propObj->name;
423
424
                if (isset($row[$propName])) {
425
                    // filter transient attributes
426
                    if (!in_array($propName, $this->record->getTransientAttributes())) {
427
                        $this->record->set($propName, $row[$propName]);
428
                    } elseif (!$propObj->isPrivate() && $this->record->get($propName) != '' && $this->record->getPropObject($propName) instanceof Relation) {
429
                        $prop = $this->record->getPropObject($propName);
430
431
                        // handle the setting of ONE-TO-MANY relation values
432
                        if ($prop->getRelationType() == 'ONE-TO-MANY') {
433
                            $this->record->set($propObj->name, $this->record->getID());
434
                        }
435
                    }
436
                }
437
            }
438
        } catch (IllegalArguementException $e) {
439
            self::$logger->warn('Bad data stored in the table ['.$this->record->getTableName().'], field ['.$propObj->name.'] bad value['.$row[$propObj->name].'], exception ['.$e->getMessage().']');
440
        } catch (PHPException $e) {
441
            // it is possible that the load failed due to the table not being up-to-date
442
            if ($this->record->checkTableNeedsUpdate()) {
443
                $missingFields = $this->record->findMissingFields();
444
445
                $count = count($missingFields);
446
447
                for ($i = 0; $i < $count; ++$i) {
448
                    $this->record->addProperty($missingFields[$i]);
449
                }
450
451
                self::$logger->debug('<<loadByAttribute');
452
                throw new RecordNotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], table ['.$this->record->getTableName().'] was out of sync with the database so had to be updated!');
453
            }
454
        }
455
456
        self::$logger->debug('<<loadByAttribute');
457
    }
458
459
    /**
460
     * (non-PHPdoc).
461
     *
462
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAll()
463
     */
464
    public function loadAll($start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false)
465
    {
466
        self::$logger->debug('>>loadAll(start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
467
468
        // ensure that the field name provided in the orderBy param is legit
469
        try {
470
            $this->record->get($orderBy);
471
        } catch (AlphaException $e) {
472
            throw new AlphaException('The field name ['.$orderBy.'] provided in the param orderBy does not exist on the class ['.get_class($this->record).']');
473
        }
474
475
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
476
            if ($limit == 0) {
477
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE classname = \''.get_class($this->record).'\' ORDER BY '.$orderBy.' '.$order.';';
478
            } else {
479
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE classname = \''.get_class($this->record).'\' ORDER BY '.$orderBy.' '.$order.' LIMIT '.
480
                    $limit.' OFFSET '.$start.';';
481
            }
482
        } else {
483
            if ($limit == 0) {
484
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' ORDER BY '.$orderBy.' '.$order.';';
485
            } else {
486
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' ORDER BY '.$orderBy.' '.$order.' LIMIT '.$limit.' OFFSET '.$start.';';
487
            }
488
        }
489
490
        $this->record->setLastQuery($sqlQuery);
491
492
        if (!$result = self::getConnection()->query($sqlQuery)) {
493
            self::$logger->debug('<<loadAll');
494
            throw new RecordNotFoundException('Failed to load object IDs, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
495
        }
496
497
        // now build an array of objects to be returned
498
        $objects = array();
499
        $count = 0;
500
        $RecordClass = get_class($this->record);
501
502
        while ($row = $result->fetchArray()) {
503
            try {
504
                $obj = new $RecordClass();
505
                $obj->load($row['ID']);
506
                $objects[$count] = $obj;
507
                ++$count;
508
            } catch (ResourceNotAllowedException $e) {
509
                // the resource not allowed will be absent from the list
510
            }
511
        }
512
513
        self::$logger->debug('<<loadAll ['.count($objects).']');
514
515
        return $objects;
516
    }
517
518
    /**
519
     * (non-PHPdoc).
520
     *
521
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByAttribute()
522
     */
523
    public function loadAllByAttribute($attribute, $value, $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false, $constructorArgs = array())
524
    {
525
        self::$logger->debug('>>loadAllByAttribute(attribute=['.$attribute.'], value=['.$value.'], start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.'], constructorArgs=['.print_r($constructorArgs, true).']');
526
527
        if ($start != 0 && $limit != 0) {
528
            $limit = ' LIMIT '.$limit.' OFFSET '.$start.';';
529
        } else {
530
            $limit = ';';
531
        }
532
533
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
534
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE $attribute = :attribute AND classname = :classname ORDER BY ".$orderBy.' '.$order.$limit;
535
        } else {
536
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE $attribute = :attribute ORDER BY ".$orderBy.' '.$order.$limit;
537
        }
538
539
        $this->record->setLastQuery($sqlQuery);
540
        self::$logger->debug($sqlQuery);
541
542
        $stmt = self::getConnection()->prepare($sqlQuery);
543
544
        $objects = array();
545
546
        if ($stmt instanceof SQLite3Stmt) {
547
            if ($this->record->getPropObject($attribute) instanceof Integer) {
548
                if ($this->record->isTableOverloaded()) {
549
                    $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
550
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
551
                } else {
552
                    $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
553
                }
554
            } else {
555
                if ($this->record->isTableOverloaded()) {
556
                    $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
557
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
558
                } else {
559
                    $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
560
                }
561
            }
562
563
            $result = $stmt->execute();
564
565
            // now build an array of objects to be returned
566
            $count = 0;
567
            $RecordClass = get_class($this->record);
568
569
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
570
                try {
571
                    $argsCount = count($constructorArgs);
572
573
                    if ($argsCount < 1) {
574
                        $obj = new $RecordClass();
575
                    } else {
576
                        switch ($argsCount) {
577
                            case 1:
578
                                $obj = new $RecordClass($constructorArgs[0]);
579
                                break;
580
                            case 2:
581
                                $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1]);
582
                                break;
583
                            case 3:
584
                                $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2]);
585
                                break;
586
                            case 4:
587
                                $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3]);
588
                                break;
589
                            case 5:
590
                                $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3], $constructorArgs[4]);
591
                                break;
592
                            default:
593
                                throw new IllegalArguementException('Too many elements in the $constructorArgs array passed to the loadAllByAttribute method!');
594
                        }
595
                    }
596
597
                    $obj->load($row['ID']);
598
                    $objects[$count] = $obj;
599
                    ++$count;
600
                } catch (ResourceNotAllowedException $e) {
601
                    // the resource not allowed will be absent from the list
602
                }
603
            }
604
605
            $stmt->close();
606
        } else {
607
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
608
609
            if (!$this->record->checkTableExists()) {
610
                $this->record->makeTable();
611
612
                throw new RecordFoundException('Failed to load objects by attribute ['.$attribute.'] and value ['.$value.'], table did not exist so had to create!');
613
            }
614
615
            self::$logger->debug('<<loadAllByAttribute []');
616
617
            return array();
618
        }
619
620
        self::$logger->debug('<<loadAllByAttribute ['.count($objects).']');
621
622
        return $objects;
623
    }
624
625
    /**
626
     * (non-PHPdoc).
627
     *
628
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByAttributes()
629
     */
630
    public function loadAllByAttributes($attributes = array(), $values = array(), $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false, $constructorArgs = array())
631
    {
632
        self::$logger->debug('>>loadAllByAttributes(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'], start=['.
633
            $start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.'], constructorArgs=['.print_r($constructorArgs, true).']');
634
635
        $whereClause = ' WHERE';
636
637
        $count = count($attributes);
638
639
        for ($i = 0; $i < $count; ++$i) {
640
            $whereClause .= ' '.$attributes[$i].' = :'.$attributes[$i].' AND';
641
            self::$logger->debug($whereClause);
642
        }
643
644
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
645
            $whereClause .= ' classname = :classname AND';
646
        }
647
648
        // remove the last " AND"
649
        $whereClause = mb_substr($whereClause, 0, -4);
650
651
        if ($limit != 0) {
652
            $limit = ' LIMIT '.$limit.' OFFSET '.$start.';';
653
        } else {
654
            $limit = ';';
655
        }
656
657
        $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().$whereClause.' ORDER BY '.$orderBy.' '.$order.$limit;
658
659
        $this->record->setLastQuery($sqlQuery);
660
661
        $stmt = self::getConnection()->prepare($sqlQuery);
662
663
        if ($stmt instanceof SQLite3Stmt) {
664
            // bind params where required attributes are provided
665
            if (count($attributes) > 0 && count($attributes) == count($values)) {
666
                for ($i = 0; $i < count($attributes); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
667
                    if (strcspn($values[$i], '0123456789') != strlen($values[$i])) {
668
                        $stmt->bindValue(':'.$attributes[$i], $values[$i], SQLITE3_INTEGER);
669
                    } else {
670
                        $stmt->bindValue(':'.$attributes[$i], $values[$i], SQLITE3_TEXT);
671
                    }
672
                }
673
            } else {
674
                // we'll still need to bind the "classname" for overloaded records...
675
                if ($this->record->isTableOverloaded()) {
676
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
677
                }
678
            }
679
680
            $result = $stmt->execute();
681
        } else {
682
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
683
684
            if (!$this->record->checkTableExists()) {
685
                $this->record->makeTable();
686
687
                throw new RecordFoundException('Failed to load objects by attributes ['.var_export($attributes, true).'] and values ['.
688
                    var_export($values, true).'], table did not exist so had to create!');
689
            }
690
691
            self::$logger->debug('<<loadAllByAttributes []');
692
693
            return array();
694
        }
695
696
        // now build an array of objects to be returned
697
        $objects = array();
698
        $count = 0;
699
        $RecordClass = get_class($this->record);
700
701
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
702
            try {
703
                $argsCount = count($constructorArgs);
704
705
                if ($argsCount < 1) {
706
                    $obj = new $RecordClass();
707
                } else {
708
                    switch ($argsCount) {
709
                        case 1:
710
                            $obj = new $RecordClass($constructorArgs[0]);
711
                            break;
712
                        case 2:
713
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1]);
714
                            break;
715
                        case 3:
716
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2]);
717
                            break;
718
                        case 4:
719
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3]);
720
                            break;
721
                        case 5:
722
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3], $constructorArgs[4]);
723
                            break;
724
                        default:
725
                            throw new IllegalArguementException('Too many elements in the $constructorArgs array passed to the loadAllByAttribute method!');
726
                    }
727
                }
728
729
                $obj->load($row['ID']);
730
                $objects[$count] = $obj;
731
                ++$count;
732
            } catch (ResourceNotAllowedException $e) {
733
                // the resource not allowed will be absent from the list
734
            }
735
        }
736
737
        $stmt->close();
738
739
        self::$logger->debug('<<loadAllByAttributes ['.count($objects).']');
740
741
        return $objects;
742
    }
743
744
    /**
745
     * (non-PHPdoc).
746
     *
747
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByDayUpdated()
748
     */
749
    public function loadAllByDayUpdated($date, $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false)
750
    {
751
        self::$logger->debug('>>loadAllByDayUpdated(date=['.$date.'], start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
752
753
        if ($start != 0 && $limit != 0) {
754
            $limit = ' LIMIT '.$limit.' OFFSET '.$start.';';
755
        } else {
756
            $limit = ';';
757
        }
758
759
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
760
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE updated_ts >= '".$date." 00:00:00' AND updated_ts <= '".$date." 23:59:59' AND classname = '".addslashes(get_class($this->record))."' ORDER BY ".$orderBy.' '.$order.$limit;
761
        } else {
762
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE updated_ts >= '".$date." 00:00:00' AND updated_ts <= '".$date." 23:59:59' ORDER BY ".$orderBy.' '.$order.$limit;
763
        }
764
765
        $this->record->setLastQuery($sqlQuery);
766
767
        if (!$result = self::getConnection()->query($sqlQuery)) {
768
            self::$logger->debug('<<loadAllByDayUpdated');
769
            throw new RecordNotFoundException('Failed to load object IDs, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
770
        }
771
772
        // now build an array of objects to be returned
773
        $objects = array();
774
        $count = 0;
775
        $RecordClass = get_class($this->record);
776
777
        while ($row = $result->fetchArray()) {
778
            $obj = new $RecordClass();
779
            $obj->load($row['ID']);
780
            $objects[$count] = $obj;
781
            ++$count;
782
        }
783
784
        self::$logger->debug('<<loadAllByDayUpdated ['.count($objects).']');
785
786
        return $objects;
787
    }
788
789
    /**
790
     * (non-PHPdoc).
791
     *
792
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllFieldValuesByAttribute()
793
     */
794
    public function loadAllFieldValuesByAttribute($attribute, $value, $returnAttribute, $order = 'ASC', $ignoreClassType = false)
795
    {
796
        self::$logger->debug('>>loadAllFieldValuesByAttribute(attribute=['.$attribute.'], value=['.$value.'], returnAttribute=['.$returnAttribute.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
797
798
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
799
            $sqlQuery = 'SELECT '.$returnAttribute.' FROM '.$this->record->getTableName()." WHERE $attribute = '$value' AND classname = '".addslashes(get_class($this->record))."' ORDER BY ID ".$order.';';
800
        } else {
801
            $sqlQuery = 'SELECT '.$returnAttribute.' FROM '.$this->record->getTableName()." WHERE $attribute = '$value' ORDER BY ID ".$order.';';
802
        }
803
804
        $this->record->setLastQuery($sqlQuery);
805
806
        self::$logger->debug('lastQuery ['.$sqlQuery.']');
807
808
        if (!$result = self::getConnection()->query($sqlQuery)) {
809
            self::$logger->debug('<<loadAllFieldValuesByAttribute');
810
            throw new RecordNotFoundException('Failed to load field ['.$returnAttribute.'] values, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
811
        }
812
813
        // now build an array of attribute values to be returned
814
        $values = array();
815
        $count = 0;
816
817
        while ($row = $result->fetchArray()) {
818
            $values[$count] = $row[$returnAttribute];
819
            ++$count;
820
        }
821
822
        self::$logger->debug('<<loadAllFieldValuesByAttribute ['.count($values).']');
823
824
        return $values;
825
    }
826
827
    /**
828
     * (non-PHPdoc).
829
     *
830
     * @see Alpha\Model\ActiveRecordProviderInterface::save()
831
     */
832
    public function save()
833
    {
834
        self::$logger->debug('>>save()');
835
836
        // get the class attributes
837
        $reflection = new ReflectionClass(get_class($this->record));
838
        $properties = $reflection->getProperties();
839
840
        // check to see if it is a transient object that needs to be inserted
841
        if ($this->record->isTransient()) {
842
            $savedFields = array();
843
            $sqlQuery = 'INSERT INTO '.$this->record->getTableName().' (';
844
845
            foreach ($properties as $propObj) {
846
                $propName = $propObj->name;
847
                if (!in_array($propName, $this->record->getTransientAttributes())) {
848
                    // Skip the ID, database auto number takes care of this.
849
                    if ($propName != 'ID' && $propName != 'version_num') {
850
                        $sqlQuery .= "$propName,";
851
                        $savedFields[] = $propName;
852
                    }
853
854
                    if ($propName == 'version_num') {
855
                        $sqlQuery .= 'version_num,';
856
                        $savedFields[] = 'version_num';
857
                    }
858
                }
859
            }
860
            if ($this->record->isTableOverloaded()) {
861
                $sqlQuery .= 'classname,';
862
            }
863
864
            $sqlQuery = rtrim($sqlQuery, ',');
865
866
            $sqlQuery .= ') VALUES (';
867
868
            foreach ($savedFields as $savedField) {
869
                $sqlQuery .= ':'.$savedField.',';
870
            }
871
872
            if ($this->record->isTableOverloaded()) {
873
                $sqlQuery .= ':classname,';
874
            }
875
876
            $sqlQuery = rtrim($sqlQuery, ',').')';
877
878
            $this->record->setLastQuery($sqlQuery);
879
            self::$logger->debug('Query ['.$sqlQuery.']');
880
881
            $stmt = self::getConnection()->prepare($sqlQuery);
882
883
            if ($stmt instanceof SQLite3Stmt) {
884
                foreach ($savedFields as $savedField) {
885
                    if ($this->record->get($savedField) instanceof Integer) {
886
                        $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_INTEGER);
887
                    } else {
888
                        $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_TEXT);
889
                    }
890
                }
891
892
                if ($this->record->isTableOverloaded()) {
893
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
894
                }
895
896
                $stmt->bindValue(':version_num', 1, SQLITE3_INTEGER); // on an initial save, this will always be 1
897
                $this->record->set('version_num', 1);
898
899
                try {
900
                    $stmt->execute();
901
                } catch (Exception $e) {
902
                    if (self::getConnection()->lastErrorCode() == 19) {
903
                        throw new ValidationException('Unique key violation while trying to save object, exception ['.$e->getMessage().'], SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
904
                    } else {
905
                        throw new FailedSaveException('Failed to save object, exception ['.$e->getMessage().'], DB error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
906
                    }
907
                }
908
            } else {
909
                throw new FailedSaveException('Failed to save object, exception ['.$e->getMessage().'], DB error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
910
            }
911
        } else {
912
            // assume that it is a persistent object that needs to be updated
913
            $savedFields = array();
914
            $sqlQuery = 'UPDATE '.$this->record->getTableName().' SET ';
915
916
            foreach ($properties as $propObj) {
917
                $propName = $propObj->name;
918
                if (!in_array($propName, $this->record->getTransientAttributes())) {
919
                    // Skip the ID, database auto number takes care of this.
920
                    if ($propName != 'ID' && $propName != 'version_num') {
921
                        $sqlQuery .= "$propName = :$propName,";
922
                        $savedFields[] = $propName;
923
                    }
924
925
                    if ($propName == 'version_num') {
926
                        $sqlQuery .= 'version_num = :version_num,';
927
                        $savedFields[] = 'version_num';
928
                    }
929
                }
930
            }
931
932
            if ($this->record->isTableOverloaded()) {
933
                $sqlQuery .= 'classname = :classname,';
934
            }
935
936
            $sqlQuery = rtrim($sqlQuery, ',');
937
938
            $sqlQuery .= ' WHERE ID=:ID;';
939
940
            $this->record->setLastQuery($sqlQuery);
941
            $stmt = self::getConnection()->prepare($sqlQuery);
942
943
            if ($stmt instanceof SQLite3Stmt) {
944
                foreach ($savedFields as $savedField) {
945
                    if ($this->record->get($savedField) instanceof Integer) {
946
                        $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_INTEGER);
947
                    } else {
948
                        $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_TEXT);
949
                    }
950
                }
951
952
                if ($this->record->isTableOverloaded()) {
953
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
954
                }
955
956
                $stmt->bindValue(':ID', $this->record->getID(), SQLITE3_INTEGER);
957
958
                $temp = $this->record->getVersionNumber()->getValue();
959
                $this->record->set('version_num', $temp+1);
960
                $stmt->bindValue(':version_num', $temp+1, SQLITE3_INTEGER);
961
962
                $stmt->execute();
963
            } else {
964
                throw new FailedSaveException('Failed to save object, error is ['.$stmt->error.'], query ['.$this->record->getLastQuery().']');
965
            }
966
        }
967
968
        if ($stmt != null && $stmt != false) {
969
            // populate the updated ID in case we just done an insert
970
            if ($this->record->isTransient()) {
971
                $this->record->setID(self::getConnection()->lastInsertRowID());
972
            }
973
974
            $this->record->saveRelations();
975
976
            $stmt->close();
977
        } else {
978
            // there has been an error, so decrement the version number back
979
            $temp = $this->record->getVersionNumber()->getValue();
980
            $this->record->set('version_num', $temp-1);
981
982
            throw new FailedSaveException('Failed to save object, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
983
        }
984
985
        if ($this->record->getMaintainHistory()) {
986
            $this->record->saveHistory();
987
        }
988
    }
989
990
    /**
991
     * (non-PHPdoc).
992
     *
993
     * @see Alpha\Model\ActiveRecordProviderInterface::saveAttribute()
994
     */
995
    public function saveAttribute($attribute, $value)
996
    {
997
        self::$logger->debug('>>saveAttribute(attribute=['.$attribute.'], value=['.$value.'])');
998
999
        $config = ConfigProvider::getInstance();
1000
        $sessionProvider = $config->get('session.provider.name');
1001
        $session = ServiceFactory::getInstance($sessionProvider, 'Alpha\Util\Http\Session\SessionProviderInterface');
1002
1003
        if ($this->record->getVersion() != $this->record->getVersionNumber()->getValue()) {
1004
            throw new LockingException('Could not save the object as it has been updated by another user.  Please try saving again.');
1005
        }
1006
1007
        // set the "updated by" fields, we can only set the user id if someone is logged in
1008
        if ($session->get('currentUser') != null) {
1009
            $this->record->set('updated_by', $session->get('currentUser')->getID());
1010
        }
1011
1012
        $this->record->set('updated_ts', new Timestamp(date('Y-m-d H:i:s')));
1013
1014
        // assume that it is a persistent object that needs to be updated
1015
        $sqlQuery = 'UPDATE '.$this->record->getTableName().' SET '.$attribute.'=:attribute, version_num=:version, updated_by=:updated_by, updated_ts=:updated_ts WHERE ID=:ID;';
1016
1017
        $this->record->setLastQuery($sqlQuery);
1018
        $stmt = self::getConnection()->prepare($sqlQuery);
1019
1020
        $newVersionNumber = $this->record->getVersionNumber()->getValue()+1;
1021
1022
        if ($stmt instanceof SQLite3Stmt) {
1023
            if ($this->record->getPropObject($attribute) instanceof Integer) {
1024
                $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
1025
            } else {
1026
                $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
1027
            }
1028
1029
            $updatedBy = $this->record->get('updated_by');
1030
            $updatedTS = $this->record->get('updated_ts');
1031
1032
            $stmt->bindValue(':version', $newVersionNumber, SQLITE3_INTEGER);
1033
            $stmt->bindValue(':updated_by', $updatedBy, SQLITE3_INTEGER);
1034
            $stmt->bindValue(':updated_ts', $updatedTS, SQLITE3_TEXT);
1035
            $stmt->bindValue(':ID', $this->record->getID(), SQLITE3_INTEGER);
1036
1037
            $stmt->execute();
1038
        } else {
1039
            throw new FailedSaveException('Failed to save attribute, error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
1040
        }
1041
1042
        $stmt->close();
1043
1044
        $this->record->set($attribute, $value);
1045
        $this->record->set('version_num', $newVersionNumber);
1046
1047
        if ($this->record->getMaintainHistory()) {
1048
            $this->record->saveHistory();
1049
        }
1050
1051
        self::$logger->debug('<<saveAttribute');
1052
    }
1053
1054
    /**
1055
     * (non-PHPdoc).
1056
     *
1057
     * @see Alpha\Model\ActiveRecordProviderInterface::saveHistory()
1058
     */
1059
    public function saveHistory()
1060
    {
1061
        self::$logger->debug('>>saveHistory()');
1062
1063
        // get the class attributes
1064
        $reflection = new ReflectionClass(get_class($this->record));
1065
        $properties = $reflection->getProperties();
1066
1067
        $savedFields = array();
1068
        $attributeNames = array();
1069
        $attributeValues = array();
1070
1071
        $sqlQuery = 'INSERT INTO '.$this->record->getTableName().'_history (';
1072
1073
        foreach ($properties as $propObj) {
1074
            $propName = $propObj->name;
1075
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1076
                $sqlQuery .= "$propName,";
1077
                $attributeNames[] = $propName;
1078
                $attributeValues[] = $this->record->get($propName);
1079
                $savedFields[] = $propName;
1080
            }
1081
        }
1082
1083
        if ($this->record->isTableOverloaded()) {
1084
            $sqlQuery .= 'classname,';
1085
        }
1086
1087
        $sqlQuery = rtrim($sqlQuery, ',');
1088
1089
        $sqlQuery .= ') VALUES (';
1090
1091
        foreach ($savedFields as $savedField) {
1092
            $sqlQuery .= ':'.$savedField.',';
1093
        }
1094
1095
        if ($this->record->isTableOverloaded()) {
1096
            $sqlQuery .= ':classname,';
1097
        }
1098
1099
        $sqlQuery = rtrim($sqlQuery, ',').')';
1100
1101
        $this->record->setLastQuery($sqlQuery);
1102
        self::$logger->debug('Query ['.$sqlQuery.']');
1103
1104
        $stmt = self::getConnection()->prepare($sqlQuery);
1105
1106
        if ($stmt instanceof SQLite3Stmt) {
1107
            foreach ($savedFields as $savedField) {
1108
                if ($this->record->get($savedField) instanceof Integer) {
1109
                    $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_INTEGER);
1110
                } else {
1111
                    $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_TEXT);
1112
                }
1113
            }
1114
1115
            if ($this->record->isTableOverloaded()) {
1116
                $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
1117
            }
1118
1119
            $stmt->execute();
1120
        } else {
1121
            throw new FailedSaveException('Failed to save object history, error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
1122
        }
1123
    }
1124
1125
    /**
1126
     * (non-PHPdoc).
1127
     *
1128
     * @see Alpha\Model\ActiveRecordProviderInterface::delete()
1129
     */
1130
    public function delete()
1131
    {
1132
        self::$logger->debug('>>delete()');
1133
1134
        $sqlQuery = 'DELETE FROM '.$this->record->getTableName().' WHERE ID = :ID;';
1135
1136
        $this->record->setLastQuery($sqlQuery);
1137
1138
        $stmt = self::getConnection()->prepare($sqlQuery);
1139
1140
        if ($stmt instanceof SQLite3Stmt) {
1141
            $stmt->bindValue(':ID', $this->record->getID(), SQLITE3_INTEGER);
1142
            $stmt->execute();
1143
            self::$logger->debug('Deleted the object ['.$this->record->getID().'] of class ['.get_class($this->record).']');
1144
        } else {
1145
            throw new FailedDeleteException('Failed to delete object ['.$this->record->getID().'], error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
1146
        }
1147
1148
        $stmt->close();
1149
1150
        self::$logger->debug('<<delete');
1151
    }
1152
1153
    /**
1154
     * (non-PHPdoc).
1155
     *
1156
     * @see Alpha\Model\ActiveRecordProviderInterface::getVersion()
1157
     */
1158
    public function getVersion()
1159
    {
1160
        self::$logger->debug('>>getVersion()');
1161
1162
        $sqlQuery = 'SELECT version_num FROM '.$this->record->getTableName().' WHERE ID = :ID;';
1163
        $this->record->setLastQuery($sqlQuery);
1164
1165
        $stmt = self::getConnection()->prepare($sqlQuery);
1166
1167
        if ($stmt instanceof SQLite3Stmt) {
1168
            $stmt->bindValue(':ID', $this->record->getID(), SQLITE3_INTEGER);
1169
1170
            $result = $stmt->execute();
1171
1172
            // there should only ever be one (or none)
1173
            $row = $result->fetchArray(SQLITE3_ASSOC);
1174
1175
            $stmt->close();
1176
        } else {
1177
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
1178
            if (!$this->record->checkTableExists()) {
1179
                $this->record->makeTable();
1180
1181
                throw new RecordNotFoundException('Failed to get the version number, table did not exist so had to create!');
1182
            }
1183
1184
            return;
1185
        }
1186
1187
        if (!isset($row['version_num']) || $row['version_num'] < 1) {
1188
            self::$logger->debug('<<getVersion [0]');
1189
1190
            return 0;
1191
        } else {
1192
            $version_num = $row['version_num'];
1193
1194
            self::$logger->debug('<<getVersion ['.$version_num.']');
1195
1196
            return $version_num;
1197
        }
1198
    }
1199
1200
    /**
1201
     * (non-PHPdoc).
1202
     *
1203
     * @see Alpha\Model\ActiveRecordProviderInterface::makeTable()
1204
     */
1205
    public function makeTable($checkIndexes = true)
1206
    {
1207
        self::$logger->debug('>>makeTable()');
1208
1209
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().' (ID INTEGER PRIMARY KEY,';
1210
1211
        // get the class attributes
1212
        $reflection = new ReflectionClass(get_class($this->record));
1213
        $properties = $reflection->getProperties();
1214
1215
        $foreignKeys = array();
1216
1217
        foreach ($properties as $propObj) {
1218
            $propName = $propObj->name;
1219
1220
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1221
                $prop = $this->record->getPropObject($propName);
1222
1223
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1224
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().') NOT NULL,';
1225
                } elseif ($prop instanceof Integer) {
1226
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().'),';
1227
                } elseif ($prop instanceof Double) {
1228
                    $sqlQuery .= "$propName REAL(".$prop->getSize(true).'),';
1229
                } elseif ($prop instanceof SmallText) {
1230
                    $sqlQuery .= "$propName TEXT(".$prop->getSize().'),';
1231
                } elseif ($prop instanceof Text) {
1232
                    $sqlQuery .= "$propName TEXT,";
1233
                } elseif ($prop instanceof LargeText) {
1234
                    $sqlQuery .= "$propName TEXT,";
1235
                } elseif ($prop instanceof HugeText) {
1236
                    $sqlQuery .= "$propName TEXT,";
1237
                } elseif ($prop instanceof Boolean) {
1238
                    $sqlQuery .= "$propName INTEGER(1) DEFAULT '0',";
1239
                } elseif ($prop instanceof Date) {
1240
                    $sqlQuery .= "$propName TEXT,";
1241
                } elseif ($prop instanceof Timestamp) {
1242
                    $sqlQuery .= "$propName TEXT,";
1243
                } elseif ($prop instanceof Enum) {
1244
                    $sqlQuery .= "$propName TEXT,";
1245
                } elseif ($prop instanceof DEnum) {
1246
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1247
                    $denum->saveIfNew();
1248
                    $sqlQuery .= "$propName INTEGER(11),";
1249
                } elseif ($prop instanceof Relation) {
1250
                    $sqlQuery .= "$propName INTEGER(11),";
1251
1252
                    $rel = $this->record->getPropObject($propName);
1253
1254
                    $relatedField = $rel->getRelatedClassField();
1255
                    $relatedClass = $rel->getRelatedClass();
1256
                    $relatedRecord = new $relatedClass();
1257
                    $tableName = $relatedRecord->getTableName();
1258
                    $foreignKeys[$propName] = array($tableName, $relatedField);
1259
                } else {
1260
                    $sqlQuery .= '';
1261
                }
1262
            }
1263
        }
1264
1265
        if ($this->record->isTableOverloaded()) {
1266
            $sqlQuery .= 'classname TEXT(100)';
1267
        } else {
1268
            $sqlQuery = mb_substr($sqlQuery, 0, -1);
1269
        }
1270
1271
        if (count($foreignKeys) > 0) {
1272
            foreach ($foreignKeys as $field => $related) {
1273
                $sqlQuery .= ', FOREIGN KEY ('.$field.') REFERENCES '.$related[0].'('.$related[1].')';
1274
            }
1275
        }
1276
1277
        if (count($this->foreignKeys) > 0) {
1278
            foreach ($this->foreignKeys as $field => $related) {
1279
                $sqlQuery .= ', FOREIGN KEY ('.$field.') REFERENCES '.$related[0].'('.$related[1].')';
1280
            }
1281
        }
1282
1283
        $sqlQuery .= ');';
1284
1285
        $this->record->setLastQuery($sqlQuery);
1286
1287
        if (!self::getConnection()->exec($sqlQuery)) {
1288
            self::$logger->debug('<<makeTable');
1289
            throw new AlphaException('Failed to create the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getLastDatabaseError().']');
1290
        }
1291
1292
        // check the table indexes if any additional ones required
1293
        if ($checkIndexes) {
1294
            $this->checkIndexes();
1295
        }
1296
1297
        if ($this->record->getMaintainHistory()) {
1298
            $this->record->makeHistoryTable();
1299
        }
1300
1301
        self::$logger->debug('<<makeTable');
1302
    }
1303
1304
    /**
1305
     * (non-PHPdoc).
1306
     *
1307
     * @see Alpha\Model\ActiveRecordProviderInterface::makeHistoryTable()
1308
     */
1309
    public function makeHistoryTable()
1310
    {
1311
        self::$logger->debug('>>makeHistoryTable()');
1312
1313
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().'_history (ID INTEGER NOT NULL,';
1314
1315
        // get the class attributes
1316
        $reflection = new ReflectionClass(get_class($this->record));
1317
        $properties = $reflection->getProperties();
1318
1319
        foreach ($properties as $propObj) {
1320
            $propName = $propObj->name;
1321
1322
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1323
                $prop = $this->record->getPropObject($propName);
1324
1325
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1326
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().') NOT NULL,';
1327
                } elseif ($prop instanceof Integer) {
1328
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().'),';
1329
                } elseif ($prop instanceof Double) {
1330
                    $sqlQuery .= "$propName REAL(".$prop->getSize(true).'),';
1331
                } elseif ($prop instanceof SmallText) {
1332
                    $sqlQuery .= "$propName TEXT(".$prop->getSize().'),';
1333
                } elseif ($prop instanceof Text) {
1334
                    $sqlQuery .= "$propName TEXT,";
1335
                } elseif ($prop instanceof LargeText) {
1336
                    $sqlQuery .= "$propName TEXT,";
1337
                } elseif ($prop instanceof HugeText) {
1338
                    $sqlQuery .= "$propName TEXT,";
1339
                } elseif ($prop instanceof Boolean) {
1340
                    $sqlQuery .= "$propName INTEGER(1) DEFAULT '0',";
1341
                } elseif ($prop instanceof Date) {
1342
                    $sqlQuery .= "$propName TEXT,";
1343
                } elseif ($prop instanceof Timestamp) {
1344
                    $sqlQuery .= "$propName TEXT,";
1345
                } elseif ($prop instanceof Enum) {
1346
                    $sqlQuery .= "$propName TEXT,";
1347
                } elseif ($prop instanceof DEnum) {
1348
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1349
                    $denum->saveIfNew();
1350
                    $sqlQuery .= "$propName INTEGER(11),";
1351
                } elseif ($prop instanceof Relation) {
1352
                    $sqlQuery .= "$propName INTEGER(11),";
1353
                } else {
1354
                    $sqlQuery .= '';
1355
                }
1356
            }
1357
        }
1358
1359
        if ($this->record->isTableOverloaded()) {
1360
            $sqlQuery .= 'classname TEXT(100),';
1361
        }
1362
1363
        $sqlQuery .= 'PRIMARY KEY (ID, version_num));';
1364
1365
        $this->record->setLastQuery($sqlQuery);
1366
1367
        if (!$result = self::getConnection()->query($sqlQuery)) {
1368
            self::$logger->debug('<<makeHistoryTable');
1369
            throw new AlphaException('Failed to create the table ['.$this->record->getTableName().'_history] for the class ['.get_class($this->record).'], database error is ['.self::getLastDatabaseError().']');
1370
        }
1371
1372
        self::$logger->debug('<<makeHistoryTable');
1373
    }
1374
1375
    /**
1376
     * (non-PHPdoc).
1377
     *
1378
     * @see Alpha\Model\ActiveRecordProviderInterface::rebuildTable()
1379
     */
1380
    public function rebuildTable()
1381
    {
1382
        self::$logger->debug('>>rebuildTable()');
1383
1384
        // the use of "IF EXISTS" here requires SQLite 3.3.0 or above.
1385
        $sqlQuery = 'DROP TABLE IF EXISTS '.$this->record->getTableName().';';
1386
1387
        $this->record->setLastQuery($sqlQuery);
1388
1389
        if (!$result = self::getConnection()->query($sqlQuery)) {
1390
            self::$logger->debug('<<rebuildTable');
1391
            throw new AlphaException('Failed to drop the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getLastDatabaseError().']');
1392
        }
1393
1394
        $this->record->makeTable();
1395
1396
        self::$logger->debug('<<rebuildTable');
1397
    }
1398
1399
    /**
1400
     * (non-PHPdoc).
1401
     *
1402
     * @see Alpha\Model\ActiveRecordProviderInterface::dropTable()
1403
     */
1404
    public function dropTable($tableName = null)
1405
    {
1406
        self::$logger->debug('>>dropTable()');
1407
1408
        if ($tableName === null) {
1409
            $tableName = $this->record->getTableName();
1410
        }
1411
1412
        // the use of "IF EXISTS" here requires SQLite 3.3.0 or above.
1413
        $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.';';
1414
1415
        $this->record->setLastQuery($sqlQuery);
1416
1417
        if (!$result = self::getConnection()->query($sqlQuery)) {
1418
            self::$logger->debug('<<dropTable');
1419
            throw new AlphaException('Failed to drop the table ['.$tableName.'] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1420
        }
1421
1422
        if ($this->record->getMaintainHistory()) {
1423
            $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.'_history;';
1424
1425
            $this->record->setLastQuery($sqlQuery);
1426
1427
            if (!$result = self::getConnection()->query($sqlQuery)) {
1428
                self::$logger->debug('<<dropTable');
1429
                throw new AlphaException('Failed to drop the table ['.$tableName.'_history] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1430
            }
1431
        }
1432
1433
        self::$logger->debug('<<dropTable');
1434
    }
1435
1436
    /**
1437
     * (non-PHPdoc).
1438
     *
1439
     * @see Alpha\Model\ActiveRecordProviderInterface::addProperty()
1440
     */
1441
    public function addProperty($propName)
1442
    {
1443
        self::$logger->debug('>>addProperty(propName=['.$propName.'])');
1444
1445
        $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD ';
1446
1447
        if ($this->isTableOverloaded() && $propName == 'classname') {
1448
            $sqlQuery .= 'classname TEXT(100)';
1449
        } else {
1450
            if (!in_array($propName, $this->record->getDefaultAttributes()) && !in_array($propName, $this->record->getTransientAttributes())) {
1451
                $prop = $this->record->getPropObject($propName);
1452
1453
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1454
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().') NOT NULL';
1455
                } elseif ($prop instanceof Integer) {
1456
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().')';
1457
                } elseif ($prop instanceof Double) {
1458
                    $sqlQuery .= "$propName REAL(".$prop->getSize(true).')';
1459
                } elseif ($prop instanceof SmallText) {
1460
                    $sqlQuery .= "$propName TEXT(".$prop->getSize().')';
1461
                } elseif ($prop instanceof Text) {
1462
                    $sqlQuery .= "$propName TEXT";
1463
                } elseif ($prop instanceof Boolean) {
1464
                    $sqlQuery .= "$propName INTEGER(1) DEFAULT '0'";
1465
                } elseif ($prop instanceof Date) {
1466
                    $sqlQuery .= "$propName TEXT";
1467
                } elseif ($prop instanceof Timestamp) {
1468
                    $sqlQuery .= "$propName TEXT";
1469
                } elseif ($prop instanceof Enum) {
1470
                    $sqlQuery .= "$propName TEXT";
1471
                } elseif ($prop instanceof DEnum) {
1472
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1473
                    $denum->saveIfNew();
1474
                    $sqlQuery .= "$propName INTEGER(11)";
1475
                } elseif ($prop instanceof Relation) {
1476
                    $sqlQuery .= "$propName INTEGER(11)";
1477
                } else {
1478
                    $sqlQuery .= '';
1479
                }
1480
            }
1481
        }
1482
1483
        $this->record->setLastQuery($sqlQuery);
1484
1485
        if (!$result = self::getConnection()->query($sqlQuery)) {
1486
            self::$logger->debug('<<addProperty');
1487
            throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1488
        } else {
1489
            self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'] table for the class ['.get_class($this->record).']');
1490
        }
1491
1492
        if ($this->record->getMaintainHistory()) {
1493
            $sqlQuery = str_replace($this->record->getTableName(), $this->record->getTableName().'_history', $sqlQuery);
1494
1495
            if (!$result = self::getConnection()->query($sqlQuery)) {
1496
                self::$logger->debug('<<addProperty');
1497
                throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'_history], query is ['.$this->record->getLastQuery().']');
1498
            } else {
1499
                self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'_history] table for the class ['.get_class($this->record).']');
1500
            }
1501
        }
1502
1503
        self::$logger->debug('<<addProperty');
1504
    }
1505
1506
    /**
1507
     * (non-PHPdoc).
1508
     *
1509
     * @see Alpha\Model\ActiveRecordProviderInterface::getMAX()
1510
     */
1511
    public function getMAX()
1512
    {
1513
        self::$logger->debug('>>getMAX()');
1514
1515
        $sqlQuery = 'SELECT MAX(ID) AS max_ID FROM '.$this->record->getTableName();
1516
1517
        $this->record->setLastQuery($sqlQuery);
1518
1519
        try {
1520
            $result = $this->record->query($sqlQuery);
1521
1522
            $row = $result[0];
1523
1524
            if (isset($row['max_ID'])) {
1525
                self::$logger->debug('<<getMAX ['.$row['max_ID'].']');
1526
1527
                return $row['max_ID'];
1528
            } else {
1529
                throw new AlphaException('Failed to get the MAX ID for the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1530
            }
1531
        } catch (Exception $e) {
1532
            self::$logger->debug('<<getMAX');
1533
            throw new AlphaException($e->getMessage());
1534
        }
1535
    }
1536
1537
    /**
1538
     * (non-PHPdoc).
1539
     *
1540
     * @see Alpha\Model\ActiveRecordProviderInterface::getCount()
1541
     */
1542
    public function getCount($attributes = array(), $values = array())
1543
    {
1544
        self::$logger->debug('>>getCount(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'])');
1545
1546
        if ($this->record->isTableOverloaded()) {
1547
            $whereClause = ' WHERE classname = \''.get_class($this->record).'\' AND';
1548
        } else {
1549
            $whereClause = ' WHERE';
1550
        }
1551
1552
        $count = count($attributes);
1553
1554
        for ($i = 0; $i < $count; ++$i) {
1555
            $whereClause .= ' '.$attributes[$i].' = \''.$values[$i].'\' AND';
1556
            self::$logger->debug($whereClause);
1557
        }
1558
        // remove the last " AND"
1559
        $whereClause = mb_substr($whereClause, 0, -4);
1560
1561
        if ($whereClause != ' WHERE') {
1562
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName().$whereClause;
1563
        } else {
1564
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName();
1565
        }
1566
1567
        $this->record->setLastQuery($sqlQuery);
1568
1569
        if (!$result = self::getConnection()->query($sqlQuery)) {
1570
            self::$logger->debug('<<getCount');
1571
            throw new AlphaException('Failed to get the count for the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1572
        } else {
1573
            $row = $result->fetchArray(SQLITE3_ASSOC);
1574
1575
            self::$logger->debug('<<getCount ['.$row['class_count'].']');
1576
1577
            return $row['class_count'];
1578
        }
1579
    }
1580
1581
    /**
1582
     * (non-PHPdoc).
1583
     *
1584
     * @see Alpha\Model\ActiveRecordProviderInterface::getHistoryCount()
1585
     */
1586
    public function getHistoryCount()
1587
    {
1588
        self::$logger->debug('>>getHistoryCount()');
1589
1590
        if (!$this->record->getMaintainHistory()) {
1591
            throw new AlphaException('getHistoryCount method called on a DAO where no history is maintained!');
1592
        }
1593
1594
        $sqlQuery = 'SELECT COUNT(ID) AS object_count FROM '.$this->record->getTableName().'_history WHERE ID='.$this->record->getID();
1595
1596
        $this->record->setLastQuery($sqlQuery);
1597
        self::$logger->debug('query ['.$sqlQuery.']');
1598
1599
        if (!$result = self::getConnection()->query($sqlQuery)) {
1600
            self::$logger->debug('<<getHistoryCount');
1601
            throw new AlphaException('Failed to get the history count for the business object ['.$this->record->getID().'] from the table ['.$this->record->getTableName().'_history], query is ['.$this->record->getLastQuery().']');
1602
        } else {
1603
            $row = $result->fetchArray(SQLITE3_ASSOC);
1604
1605
            self::$logger->debug('<<getHistoryCount ['.$row['object_count'].']');
1606
1607
            return $row['object_count'];
1608
        }
1609
    }
1610
1611
    /**
1612
     * Given that Enum values are not saved in the database for SQLite, an implementation is not required here.
1613
     *
1614
     * (non-PHPdoc)
1615
     *
1616
     * @see Alpha\Model\ActiveRecordProviderInterface::setEnumOptions()
1617
     *
1618
     * @throws \Alpha\Exception\NotImplementedException
1619
     */
1620
    public function setEnumOptions()
1621
    {
1622
        throw new NotImplementedException('ActiveRecordProviderInterface::setEnumOptions() not implemented by the SQLite3 provider');
1623
    }
1624
1625
    /**
1626
     * (non-PHPdoc).
1627
     *
1628
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableExists()
1629
     */
1630
    public function checkTableExists($checkHistoryTable = false)
1631
    {
1632
        self::$logger->debug('>>checkTableExists(checkHistoryTable=['.$checkHistoryTable.'])');
1633
1634
        $tableExists = false;
1635
1636
        $sqlQuery = 'SELECT name FROM sqlite_master WHERE type = "table";';
1637
        $this->record->setLastQuery($sqlQuery);
1638
1639
        $result = self::getConnection()->query($sqlQuery);
1640
1641
        $tableName = ($checkHistoryTable ? $this->record->getTableName().'_history' : $this->record->getTableName());
1642
1643
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1644
            if (strtolower($row['name']) == mb_strtolower($tableName)) {
1645
                $tableExists = true;
1646
            }
1647
        }
1648
1649
        if ($result) {
1650
            self::$logger->debug('<<checkTableExists ['.$tableExists.']');
1651
1652
            return $tableExists;
1653
        } else {
1654
            self::$logger->debug('<<checkTableExists');
1655
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1656
        }
1657
    }
1658
1659
    /**
1660
     * (non-PHPdoc).
1661
     *
1662
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordTableExists()
1663
     */
1664
    public static function checkRecordTableExists($RecordClassName, $checkHistoryTable = false)
1665
    {
1666
        if (self::$logger == null) {
1667
            self::$logger = new Logger('ActiveRecordProviderSQLite');
1668
        }
1669
        self::$logger->debug('>>checkRecordTableExists(RecordClassName=['.$RecordClassName.'], checkHistoryTable=['.$checkHistoryTable.'])');
1670
1671
        if (!class_exists($RecordClassName)) {
1672
            throw new IllegalArguementException('The classname provided ['.$checkHistoryTable.'] is not defined!');
1673
        }
1674
1675
        $tableName = $RecordClassName::TABLE_NAME;
1676
1677
        if (empty($tableName)) {
1678
            $tableName = mb_substr($RecordClassName, 0, mb_strpos($RecordClassName, '_'));
1679
        }
1680
1681
        if ($checkHistoryTable) {
1682
            $tableName .= '_history';
1683
        }
1684
1685
        $tableExists = false;
1686
1687
        $sqlQuery = 'SELECT name FROM sqlite_master WHERE type = "table";';
1688
1689
        $result = self::getConnection()->query($sqlQuery);
1690
1691
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1692
            if ($row['name'] == $tableName) {
1693
                $tableExists = true;
1694
            }
1695
        }
1696
1697
        if ($result) {
1698
            self::$logger->debug('<<checkRecordTableExists ['.($tableExists ? 'true' : 'false').']');
1699
1700
            return $tableExists;
1701
        } else {
1702
            self::$logger->debug('<<checkRecordTableExists');
1703
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1704
        }
1705
    }
1706
1707
    /**
1708
     * (non-PHPdoc).
1709
     *
1710
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableNeedsUpdate()
1711
     */
1712
    public function checkTableNeedsUpdate()
1713
    {
1714
        self::$logger->debug('>>checkTableNeedsUpdate()');
1715
1716
        if (!$this->record->checkTableExists()) {
1717
            return false;
1718
        }
1719
1720
        $updateRequired = false;
1721
1722
        $matchCount = 0;
1723
1724
        $query = 'PRAGMA table_info('.$this->record->getTableName().')';
1725
        $result = self::getConnection()->query($query);
1726
        $this->record->setLastQuery($query);
1727
1728
        // get the class attributes
1729
        $reflection = new ReflectionClass(get_class($this->record));
1730
        $properties = $reflection->getProperties();
1731
1732
        foreach ($properties as $propObj) {
1733
            $propName = $propObj->name;
1734
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1735
                $foundMatch = false;
1736
1737
                while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1738
                    if ($propName == $row['name']) {
1739
                        $foundMatch = true;
1740
                        break;
1741
                    }
1742
                }
1743
1744
                if (!$foundMatch) {
1745
                    --$matchCount;
1746
                }
1747
1748
                $result->reset();
1749
            }
1750
        }
1751
1752
        // check for the "classname" field in overloaded tables
1753
        if ($this->record->isTableOverloaded()) {
1754
            $foundMatch = false;
1755
1756
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1757
                if ('classname' == $row['name']) {
1758
                    $foundMatch = true;
1759
                    break;
1760
                }
1761
            }
1762
            if (!$foundMatch) {
1763
                --$matchCount;
1764
            }
1765
        }
1766
1767
        if ($matchCount != 0) {
1768
            $updateRequired = true;
1769
        }
1770
1771
        if (!$result) {
1772
            self::$logger->debug('<<checkTableNeedsUpdate');
1773
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1774
        } else {
1775
            // check the table indexes
1776
            try {
1777
                $this->checkIndexes();
1778
            } catch (AlphaException $ae) {
1779
                self::$logger->warn("Error while checking database indexes:\n\n".$ae->getMessage());
1780
            }
1781
1782
            self::$logger->debug('<<checkTableNeedsUpdate ['.$updateRequired.']');
1783
1784
            return $updateRequired;
1785
        }
1786
    }
1787
1788
    /**
1789
     * (non-PHPdoc).
1790
     *
1791
     * @see Alpha\Model\ActiveRecordProviderInterface::findMissingFields()
1792
     */
1793
    public function findMissingFields()
1794
    {
1795
        self::$logger->debug('>>findMissingFields()');
1796
1797
        $missingFields = array();
1798
        $matchCount = 0;
1799
1800
        $sqlQuery = 'PRAGMA table_info('.$this->record->getTableName().')';
1801
        $result = self::getConnection()->query($sqlQuery);
1802
        $this->record->setLastQuery($sqlQuery);
1803
1804
        // get the class attributes
1805
        $reflection = new ReflectionClass(get_class($this->record));
1806
        $properties = $reflection->getProperties();
1807
1808
        foreach ($properties as $propObj) {
1809
            $propName = $propObj->name;
1810
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1811
                while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1812
                    if ($propName == $row['name']) {
1813
                        ++$matchCount;
1814
                        break;
1815
                    }
1816
                }
1817
                $result->reset();
1818
            } else {
1819
                ++$matchCount;
1820
            }
1821
1822
            if ($matchCount == 0) {
1823
                array_push($missingFields, $propName);
1824
            } else {
1825
                $matchCount = 0;
1826
            }
1827
        }
1828
1829
        // check for the "classname" field in overloaded tables
1830
        if ($this->record->isTableOverloaded()) {
1831
            $foundMatch = false;
1832
1833
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1834
                if ('classname' == $row['name']) {
1835
                    $foundMatch = true;
1836
                    break;
1837
                }
1838
            }
1839
            if (!$foundMatch) {
1840
                array_push($missingFields, 'classname');
1841
            }
1842
        }
1843
1844
        if (!$result) {
1845
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1846
        }
1847
1848
        self::$logger->debug('<<findMissingFields ['.var_export($missingFields, true).']');
1849
1850
        return $missingFields;
1851
    }
1852
1853
    /**
1854
     * (non-PHPdoc).
1855
     *
1856
     * @see Alpha\Model\ActiveRecordProviderInterface::getIndexes()
1857
     */
1858
    public function getIndexes()
1859
    {
1860
        self::$logger->debug('>>getIndexes()');
1861
1862
        $sqlQuery = "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='".$this->record->getTableName()."'";
1863
1864
        $this->record->setLastQuery($sqlQuery);
1865
1866
        $indexNames = array();
1867
1868
        if (!$result = self::getConnection()->query($sqlQuery)) {
1869
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1870
        } else {
1871
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1872
                array_push($indexNames, $row['name']);
1873
            }
1874
        }
1875
1876
        // in SQLite foreign keys are not stored in sqlite_master, so we have to run a different query and append the results
1877
        $sqlQuery = 'PRAGMA foreign_key_list('.$this->record->getTableName().')';
1878
        
1879
        $this->record->setLastQuery($sqlQuery);
1880
1881
        if (!$result = self::getConnection()->query($sqlQuery)) {
1882
            self::$logger->warn('Error during pragma table foreign key lookup ['.self::getLastDatabaseError().']');
1883
        } else {
1884
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1885
                // SQLite does not name FK indexes, so we will return a fake name based the same convention used in MySQL
1886
                $fakeIndexName = $this->record->getTableName().'_'.$row['from'].'_fk_idx';
1887
                array_push($indexNames, $fakeIndexName);
1888
            }
1889
        }
1890
1891
        self::$logger->debug('<<getIndexes');
1892
1893
        return $indexNames;
1894
    }
1895
1896
    /**
1897
     * Checks to see if all of the indexes are in place for the record's table, creates those that are missing.
1898
     *
1899
     * @since 1.2
1900
     */
1901
    private function checkIndexes()
1902
    {
1903
        self::$logger->debug('>>checkIndexes()');
1904
1905
        $indexNames = $this->record->getIndexes();
1906
1907
        // process unique keys
1908
        foreach ($this->record->getUniqueAttributes() as $prop) {
1909
            // check for composite indexes
1910
            if (mb_strpos($prop, '+')) {
1911
                $attributes = explode('+', $prop);
1912
1913
                $index_exists = false;
1914
                foreach ($indexNames as $index) {
1915
                    if ($attributes[0].'_'.$attributes[1].'_unq_idx' == $index) {
1916
                        $index_exists = true;
1917
                    }
1918
                    if (count($attributes) == 3) {
1919
                        if ($attributes[0].'_'.$attributes[1].'_'.$attributes[2].'_unq_idx' == $index) {
1920
                            $index_exists = true;
1921
                        }
1922
                    }
1923
                }
1924
1925
                if (!$index_exists) {
1926
                    if (count($attributes) == 3) {
1927
                        $this->record->createUniqueIndex($attributes[0], $attributes[1], $attributes[2]);
1928
                    } else {
1929
                        $this->record->createUniqueIndex($attributes[0], $attributes[1]);
1930
                    }
1931
                }
1932
            } else {
1933
                $index_exists = false;
1934
                foreach ($indexNames as $index) {
1935
                    if ($prop.'_unq_idx' == $index) {
1936
                        $index_exists = true;
1937
                    }
1938
                }
1939
1940
                if (!$index_exists) {
1941
                    $this->createUniqueIndex($prop);
1942
                }
1943
            }
1944
        }
1945
1946
        // process foreign-key indexes
1947
        // get the class attributes
1948
        $reflection = new ReflectionClass(get_class($this->record));
1949
        $properties = $reflection->getProperties();
1950
        foreach ($properties as $propObj) {
1951
            $propName = $propObj->name;
1952
            $prop = $this->record->getPropObject($propName);
1953
            if ($prop instanceof Relation) {
1954
                if ($prop->getRelationType() == 'MANY-TO-ONE') {
1955
                    $indexExists = false;
1956
                    foreach ($indexNames as $index) {
1957
                        if ($this->record->getTableName().'_'.$propName.'_fk_idx' == $index) {
1958
                            $indexExists = true;
1959
                        }
1960
                    }
1961
                    if (!$indexExists) {
1962
                        $this->createForeignIndex($propName, $prop->getRelatedClass(), $prop->getRelatedClassField());
1963
                    }
1964
                }
1965
                if ($prop->getRelationType() == 'MANY-TO-MANY') {
1966
                    $lookup = $prop->getLookup();
1967
                    if ($lookup != null) {
1968
                        try {
1969
                            $lookupIndexNames = $lookup->getIndexes();
1970
                            // handle index check/creation on left side of Relation
1971
                            $indexExists = false;
1972
                            foreach ($lookupIndexNames as $index) {
1973
                                if ($lookup->getTableName().'_leftID_fk_idx' == $index) {
1974
                                    $indexExists = true;
1975
                                }
1976
                            }
1977
                            if (!$indexExists) {
1978
                                $lookup->createForeignIndex('leftID', $prop->getRelatedClass('left'), 'ID');
1979
                            }
1980
                            // handle index check/creation on right side of Relation
1981
                            $indexExists = false;
1982
                            foreach ($lookupIndexNames as $index) {
1983
                                if ($lookup->getTableName().'_rightID_fk_idx' == $index) {
1984
                                    $indexExists = true;
1985
                                }
1986
                            }
1987
                            if (!$indexExists) {
1988
                                $lookup->createForeignIndex('rightID', $prop->getRelatedClass('right'), 'ID');
1989
                            }
1990
                        } catch (AlphaException $e) {
1991
                            self::$logger->error($e->getMessage());
1992
                        }
1993
                    }
1994
                }
1995
            }
1996
        }
1997
1998
        self::$logger->debug('<<checkIndexes');
1999
    }
2000
2001
    /**
2002
     * Note that SQLite 3.6.19 is requrired for foreign key support.
2003
     *
2004
     * (non-PHPdoc)
2005
     *
2006
     * @see Alpha\Model\ActiveRecordProviderInterface::createForeignIndex()
2007
     */
2008
    public function createForeignIndex($attributeName, $relatedClass, $relatedClassAttribute, $indexName = null)
2009
    {
2010
        self::$logger->info('>>createForeignIndex(attributeName=['.$attributeName.'], relatedClass=['.$relatedClass.'], relatedClassAttribute=['.$relatedClassAttribute.'], indexName=['.$indexName.']');
2011
2012
        /*
2013
         * High-level approach
2014
         *
2015
         * 1. Rename the source table to [tablename]_temp
2016
         * 2. Create a new [tablename] table, with the new FK in place.
2017
         * 3. Copy all of the data from [tablename]_temp to [tablename].
2018
         * 4. Drop [tablename]_temp.
2019
         */
2020
        try {
2021
            // rename the table to [tablename]_temp
2022
            $query = 'ALTER TABLE '.$this->record->getTableName().' RENAME TO '.$this->record->getTableName().'_temp;';
2023
            $this->record->setLastQuery($query);
2024
            self::getConnection()->query($query);
2025
2026
            self::$logger->info('Renamed the table ['.$this->record->getTableName().'] to ['.$this->record->getTableName().'_temp]');
2027
2028
            // now create the new table with the FK in place
2029
            $record = new $relatedClass();
2030
            $tableName = $record->getTableName();
2031
            $this->foreignKeys[$attributeName] = array($tableName, $relatedClassAttribute);
2032
2033
            if (!$this->checkTableExists()) {
2034
                $this->makeTable(false);
2035
            }
2036
2037
            self::$logger->info('Made a new copy of the table ['.$this->record->getTableName().']');
2038
2039
            // copy all of the old data to the new table
2040
            $query = 'INSERT INTO '.$this->record->getTableName().' SELECT * FROM '.$this->record->getTableName().'_temp;';
2041
            $this->record->setLastQuery($query);
2042
            self::getConnection()->query($query);
2043
2044
            self::$logger->info('Copied all of the data from ['.$this->record->getTableName().'] to ['.$this->record->getTableName().'_temp]');
2045
2046
            // finally, drop the _temp table and commit the changes
2047
            $this->record->dropTable($this->record->getTableName().'_temp');
2048
2049
            self::$logger->info('Dropped the table ['.$this->record->getTableName().'_temp]');
2050
        } catch (Exception $e) {
2051
            throw new FailedIndexCreateException('Failed to create the index ['.$attributeName.'] on ['.$this->record->getTableName().'], error is ['.$e->getMessage().'], query ['.$this->record->getLastQuery().']');
2052
        }
2053
2054
        self::$logger->info('<<createForeignIndex');
2055
    }
2056
2057
    /**
2058
     * (non-PHPdoc).
2059
     *
2060
     * @see Alpha\Model\ActiveRecordProviderInterface::createUniqueIndex()
2061
     */
2062
    public function createUniqueIndex($attribute1Name, $attribute2Name = '', $attribute3Name = '')
2063
    {
2064
        self::$logger->debug('>>createUniqueIndex(attribute1Name=['.$attribute1Name.'], attribute2Name=['.$attribute2Name.'], attribute3Name=['.$attribute3Name.'])');
2065
2066
        $sqlQuery = '';
2067
2068
        if ($attribute2Name != '' && $attribute3Name != '') {
2069
            $sqlQuery = 'CREATE UNIQUE INDEX IF NOT EXISTS '.$attribute1Name.'_'.$attribute2Name.'_'.$attribute3Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.','.$attribute2Name.','.$attribute3Name.');';
2070
        }
2071
2072
        if ($attribute2Name != '' && $attribute3Name == '') {
2073
            $sqlQuery = 'CREATE UNIQUE INDEX IF NOT EXISTS '.$attribute1Name.'_'.$attribute2Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.','.$attribute2Name.');';
2074
        }
2075
2076
        if ($attribute2Name == '' && $attribute3Name == '') {
2077
            $sqlQuery = 'CREATE UNIQUE INDEX IF NOT EXISTS '.$attribute1Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.');';
2078
        }
2079
2080
        $this->record->setLastQuery($sqlQuery);
2081
2082
        $result = self::getConnection()->query($sqlQuery);
2083
2084
        if ($result) {
2085
            self::$logger->debug('Successfully created the unique index on ['.$this->record->getTableName().']');
2086
        } else {
2087
            throw new FailedIndexCreateException('Failed to create the unique index on ['.$this->record->getTableName().'], error is ['.self::getConnection()->lastErrorMsg().']');
2088
        }
2089
2090
        self::$logger->debug('<<createUniqueIndex');
2091
    }
2092
2093
    /**
2094
     * (non-PHPdoc).
2095
     *
2096
     * @see Alpha\Model\ActiveRecordProviderInterface::reload()
2097
     */
2098
    public function reload()
2099
    {
2100
        self::$logger->debug('>>reload()');
2101
2102
        if (!$this->record->isTransient()) {
2103
            $this->record->load($this->record->getID());
2104
        } else {
2105
            throw new AlphaException('Cannot reload transient object from database!');
2106
        }
2107
2108
        self::$logger->debug('<<reload');
2109
    }
2110
2111
    /**
2112
     * (non-PHPdoc).
2113
     *
2114
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordExists()
2115
     */
2116
    public function checkRecordExists($ID)
2117
    {
2118
        self::$logger->debug('>>checkRecordExists(ID=['.$ID.'])');
2119
2120
        $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE ID = :ID;';
2121
        $this->record->setLastQuery($sqlQuery);
2122
        $stmt = self::getConnection()->prepare($sqlQuery);
2123
2124
        if ($stmt instanceof SQLite3Stmt) {
2125
            $stmt->bindValue(':ID', $ID, SQLITE3_INTEGER);
2126
2127
            $result = $stmt->execute();
2128
2129
            // there should only ever be one (or none)
2130
            $row = $result->fetchArray(SQLITE3_ASSOC);
2131
2132
            $stmt->close();
2133
        } else {
2134
            self::$logger->debug('<<checkRecordExists');
2135
            throw new AlphaException('Failed to check for the record ['.$ID.'] on the class ['.get_class($this->record).'] from the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
2136
        }
2137
2138
        if (!isset($row['ID'])) {
2139
            self::$logger->debug('<<checkRecordExists [false]');
2140
2141
            return false;
2142
        } else {
2143
            self::$logger->debug('<<checkRecordExists [true]');
2144
2145
            return true;
2146
        }
2147
    }
2148
2149
    /**
2150
     * (non-PHPdoc).
2151
     *
2152
     * @see Alpha\Model\ActiveRecordProviderInterface::isTableOverloaded()
2153
     */
2154
    public function isTableOverloaded()
2155
    {
2156
        self::$logger->debug('>>isTableOverloaded()');
2157
2158
        $reflection = new ReflectionClass($this->record);
2159
        $classname = $reflection->getShortName();
2160
        $tablename = ucfirst($this->record->getTableName());
2161
2162
        // use reflection to check to see if we are dealing with a persistent type (e.g. DEnum) which are never overloaded
2163
        $implementedInterfaces = $reflection->getInterfaces();
2164
2165
        foreach ($implementedInterfaces as $interface) {
2166
            if ($interface->name == 'Alpha\Model\Type\TypeInterface') {
2167
                self::$logger->debug('<<isTableOverloaded [false]');
2168
2169
                return false;
2170
            }
2171
        }
2172
2173
        if ($classname != $tablename) {
2174
            // loop over all records to see if there is one using the same table as this record
2175
2176
            $Recordclasses = ActiveRecord::getRecordClassNames();
2177
2178
            foreach ($Recordclasses as $RecordclassName) {
2179
                $reflection = new ReflectionClass($RecordclassName);
2180
                $classname = $reflection->getShortName();
2181
                if ($tablename == $classname) {
2182
                    self::$logger->debug('<<isTableOverloaded [true]');
2183
2184
                    return true;
2185
                }
2186
            }
2187
            self::$logger->debug('<<isTableOverloaded');
2188
            throw new BadTableNameException('The table name ['.$tablename.'] for the class ['.$classname.'] is invalid as it does not match a Record definition in the system!');
2189
        } else {
2190
            // check to see if there is already a "classname" column in the database for this record
2191
            $sqlQuery = 'PRAGMA table_info('.$this->record->getTableName().')';
2192
            $result = self::getConnection()->query($sqlQuery);
2193
            $this->record->setLastQuery($sqlQuery);
2194
2195
            if (!$result) {
2196
                self::$logger->warn('Error during pragma table info lookup ['.self::getLastDatabaseError().']');
2197
            } else {
2198
                while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
2199
                    if ('classname' == $row['name']) {
2200
                        self::$logger->debug('<<isTableOverloaded [true]');
2201
2202
                        return true;
2203
                    }
2204
                }
2205
            }
2206
2207
            self::$logger->debug('<<isTableOverloaded [false]');
2208
2209
            return false;
2210
        }
2211
    }
2212
2213
    /**
2214
     * (non-PHPdoc).
2215
     *
2216
     * @see Alpha\Model\ActiveRecordProviderInterface::begin()
2217
     */
2218
    public static function begin()
2219
    {
2220
        if (self::$logger == null) {
2221
            self::$logger = new Logger('ActiveRecordProviderSQLite');
2222
        }
2223
        self::$logger->debug('>>begin()');
2224
2225
        if (!self::getConnection()->exec('BEGIN')) {
2226
            throw new AlphaException('Error beginning a new transaction, error is ['.self::getLastDatabaseError().']');
2227
        }
2228
2229
        self::$logger->debug('<<begin');
2230
    }
2231
2232
    /**
2233
     * (non-PHPdoc).
2234
     *
2235
     * @see Alpha\Model\ActiveRecordProviderInterface::commit()
2236
     */
2237
    public static function commit()
2238
    {
2239
        if (self::$logger == null) {
2240
            self::$logger = new Logger('ActiveRecordProviderSQLite');
2241
        }
2242
        self::$logger->debug('>>commit()');
2243
2244
        if (!self::getConnection()->exec('COMMIT')) {
2245
            throw new AlphaException('Error commiting a transaction, error is ['.self::getLastDatabaseError().']');
2246
        }
2247
2248
        self::$logger->debug('<<commit');
2249
    }
2250
2251
    /**
2252
     * (non-PHPdoc).
2253
     *
2254
     * @see Alpha\Model\ActiveRecordProviderInterface::rollback()
2255
     */
2256
    public static function rollback()
2257
    {
2258
        if (self::$logger == null) {
2259
            self::$logger = new Logger('ActiveRecordProviderSQLite');
2260
        }
2261
2262
        self::$logger->debug('>>rollback()');
2263
2264
        try {
2265
            self::getConnection()->exec('ROLLBACK');
2266
            self::disconnect();
2267
        } catch (Exception $e) {
2268
            if (mb_strpos($e->getMessage(), 'cannot rollback - no transaction is active') === false) { // just filtering out errors where the rollback failed due to no current transaction
2269
                throw new AlphaException('Error rolling back a transaction, error is ['.self::getLastDatabaseError().']');
2270
            }
2271
        }
2272
2273
        self::$logger->debug('<<rollback');
2274
    }
2275
2276
    /**
2277
     * (non-PHPdoc).
2278
     *
2279
     * @see Alpha\Model\ActiveRecordProviderInterface::setRecord()
2280
     */
2281
    public function setRecord($Record)
2282
    {
2283
        $this->record = $Record;
2284
    }
2285
2286
    /**
2287
     * (non-PHPdoc).
2288
     *
2289
     * @see Alpha\Model\ActiveRecordProviderInterface::checkDatabaseExists()
2290
     */
2291
    public static function checkDatabaseExists()
2292
    {
2293
        $config = ConfigProvider::getInstance();
2294
2295
        return file_exists($config->get('db.file.path'));
2296
    }
2297
2298
    /**
2299
     * (non-PHPdoc).
2300
     *
2301
     * @see Alpha\Model\ActiveRecordProviderInterface::createDatabase()
2302
     */
2303
    public static function createDatabase()
2304
    {
2305
        $config = ConfigProvider::getInstance();
2306
2307
        if (!self::checkDatabaseExists()) {
2308
            fopen($config->get('db.file.path'), 'x+');
2309
            chmod($config->get('db.file.path'), 0755);
2310
        }
2311
    }
2312
2313
    /**
2314
     * (non-PHPdoc).
2315
     *
2316
     * @see Alpha\Model\ActiveRecordProviderInterface::dropDatabase()
2317
     */
2318
    public static function dropDatabase()
2319
    {
2320
        $config = ConfigProvider::getInstance();
2321
2322
        if (self::checkDatabaseExists()) {
2323
            unlink($config->get('db.file.path'));
2324
        }
2325
    }
2326
2327
    /**
2328
     * (non-PHPdoc).
2329
     *
2330
     * @see Alpha\Model\ActiveRecordProviderInterface::backupDatabase()
2331
     */
2332
    public static function backupDatabase($targetFile)
2333
    {
2334
        $config = ConfigProvider::getInstance();
2335
2336
        exec('sqlite3 '.$config->get('db.file.path').' ".backup '.$targetFile.'"');
2337
    }
2338
}
2339