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

ActiveRecordProviderSQLite::backupDatabase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
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\SmallText;
13
use Alpha\Model\Type\Date;
14
use Alpha\Model\Type\Enum;
15
use Alpha\Model\Type\Boolean;
16
use Alpha\Util\Config\ConfigProvider;
17
use Alpha\Util\Logging\Logger;
18
use Alpha\Util\Service\ServiceFactory;
19
use Alpha\Exception\AlphaException;
20
use Alpha\Exception\FailedSaveException;
21
use Alpha\Exception\FailedDeleteException;
22
use Alpha\Exception\FailedIndexCreateException;
23
use Alpha\Exception\LockingException;
24
use Alpha\Exception\ValidationException;
25
use Alpha\Exception\CustomQueryException;
26
use Alpha\Exception\RecordNotFoundException;
27
use Alpha\Exception\BadTableNameException;
28
use Alpha\Exception\NotImplementedException;
29
use Alpha\Exception\PHPException;
30
use Alpha\Exception\ResourceNotAllowedException;
31
use Alpha\Exception\IllegalArguementException;
32
use Exception;
33
use SQLite3Stmt;
34
use SQLite3;
35
use ReflectionClass;
36
37
/**
38
 * SQLite active record provider (uses the SQLite3 native API in PHP).
39
 *
40
 * @since 1.2
41
 *
42
 * @author John Collins <[email protected]>
43
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
44
 * @copyright Copyright (c) 2017, John Collins (founder of Alpha Framework).
45
 * All rights reserved.
46
 *
47
 * <pre>
48
 * Redistribution and use in source and binary forms, with or
49
 * without modification, are permitted provided that the
50
 * following conditions are met:
51
 *
52
 * * Redistributions of source code must retain the above
53
 *   copyright notice, this list of conditions and the
54
 *   following disclaimer.
55
 * * Redistributions in binary form must reproduce the above
56
 *   copyright notice, this list of conditions and the
57
 *   following disclaimer in the documentation and/or other
58
 *   materials provided with the distribution.
59
 * * Neither the name of the Alpha Framework nor the names
60
 *   of its contributors may be used to endorse or promote
61
 *   products derived from this software without specific
62
 *   prior written permission.
63
 *
64
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
65
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
66
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
67
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
68
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
69
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
70
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
71
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
72
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
73
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
74
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
75
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
76
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
77
 * </pre>
78
 */
79
class ActiveRecordProviderSQLite implements ActiveRecordProviderInterface
80
{
81
    /**
82
     * Trace logger.
83
     *
84
     * @var \Alpha\Util\Logging\Logger
85
     *
86
     * @since 1.2
87
     */
88
    private static $logger = null;
89
90
    /**
91
     * Database connection.
92
     *
93
     * @var SQLite3
94
     *
95
     * @since 1.2
96
     */
97
    private static $connection;
98
99
    /**
100
     * The business object that we are mapping back to.
101
     *
102
     * @var \Alpha\Model\ActiveRecord
103
     *
104
     * @since 1.2
105
     */
106
    private $record;
107
108
    /**
109
     * An array of new foreign keys that need to be created.
110
     *
111
     * @var array
112
     *
113
     * @since 2.0.1
114
     */
115
    private $foreignKeys = array();
116
117
    /**
118
     * The constructor.
119
     *
120
     * @since 1.2
121
     */
122
    public function __construct()
123
    {
124
        self::$logger = new Logger('ActiveRecordProviderSQLite');
125
        self::$logger->debug('>>__construct()');
126
127
        self::$logger->debug('<<__construct');
128
    }
129
130
    /**
131
     * (non-PHPdoc).
132
     *
133
     * @see Alpha\Model\ActiveRecordProviderInterface::getConnection()
134
     */
135
    public static function getConnection()
136
    {
137
        $config = ConfigProvider::getInstance();
138
139
        if (!isset(self::$connection)) {
140
            try {
141
                self::$connection = new SQLite3($config->get('db.file.path'));
142
            } catch (\Exception $e) {
143
                self::$logger->fatal('Could not open SQLite database: ['.$e->getMessage().']');
144
            }
145
        }
146
147
        return self::$connection;
148
    }
149
150
    /**
151
     * (non-PHPdoc).
152
     *
153
     * @see Alpha\Model\ActiveRecordProviderInterface::disconnect()
154
     */
155
    public static function disconnect()
156
    {
157
        if (isset(self::$connection)) {
158
            self::$connection->close();
159
            self::$connection = null;
160
        }
161
    }
162
163
    /**
164
     * (non-PHPdoc).
165
     *
166
     * @see Alpha\Model\ActiveRecordProviderInterface::getLastDatabaseError()
167
     */
168
    public static function getLastDatabaseError()
169
    {
170
        self::$connection->lastErrorMsg();
171
    }
172
173
    /**
174
     * (non-PHPdoc).
175
     *
176
     * @see Alpha\Model\ActiveRecordProviderInterface::query()
177
     */
178
    public function query($sqlQuery)
179
    {
180
        $this->record->setLastQuery($sqlQuery);
181
182
        $resultArray = array();
183
184
        if (!$result = self::getConnection()->query($sqlQuery)) {
185
            throw new CustomQueryException('Failed to run the custom query, SQLite error is ['.self::getLastDatabaseError().'], query ['.$sqlQuery.']');
186
        } else {
187
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
188
                array_push($resultArray, $row);
189
            }
190
191
            return $resultArray;
192
        }
193
    }
194
195
    /**
196
     * (non-PHPdoc).
197
     *
198
     * @see Alpha\Model\ActiveRecordProviderInterface::load()
199
     */
200
    public function load($ID, $version = 0)
201
    {
202
        self::$logger->debug('>>load(ID=['.$ID.'], version=['.$version.'])');
203
204
        $attributes = $this->record->getPersistentAttributes();
205
        $fields = '';
206
        foreach ($attributes as $att) {
207
            $fields .= $att.',';
208
        }
209
        $fields = mb_substr($fields, 0, -1);
210
211
        if ($version > 0) {
212
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().'_history WHERE ID = :ID AND version_num = :version LIMIT 1;';
213
        } else {
214
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE ID = :ID LIMIT 1;';
215
        }
216
        $this->record->setLastQuery($sqlQuery);
217
218
        try {
219
            $stmt = self::getConnection()->prepare($sqlQuery);
220
221
            $row = array();
222
223
            if ($version > 0) {
224
                $stmt->bindValue(':version', $version, SQLITE3_INTEGER);
225
            }
226
227
            $stmt->bindValue(':ID', $ID, SQLITE3_INTEGER);
228
229
            $result = $stmt->execute();
230
231
            // there should only ever be one (or none)
232
            $row = $result->fetchArray(SQLITE3_ASSOC);
233
234
            $stmt->close();
235
        } catch (PHPException $e) {
236
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
237
            if (!$this->record->checkTableExists()) {
238
                $this->record->makeTable();
239
240
                throw new RecordNotFoundException('Failed to load object of ID ['.$ID.'], table ['.$this->record->getTableName().'] did not exist so had to create!');
241
            }
242
243
            return;
244
        }
245
246
        if (!isset($row['ID']) || $row['ID'] < 1) {
247
            self::$logger->debug('<<load');
248
            throw new RecordNotFoundException('Failed to load object of ID ['.$ID.'] not found in database.');
249
        }
250
251
        // get the class attributes
252
        $reflection = new ReflectionClass(get_class($this->record));
253
        $properties = $reflection->getProperties();
254
255
        try {
256
            foreach ($properties as $propObj) {
257
                $propName = $propObj->name;
258
259
                // filter transient attributes
260
                if (!in_array($propName, $this->record->getTransientAttributes())) {
261
                    $this->record->set($propName, $row[$propName]);
262
                } elseif (!$propObj->isPrivate() && $this->record->getPropObject($propName) instanceof Relation) {
263
                    $prop = $this->record->getPropObject($propName);
264
265
                    // handle the setting of ONE-TO-MANY relation values
266
                    if ($prop->getRelationType() == 'ONE-TO-MANY') {
267
                        $this->record->set($propObj->name, $this->record->getID());
268
                    }
269
270
                    // handle the setting of MANY-TO-ONE relation values
271
                    if ($prop->getRelationType() == 'MANY-TO-ONE' && isset($row[$propName])) {
272
                        $this->record->set($propObj->name, $row[$propName]);
273
                    }
274
                }
275
            }
276
        } catch (IllegalArguementException $e) {
277
            self::$logger->warn('Bad data stored in the table ['.$this->record->getTableName().'], field ['.$propObj->name.'] bad value['.$row[$propObj->name].'], exception ['.$e->getMessage().']');
278
        } catch (PHPException $e) {
279
            // it is possible that the load failed due to the table not being up-to-date
280
            if ($this->record->checkTableNeedsUpdate()) {
281
                $missingFields = $this->record->findMissingFields();
282
283
                $count = count($missingFields);
284
285
                for ($i = 0; $i < $count; ++$i) {
286
                    $this->record->addProperty($missingFields[$i]);
287
                }
288
289
                self::$logger->debug('<<load');
290
                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!');
291
            }
292
        }
293
294
        self::$logger->debug('<<load');
295
    }
296
297
    /**
298
     * (non-PHPdoc).
299
     *
300
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllOldVersions()
301
     */
302
    public function loadAllOldVersions($ID)
303
    {
304
        self::$logger->debug('>>loadAllOldVersions(ID=['.$ID.'])');
305
306
        if (!$this->record->getMaintainHistory()) {
307
            throw new RecordFoundException('loadAllOldVersions method called on an active record where no history is maintained!');
308
        }
309
310
        $sqlQuery = 'SELECT version_num FROM '.$this->record->getTableName().'_history WHERE ID = \''.$ID.'\' ORDER BY version_num;';
311
312
        $this->record->setLastQuery($sqlQuery);
313
314
        if (!$result = self::getConnection()->query($sqlQuery)) {
315
            self::$logger->debug('<<loadAllOldVersions');
316
            throw new RecordNotFoundException('Failed to load object versions, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
317
        }
318
319
        // now build an array of objects to be returned
320
        $objects = array();
321
        $count = 0;
322
        $RecordClass = get_class($this->record);
323
324
        while ($row = $result->fetchArray()) {
325
            try {
326
                $obj = new $RecordClass();
327
                $obj->load($ID, $row['version_num']);
328
                $objects[$count] = $obj;
329
                ++$count;
330
            } catch (ResourceNotAllowedException $e) {
331
                // the resource not allowed will be absent from the list
332
            }
333
        }
334
335
        self::$logger->warn('<<loadAllOldVersions ['.count($objects).']');
336
337
        return $objects;
338
    }
339
340
    /**
341
     * (non-PHPdoc).
342
     *
343
     * @see Alpha\Model\ActiveRecordProviderInterface::loadByAttribute()
344
     */
345
    public function loadByAttribute($attribute, $value, $ignoreClassType = false, $loadAttributes = array())
346
    {
347
        self::$logger->debug('>>loadByAttribute(attribute=['.$attribute.'], value=['.$value.'], ignoreClassType=['.$ignoreClassType.'], 
348
            loadAttributes=['.var_export($loadAttributes, true).'])');
349
350
        if (count($loadAttributes) == 0) {
351
            $attributes = $this->record->getPersistentAttributes();
352
        } else {
353
            $attributes = $loadAttributes;
354
        }
355
356
        $fields = '';
357
        foreach ($attributes as $att) {
358
            $fields .= $att.',';
359
        }
360
        $fields = mb_substr($fields, 0, -1);
361
362
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
363
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE '.$attribute.' = :attribute AND classname = :classname LIMIT 1;';
364
        } else {
365
            $sqlQuery = 'SELECT '.$fields.' FROM '.$this->record->getTableName().' WHERE '.$attribute.' = :attribute LIMIT 1;';
366
        }
367
368
        self::$logger->debug('Query=['.$sqlQuery.']');
369
370
        $this->record->setLastQuery($sqlQuery);
371
        $stmt = self::getConnection()->prepare($sqlQuery);
372
373
        if ($stmt instanceof SQLite3Stmt) {
374
            if ($this->record->getPropObject($attribute) instanceof Integer) {
375
                if (!$ignoreClassType && $this->record->isTableOverloaded()) {
376
                    $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
377
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
378
                } else {
379
                    $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
380
                }
381
            } else {
382
                if (!$ignoreClassType && $this->record->isTableOverloaded()) {
383
                    $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
384
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
385
                } else {
386
                    $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
387
                }
388
            }
389
390
            $result = $stmt->execute();
391
392
            // there should only ever be one (or none)
393
            $row = $result->fetchArray(SQLITE3_ASSOC);
394
395
            $stmt->close();
396
        } else {
397
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
398
            if (!$this->record->checkTableExists()) {
399
                $this->record->makeTable();
400
401
                throw new RecordNotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], table did not exist so had to create!');
402
            }
403
404
            return;
405
        }
406
407
        if (!isset($row['ID']) || $row['ID'] < 1) {
408
            self::$logger->debug('<<loadByAttribute');
409
            throw new RecordNotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], not found in database.');
410
        }
411
412
        $this->record->setID($row['ID']);
413
414
        // get the class attributes
415
        $reflection = new ReflectionClass(get_class($this->record));
416
        $properties = $reflection->getProperties();
417
418
        try {
419
            foreach ($properties as $propObj) {
420
                $propName = $propObj->name;
421
422
                if (isset($row[$propName])) {
423
                    // filter transient attributes
424
                    if (!in_array($propName, $this->record->getTransientAttributes())) {
425
                        $this->record->set($propName, $row[$propName]);
426
                    } elseif (!$propObj->isPrivate() && $this->record->get($propName) != '' && $this->record->getPropObject($propName) instanceof Relation) {
427
                        $prop = $this->record->getPropObject($propName);
428
429
                        // handle the setting of ONE-TO-MANY relation values
430
                        if ($prop->getRelationType() == 'ONE-TO-MANY') {
431
                            $this->record->set($propObj->name, $this->record->getID());
432
                        }
433
                    }
434
                }
435
            }
436
        } catch (IllegalArguementException $e) {
437
            self::$logger->warn('Bad data stored in the table ['.$this->record->getTableName().'], field ['.$propObj->name.'] bad value['.$row[$propObj->name].'], exception ['.$e->getMessage().']');
438
        } catch (PHPException $e) {
439
            // it is possible that the load failed due to the table not being up-to-date
440
            if ($this->record->checkTableNeedsUpdate()) {
441
                $missingFields = $this->record->findMissingFields();
442
443
                $count = count($missingFields);
444
445
                for ($i = 0; $i < $count; ++$i) {
446
                    $this->record->addProperty($missingFields[$i]);
447
                }
448
449
                self::$logger->debug('<<loadByAttribute');
450
                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!');
451
            }
452
        }
453
454
        self::$logger->debug('<<loadByAttribute');
455
    }
456
457
    /**
458
     * (non-PHPdoc).
459
     *
460
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAll()
461
     */
462
    public function loadAll($start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false)
463
    {
464
        self::$logger->debug('>>loadAll(start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
465
466
        // ensure that the field name provided in the orderBy param is legit
467
        try {
468
            $this->record->get($orderBy);
469
        } catch (AlphaException $e) {
470
            throw new AlphaException('The field name ['.$orderBy.'] provided in the param orderBy does not exist on the class ['.get_class($this->record).']');
471
        }
472
473
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
474
            if ($limit == 0) {
475
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE classname = \''.get_class($this->record).'\' ORDER BY '.$orderBy.' '.$order.';';
476
            } else {
477
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE classname = \''.get_class($this->record).'\' ORDER BY '.$orderBy.' '.$order.' LIMIT '.
478
                    $limit.' OFFSET '.$start.';';
479
            }
480
        } else {
481
            if ($limit == 0) {
482
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' ORDER BY '.$orderBy.' '.$order.';';
483
            } else {
484
                $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' ORDER BY '.$orderBy.' '.$order.' LIMIT '.$limit.' OFFSET '.$start.';';
485
            }
486
        }
487
488
        $this->record->setLastQuery($sqlQuery);
489
490
        if (!$result = self::getConnection()->query($sqlQuery)) {
491
            self::$logger->debug('<<loadAll');
492
            throw new RecordNotFoundException('Failed to load object IDs, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
493
        }
494
495
        // now build an array of objects to be returned
496
        $objects = array();
497
        $count = 0;
498
        $RecordClass = get_class($this->record);
499
500
        while ($row = $result->fetchArray()) {
501
            try {
502
                $obj = new $RecordClass();
503
                $obj->load($row['ID']);
504
                $objects[$count] = $obj;
505
                ++$count;
506
            } catch (ResourceNotAllowedException $e) {
507
                // the resource not allowed will be absent from the list
508
            }
509
        }
510
511
        self::$logger->debug('<<loadAll ['.count($objects).']');
512
513
        return $objects;
514
    }
515
516
    /**
517
     * (non-PHPdoc).
518
     *
519
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByAttribute()
520
     */
521
    public function loadAllByAttribute($attribute, $value, $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false, $constructorArgs = array())
522
    {
523
        self::$logger->debug('>>loadAllByAttribute(attribute=['.$attribute.'], value=['.$value.'], start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.'], constructorArgs=['.print_r($constructorArgs, true).']');
524
525
        if ($start != 0 && $limit != 0) {
526
            $limit = ' LIMIT '.$limit.' OFFSET '.$start.';';
527
        } else {
528
            $limit = ';';
529
        }
530
531
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
532
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE $attribute = :attribute AND classname = :classname ORDER BY ".$orderBy.' '.$order.$limit;
533
        } else {
534
            $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName()." WHERE $attribute = :attribute ORDER BY ".$orderBy.' '.$order.$limit;
535
        }
536
537
        $this->record->setLastQuery($sqlQuery);
538
        self::$logger->debug($sqlQuery);
539
540
        $stmt = self::getConnection()->prepare($sqlQuery);
541
542
        $objects = array();
543
544
        if ($stmt instanceof SQLite3Stmt) {
545
            if ($this->record->getPropObject($attribute) instanceof Integer) {
546
                if ($this->record->isTableOverloaded()) {
547
                    $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
548
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
549
                } else {
550
                    $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
551
                }
552
            } else {
553
                if ($this->record->isTableOverloaded()) {
554
                    $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
555
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
556
                } else {
557
                    $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
558
                }
559
            }
560
561
            $result = $stmt->execute();
562
563
            // now build an array of objects to be returned
564
            $count = 0;
565
            $RecordClass = get_class($this->record);
566
567
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
568
                try {
569
                    $argsCount = count($constructorArgs);
570
571
                    if ($argsCount < 1) {
572
                        $obj = new $RecordClass();
573
                    } else {
574
                        switch ($argsCount) {
575
                            case 1:
576
                                $obj = new $RecordClass($constructorArgs[0]);
577
                                break;
578
                            case 2:
579
                                $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1]);
580
                                break;
581
                            case 3:
582
                                $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2]);
583
                                break;
584
                            case 4:
585
                                $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3]);
586
                                break;
587
                            case 5:
588
                                $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3], $constructorArgs[4]);
589
                                break;
590
                            default:
591
                                throw new IllegalArguementException('Too many elements in the $constructorArgs array passed to the loadAllByAttribute method!');
592
                        }
593
                    }
594
595
                    $obj->load($row['ID']);
596
                    $objects[$count] = $obj;
597
                    ++$count;
598
                } catch (ResourceNotAllowedException $e) {
599
                    // the resource not allowed will be absent from the list
600
                }
601
            }
602
603
            $stmt->close();
604
        } else {
605
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
606
607
            if (!$this->record->checkTableExists()) {
608
                $this->record->makeTable();
609
610
                throw new RecordFoundException('Failed to load objects by attribute ['.$attribute.'] and value ['.$value.'], table did not exist so had to create!');
611
            }
612
613
            self::$logger->debug('<<loadAllByAttribute []');
614
615
            return array();
616
        }
617
618
        self::$logger->debug('<<loadAllByAttribute ['.count($objects).']');
619
620
        return $objects;
621
    }
622
623
    /**
624
     * (non-PHPdoc).
625
     *
626
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByAttributes()
627
     */
628
    public function loadAllByAttributes($attributes = array(), $values = array(), $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false, $constructorArgs = array())
629
    {
630
        self::$logger->debug('>>loadAllByAttributes(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'], start=['.
631
            $start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.'], constructorArgs=['.print_r($constructorArgs, true).']');
632
633
        $whereClause = ' WHERE';
634
635
        $count = count($attributes);
636
637
        for ($i = 0; $i < $count; ++$i) {
638
            $whereClause .= ' '.$attributes[$i].' = :'.$attributes[$i].' AND';
639
            self::$logger->debug($whereClause);
640
        }
641
642
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
643
            $whereClause .= ' classname = :classname AND';
644
        }
645
646
        // remove the last " AND"
647
        $whereClause = mb_substr($whereClause, 0, -4);
648
649
        if ($limit != 0) {
650
            $limit = ' LIMIT '.$limit.' OFFSET '.$start.';';
651
        } else {
652
            $limit = ';';
653
        }
654
655
        $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().$whereClause.' ORDER BY '.$orderBy.' '.$order.$limit;
656
657
        $this->record->setLastQuery($sqlQuery);
658
659
        $stmt = self::getConnection()->prepare($sqlQuery);
660
661
        if ($stmt instanceof SQLite3Stmt) {
662
            // bind params where required attributes are provided
663
            if (count($attributes) > 0 && count($attributes) == count($values)) {
664
                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...
665
                    if (strcspn($values[$i], '0123456789') != strlen($values[$i])) {
666
                        $stmt->bindValue(':'.$attributes[$i], $values[$i], SQLITE3_INTEGER);
667
                    } else {
668
                        $stmt->bindValue(':'.$attributes[$i], $values[$i], SQLITE3_TEXT);
669
                    }
670
                }
671
            } else {
672
                // we'll still need to bind the "classname" for overloaded records...
673
                if ($this->record->isTableOverloaded()) {
674
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
675
                }
676
            }
677
678
            $result = $stmt->execute();
679
        } else {
680
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
681
682
            if (!$this->record->checkTableExists()) {
683
                $this->record->makeTable();
684
685
                throw new RecordFoundException('Failed to load objects by attributes ['.var_export($attributes, true).'] and values ['.
686
                    var_export($values, true).'], table did not exist so had to create!');
687
            }
688
689
            self::$logger->debug('<<loadAllByAttributes []');
690
691
            return array();
692
        }
693
694
        // now build an array of objects to be returned
695
        $objects = array();
696
        $count = 0;
697
        $RecordClass = get_class($this->record);
698
699
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
700
            try {
701
                $argsCount = count($constructorArgs);
702
703
                if ($argsCount < 1) {
704
                    $obj = new $RecordClass();
705
                } else {
706
                    switch ($argsCount) {
707
                        case 1:
708
                            $obj = new $RecordClass($constructorArgs[0]);
709
                            break;
710
                        case 2:
711
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1]);
712
                            break;
713
                        case 3:
714
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2]);
715
                            break;
716
                        case 4:
717
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3]);
718
                            break;
719
                        case 5:
720
                            $obj = new $RecordClass($constructorArgs[0], $constructorArgs[1], $constructorArgs[2], $constructorArgs[3], $constructorArgs[4]);
721
                            break;
722
                        default:
723
                            throw new IllegalArguementException('Too many elements in the $constructorArgs array passed to the loadAllByAttribute method!');
724
                    }
725
                }
726
727
                $obj->load($row['ID']);
728
                $objects[$count] = $obj;
729
                ++$count;
730
            } catch (ResourceNotAllowedException $e) {
731
                // the resource not allowed will be absent from the list
732
            }
733
        }
734
735
        $stmt->close();
736
737
        self::$logger->debug('<<loadAllByAttributes ['.count($objects).']');
738
739
        return $objects;
740
    }
741
742
    /**
743
     * (non-PHPdoc).
744
     *
745
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllByDayUpdated()
746
     */
747
    public function loadAllByDayUpdated($date, $start = 0, $limit = 0, $orderBy = 'ID', $order = 'ASC', $ignoreClassType = false)
748
    {
749
        self::$logger->debug('>>loadAllByDayUpdated(date=['.$date.'], start=['.$start.'], limit=['.$limit.'], orderBy=['.$orderBy.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
750
751
        if ($start != 0 && $limit != 0) {
752
            $limit = ' LIMIT '.$limit.' OFFSET '.$start.';';
753
        } else {
754
            $limit = ';';
755
        }
756
757
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
758
            $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;
759
        } else {
760
            $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;
761
        }
762
763
        $this->record->setLastQuery($sqlQuery);
764
765
        if (!$result = self::getConnection()->query($sqlQuery)) {
766
            self::$logger->debug('<<loadAllByDayUpdated');
767
            throw new RecordNotFoundException('Failed to load object IDs, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
768
        }
769
770
        // now build an array of objects to be returned
771
        $objects = array();
772
        $count = 0;
773
        $RecordClass = get_class($this->record);
774
775
        while ($row = $result->fetchArray()) {
776
            $obj = new $RecordClass();
777
            $obj->load($row['ID']);
778
            $objects[$count] = $obj;
779
            ++$count;
780
        }
781
782
        self::$logger->debug('<<loadAllByDayUpdated ['.count($objects).']');
783
784
        return $objects;
785
    }
786
787
    /**
788
     * (non-PHPdoc).
789
     *
790
     * @see Alpha\Model\ActiveRecordProviderInterface::loadAllFieldValuesByAttribute()
791
     */
792
    public function loadAllFieldValuesByAttribute($attribute, $value, $returnAttribute, $order = 'ASC', $ignoreClassType = false)
793
    {
794
        self::$logger->debug('>>loadAllFieldValuesByAttribute(attribute=['.$attribute.'], value=['.$value.'], returnAttribute=['.$returnAttribute.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']');
795
796
        if (!$ignoreClassType && $this->record->isTableOverloaded()) {
797
            $sqlQuery = 'SELECT '.$returnAttribute.' FROM '.$this->record->getTableName()." WHERE $attribute = '$value' AND classname = '".addslashes(get_class($this->record))."' ORDER BY ID ".$order.';';
798
        } else {
799
            $sqlQuery = 'SELECT '.$returnAttribute.' FROM '.$this->record->getTableName()." WHERE $attribute = '$value' ORDER BY ID ".$order.';';
800
        }
801
802
        $this->record->setLastQuery($sqlQuery);
803
804
        self::$logger->debug('lastQuery ['.$sqlQuery.']');
805
806
        if (!$result = self::getConnection()->query($sqlQuery)) {
807
            self::$logger->debug('<<loadAllFieldValuesByAttribute');
808
            throw new RecordNotFoundException('Failed to load field ['.$returnAttribute.'] values, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
809
        }
810
811
        // now build an array of attribute values to be returned
812
        $values = array();
813
        $count = 0;
814
815
        while ($row = $result->fetchArray()) {
816
            $values[$count] = $row[$returnAttribute];
817
            ++$count;
818
        }
819
820
        self::$logger->debug('<<loadAllFieldValuesByAttribute ['.count($values).']');
821
822
        return $values;
823
    }
824
825
    /**
826
     * (non-PHPdoc).
827
     *
828
     * @see Alpha\Model\ActiveRecordProviderInterface::save()
829
     */
830
    public function save()
831
    {
832
        self::$logger->debug('>>save()');
833
834
        $config = ConfigProvider::getInstance();
835
        $sessionProvider = $config->get('session.provider.name');
836
        $session = ServiceFactory::getInstance($sessionProvider, 'Alpha\Util\Http\Session\SessionProviderInterface');
837
838
        // get the class attributes
839
        $reflection = new ReflectionClass(get_class($this->record));
840
        $properties = $reflection->getProperties();
841
842
        if ($this->record->getVersion() != $this->record->getVersionNumber()->getValue()) {
843
            throw new LockingException('Could not save the object as it has been updated by another user.  Please try saving again.');
844
        }
845
846
        // set the "updated by" fields, we can only set the user id if someone is logged in
847
        if ($session->get('currentUser') != null) {
848
            $this->record->set('updated_by', $session->get('currentUser')->getID());
849
        }
850
851
        $this->record->set('updated_ts', new Timestamp(date('Y-m-d H:i:s')));
852
853
        // check to see if it is a transient object that needs to be inserted
854
        if ($this->record->isTransient()) {
855
            $savedFields = array();
856
            $sqlQuery = 'INSERT INTO '.$this->record->getTableName().' (';
857
858
            foreach ($properties as $propObj) {
859
                $propName = $propObj->name;
860
                if (!in_array($propName, $this->record->getTransientAttributes())) {
861
                    // Skip the ID, database auto number takes care of this.
862
                    if ($propName != 'ID' && $propName != 'version_num') {
863
                        $sqlQuery .= "$propName,";
864
                        $savedFields[] = $propName;
865
                    }
866
867
                    if ($propName == 'version_num') {
868
                        $sqlQuery .= 'version_num,';
869
                        $savedFields[] = 'version_num';
870
                    }
871
                }
872
            }
873
            if ($this->record->isTableOverloaded()) {
874
                $sqlQuery .= 'classname,';
875
            }
876
877
            $sqlQuery = rtrim($sqlQuery, ',');
878
879
            $sqlQuery .= ') VALUES (';
880
881
            foreach ($savedFields as $savedField) {
882
                $sqlQuery .= ':'.$savedField.',';
883
            }
884
885
            if ($this->record->isTableOverloaded()) {
886
                $sqlQuery .= ':classname,';
887
            }
888
889
            $sqlQuery = rtrim($sqlQuery, ',').')';
890
891
            $this->record->setLastQuery($sqlQuery);
892
            self::$logger->debug('Query ['.$sqlQuery.']');
893
894
            $stmt = self::getConnection()->prepare($sqlQuery);
895
896
            if ($stmt instanceof SQLite3Stmt) {
897
                foreach ($savedFields as $savedField) {
898
                    if ($this->record->get($savedField) instanceof Integer) {
899
                        $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_INTEGER);
900
                    } else {
901
                        $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_TEXT);
902
                    }
903
                }
904
905
                if ($this->record->isTableOverloaded()) {
906
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
907
                }
908
909
                $stmt->bindValue(':version_num', 1, SQLITE3_INTEGER); // on an initial save, this will always be 1
910
                $this->record->set('version_num', 1);
911
912
                try {
913
                    $stmt->execute();
914
                } catch (Exception $e) {
915
                    if (self::getConnection()->lastErrorCode() == 19) {
916
                        throw new ValidationException('Unique key violation while trying to save object, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
917
                    } else {
918
                        throw new FailedSaveException('Failed to save object, exception ['.$e->getMessage().'], DB error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
919
                    }
920
                }
921
            } else {
922
                throw new FailedSaveException('Failed to save object, exception ['.$e->getMessage().'], DB error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
923
            }
924
        } else {
925
            // assume that it is a persistent object that needs to be updated
926
            $savedFields = array();
927
            $sqlQuery = 'UPDATE '.$this->record->getTableName().' SET ';
928
929
            foreach ($properties as $propObj) {
930
                $propName = $propObj->name;
931
                if (!in_array($propName, $this->record->getTransientAttributes())) {
932
                    // Skip the ID, database auto number takes care of this.
933
                    if ($propName != 'ID' && $propName != 'version_num') {
934
                        $sqlQuery .= "$propName = :$propName,";
935
                        $savedFields[] = $propName;
936
                    }
937
938
                    if ($propName == 'version_num') {
939
                        $sqlQuery .= 'version_num = :version_num,';
940
                        $savedFields[] = 'version_num';
941
                    }
942
                }
943
            }
944
945
            if ($this->record->isTableOverloaded()) {
946
                $sqlQuery .= 'classname = :classname,';
947
            }
948
949
            $sqlQuery = rtrim($sqlQuery, ',');
950
951
            $sqlQuery .= ' WHERE ID=:ID;';
952
953
            $this->record->setLastQuery($sqlQuery);
954
            $stmt = self::getConnection()->prepare($sqlQuery);
955
956
            if ($stmt instanceof SQLite3Stmt) {
957
                foreach ($savedFields as $savedField) {
958
                    if ($this->record->get($savedField) instanceof Integer) {
959
                        $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_INTEGER);
960
                    } else {
961
                        $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_TEXT);
962
                    }
963
                }
964
965
                if ($this->record->isTableOverloaded()) {
966
                    $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
967
                }
968
969
                $stmt->bindValue(':ID', $this->record->getID(), SQLITE3_INTEGER);
970
971
                $temp = $this->record->getVersionNumber()->getValue();
972
                $this->record->set('version_num', $temp+1);
973
                $stmt->bindValue(':version_num', $temp+1, SQLITE3_INTEGER);
974
975
                $stmt->execute();
976
            } else {
977
                throw new FailedSaveException('Failed to save object, error is ['.$stmt->error.'], query ['.$this->record->getLastQuery().']');
978
            }
979
        }
980
981
        if ($stmt != null && $stmt != false) {
982
            // populate the updated ID in case we just done an insert
983
            if ($this->record->isTransient()) {
984
                $this->record->setID(self::getConnection()->lastInsertRowID());
985
            }
986
987
            try {
988
                foreach ($properties as $propObj) {
989
                    $propName = $propObj->name;
990
991
                    if ($this->record->getPropObject($propName) instanceof Relation) {
992
                        $prop = $this->record->getPropObject($propName);
993
994
                        // handle the saving of MANY-TO-MANY relation values
995
                        if ($prop->getRelationType() == 'MANY-TO-MANY' && count($prop->getRelatedIDs()) > 0) {
996
                            try {
997
                                try {
998
                                    // check to see if the rel is on this class
999
                                    $side = $prop->getSide(get_class($this->record));
1000
                                } catch (IllegalArguementException $iae) {
1001
                                    $side = $prop->getSide(get_parent_class($this->record));
1002
                                }
1003
1004
                                $lookUp = $prop->getLookup();
1005
1006
                                // first delete all of the old RelationLookup objects for this rel
1007
                                try {
1008
                                    if ($side == 'left') {
1009
                                        $lookUp->deleteAllByAttribute('leftID', $this->record->getID());
1010
                                    } else {
1011
                                        $lookUp->deleteAllByAttribute('rightID', $this->record->getID());
1012
                                    }
1013
                                } catch (Exception $e) {
1014
                                    throw new FailedSaveException('Failed to delete old RelationLookup objects on the table ['.$prop->getLookup()->getTableName().'], error is ['.$e->getMessage().']');
1015
                                }
1016
1017
                                $IDs = $prop->getRelatedIDs();
1018
1019
                                if (isset($IDs) && !empty($IDs[0])) {
1020
                                    // now for each posted ID, create a new RelationLookup record and save
1021
                                    foreach ($IDs as $id) {
1022
                                        $newLookUp = new RelationLookup($lookUp->get('leftClassName'), $lookUp->get('rightClassName'));
1023
                                        if ($side == 'left') {
1024
                                            $newLookUp->set('leftID', $this->record->getID());
1025
                                            $newLookUp->set('rightID', $id);
1026
                                        } else {
1027
                                            $newLookUp->set('rightID', $this->record->getID());
1028
                                            $newLookUp->set('leftID', $id);
1029
                                        }
1030
                                        $newLookUp->save();
1031
                                    }
1032
                                }
1033
                            } catch (Exception $e) {
1034
                                throw new FailedSaveException('Failed to update a MANY-TO-MANY relation on the object, error is ['.$e->getMessage().']');
1035
                            }
1036
                        }
1037
1038
                        // handle the saving of ONE-TO-MANY relation values
1039
                        if ($prop->getRelationType() == 'ONE-TO-MANY') {
1040
                            $prop->setValue($this->record->getID());
1041
                        }
1042
                    }
1043
                }
1044
            } catch (Exception $e) {
1045
                throw new FailedSaveException('Failed to save object, error is ['.$e->getMessage().']');
1046
            }
1047
1048
            $stmt->close();
1049
        } else {
1050
            // there has been an error, so decrement the version number back
1051
            $temp = $this->record->getVersionNumber()->getValue();
1052
            $this->record->set('version_num', $temp-1);
1053
1054
            throw new FailedSaveException('Failed to save object, SQLite error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
1055
        }
1056
1057
        if ($this->record->getMaintainHistory()) {
1058
            $this->record->saveHistory();
1059
        }
1060
    }
1061
1062
    /**
1063
     * (non-PHPdoc).
1064
     *
1065
     * @see Alpha\Model\ActiveRecordProviderInterface::saveAttribute()
1066
     */
1067
    public function saveAttribute($attribute, $value)
1068
    {
1069
        self::$logger->debug('>>saveAttribute(attribute=['.$attribute.'], value=['.$value.'])');
1070
1071
        $config = ConfigProvider::getInstance();
1072
        $sessionProvider = $config->get('session.provider.name');
1073
        $session = ServiceFactory::getInstance($sessionProvider, 'Alpha\Util\Http\Session\SessionProviderInterface');
1074
1075
        if ($this->record->getVersion() != $this->record->getVersionNumber()->getValue()) {
1076
            throw new LockingException('Could not save the object as it has been updated by another user.  Please try saving again.');
1077
        }
1078
1079
        // set the "updated by" fields, we can only set the user id if someone is logged in
1080
        if ($session->get('currentUser') != null) {
1081
            $this->record->set('updated_by', $session->get('currentUser')->getID());
1082
        }
1083
1084
        $this->record->set('updated_ts', new Timestamp(date('Y-m-d H:i:s')));
1085
1086
        // assume that it is a persistent object that needs to be updated
1087
        $sqlQuery = 'UPDATE '.$this->record->getTableName().' SET '.$attribute.'=:attribute, version_num=:version, updated_by=:updated_by, updated_ts=:updated_ts WHERE ID=:ID;';
1088
1089
        $this->record->setLastQuery($sqlQuery);
1090
        $stmt = self::getConnection()->prepare($sqlQuery);
1091
1092
        $newVersionNumber = $this->record->getVersionNumber()->getValue()+1;
1093
1094
        if ($stmt instanceof SQLite3Stmt) {
1095
            if ($this->record->getPropObject($attribute) instanceof Integer) {
1096
                $stmt->bindValue(':attribute', $value, SQLITE3_INTEGER);
1097
            } else {
1098
                $stmt->bindValue(':attribute', $value, SQLITE3_TEXT);
1099
            }
1100
1101
            $updatedBy = $this->record->get('updated_by');
1102
            $updatedTS = $this->record->get('updated_ts');
1103
1104
            $stmt->bindValue(':version', $newVersionNumber, SQLITE3_INTEGER);
1105
            $stmt->bindValue(':updated_by', $updatedBy, SQLITE3_INTEGER);
1106
            $stmt->bindValue(':updated_ts', $updatedTS, SQLITE3_TEXT);
1107
            $stmt->bindValue(':ID', $this->record->getID(), SQLITE3_INTEGER);
1108
1109
            $stmt->execute();
1110
        } else {
1111
            throw new FailedSaveException('Failed to save attribute, error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
1112
        }
1113
1114
        $stmt->close();
1115
1116
        $this->record->set($attribute, $value);
1117
        $this->record->set('version_num', $newVersionNumber);
1118
1119
        if ($this->record->getMaintainHistory()) {
1120
            $this->record->saveHistory();
1121
        }
1122
1123
        self::$logger->debug('<<saveAttribute');
1124
    }
1125
1126
    /**
1127
     * (non-PHPdoc).
1128
     *
1129
     * @see Alpha\Model\ActiveRecordProviderInterface::saveHistory()
1130
     */
1131
    public function saveHistory()
1132
    {
1133
        self::$logger->debug('>>saveHistory()');
1134
1135
        // get the class attributes
1136
        $reflection = new ReflectionClass(get_class($this->record));
1137
        $properties = $reflection->getProperties();
1138
1139
        $savedFields = array();
1140
        $attributeNames = array();
1141
        $attributeValues = array();
1142
1143
        $sqlQuery = 'INSERT INTO '.$this->record->getTableName().'_history (';
1144
1145
        foreach ($properties as $propObj) {
1146
            $propName = $propObj->name;
1147
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1148
                $sqlQuery .= "$propName,";
1149
                $attributeNames[] = $propName;
1150
                $attributeValues[] = $this->record->get($propName);
1151
                $savedFields[] = $propName;
1152
            }
1153
        }
1154
1155
        if ($this->record->isTableOverloaded()) {
1156
            $sqlQuery .= 'classname,';
1157
        }
1158
1159
        $sqlQuery = rtrim($sqlQuery, ',');
1160
1161
        $sqlQuery .= ') VALUES (';
1162
1163
        foreach ($savedFields as $savedField) {
1164
            $sqlQuery .= ':'.$savedField.',';
1165
        }
1166
1167
        if ($this->record->isTableOverloaded()) {
1168
            $sqlQuery .= ':classname,';
1169
        }
1170
1171
        $sqlQuery = rtrim($sqlQuery, ',').')';
1172
1173
        $this->record->setLastQuery($sqlQuery);
1174
        self::$logger->debug('Query ['.$sqlQuery.']');
1175
1176
        $stmt = self::getConnection()->prepare($sqlQuery);
1177
1178
        if ($stmt instanceof SQLite3Stmt) {
1179
            foreach ($savedFields as $savedField) {
1180
                if ($this->record->get($savedField) instanceof Integer) {
1181
                    $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_INTEGER);
1182
                } else {
1183
                    $stmt->bindValue(':'.$savedField, $this->record->get($savedField), SQLITE3_TEXT);
1184
                }
1185
            }
1186
1187
            if ($this->record->isTableOverloaded()) {
1188
                $stmt->bindValue(':classname', get_class($this->record), SQLITE3_TEXT);
1189
            }
1190
1191
            $stmt->execute();
1192
        } else {
1193
            throw new FailedSaveException('Failed to save object history, error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
1194
        }
1195
    }
1196
1197
    /**
1198
     * (non-PHPdoc).
1199
     *
1200
     * @see Alpha\Model\ActiveRecordProviderInterface::delete()
1201
     */
1202
    public function delete()
1203
    {
1204
        self::$logger->debug('>>delete()');
1205
1206
        $sqlQuery = 'DELETE FROM '.$this->record->getTableName().' WHERE ID = :ID;';
1207
1208
        $this->record->setLastQuery($sqlQuery);
1209
1210
        $stmt = self::getConnection()->prepare($sqlQuery);
1211
1212
        if ($stmt instanceof SQLite3Stmt) {
1213
            $stmt->bindValue(':ID', $this->record->getID(), SQLITE3_INTEGER);
1214
            $stmt->execute();
1215
            self::$logger->debug('Deleted the object ['.$this->record->getID().'] of class ['.get_class($this->record).']');
1216
        } else {
1217
            throw new FailedDeleteException('Failed to delete object ['.$this->record->getID().'], error is ['.self::getLastDatabaseError().'], query ['.$this->record->getLastQuery().']');
1218
        }
1219
1220
        $stmt->close();
1221
1222
        self::$logger->debug('<<delete');
1223
    }
1224
1225
    /**
1226
     * (non-PHPdoc).
1227
     *
1228
     * @see Alpha\Model\ActiveRecordProviderInterface::getVersion()
1229
     */
1230
    public function getVersion()
1231
    {
1232
        self::$logger->debug('>>getVersion()');
1233
1234
        $sqlQuery = 'SELECT version_num FROM '.$this->record->getTableName().' WHERE ID = :ID;';
1235
        $this->record->setLastQuery($sqlQuery);
1236
1237
        $stmt = self::getConnection()->prepare($sqlQuery);
1238
1239
        if ($stmt instanceof SQLite3Stmt) {
1240
            $stmt->bindValue(':ID', $this->record->getID(), SQLITE3_INTEGER);
1241
1242
            $result = $stmt->execute();
1243
1244
            // there should only ever be one (or none)
1245
            $row = $result->fetchArray(SQLITE3_ASSOC);
1246
1247
            $stmt->close();
1248
        } else {
1249
            self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']');
1250
            if (!$this->record->checkTableExists()) {
1251
                $this->record->makeTable();
1252
1253
                throw new RecordNotFoundException('Failed to get the version number, table did not exist so had to create!');
1254
            }
1255
1256
            return;
1257
        }
1258
1259
        if (!isset($row['version_num']) || $row['version_num'] < 1) {
1260
            self::$logger->debug('<<getVersion [0]');
1261
1262
            return 0;
1263
        } else {
1264
            $version_num = $row['version_num'];
1265
1266
            self::$logger->debug('<<getVersion ['.$version_num.']');
1267
1268
            return $version_num;
1269
        }
1270
    }
1271
1272
    /**
1273
     * (non-PHPdoc).
1274
     *
1275
     * @see Alpha\Model\ActiveRecordProviderInterface::makeTable()
1276
     */
1277
    public function makeTable()
1278
    {
1279
        self::$logger->debug('>>makeTable()');
1280
1281
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().' (ID INTEGER PRIMARY KEY,';
1282
1283
        // get the class attributes
1284
        $reflection = new ReflectionClass(get_class($this->record));
1285
        $properties = $reflection->getProperties();
1286
1287
        $foreignKeys = array();
1288
1289
        foreach ($properties as $propObj) {
1290
            $propName = $propObj->name;
1291
1292
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1293
                $prop = $this->record->getPropObject($propName);
1294
1295
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1296
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().') NOT NULL,';
1297
                } elseif ($prop instanceof Integer) {
1298
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().'),';
1299
                } elseif ($prop instanceof Double) {
1300
                    $sqlQuery .= "$propName REAL(".$prop->getSize(true).'),';
1301
                } elseif ($prop instanceof SmallText) {
1302
                    $sqlQuery .= "$propName TEXT(".$prop->getSize().'),';
1303
                } elseif ($prop instanceof Text) {
1304
                    $sqlQuery .= "$propName TEXT,";
1305
                } elseif ($prop instanceof Boolean) {
1306
                    $sqlQuery .= "$propName INTEGER(1) DEFAULT '0',";
1307
                } elseif ($prop instanceof Date) {
1308
                    $sqlQuery .= "$propName TEXT,";
1309
                } elseif ($prop instanceof Timestamp) {
1310
                    $sqlQuery .= "$propName TEXT,";
1311
                } elseif ($prop instanceof Enum) {
1312
                    $sqlQuery .= "$propName TEXT,";
1313
                } elseif ($prop instanceof DEnum) {
1314
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1315
                    $denum->saveIfNew();
1316
                    $sqlQuery .= "$propName INTEGER(11),";
1317
                } elseif ($prop instanceof Relation) {
1318
                    $sqlQuery .= "$propName INTEGER(11),";
1319
1320
                    $rel = $this->record->getPropObject($propName);
1321
1322
                    $relatedField = $rel->getRelatedClassField();
1323
                    $relatedClass = $rel->getRelatedClass();
1324
                    $relatedRecord = new $relatedClass();
1325
                    $tableName = $relatedRecord->getTableName();
1326
                    $foreignKeys[$propName] = array($tableName, $relatedField);
1327
                } else {
1328
                    $sqlQuery .= '';
1329
                }
1330
            }
1331
        }
1332
1333
        if ($this->record->isTableOverloaded()) {
1334
            $sqlQuery .= 'classname TEXT(100)';
1335
        } else {
1336
            $sqlQuery = mb_substr($sqlQuery, 0, -1);
1337
        }
1338
1339
        if (count($foreignKeys) > 0) {
1340
            foreach ($foreignKeys as $field => $related) {
1341
                $sqlQuery .= ', FOREIGN KEY ('.$field.') REFERENCES '.$related[0].'('.$related[1].')';
1342
            }
1343
        }
1344
1345
        if (count($this->foreignKeys) > 0) {
1346
            foreach ($this->foreignKeys as $field => $related) {
1347
                $sqlQuery .= ', FOREIGN KEY ('.$field.') REFERENCES '.$related[0].'('.$related[1].')';
1348
            }
1349
        }
1350
1351
        $sqlQuery .= ');';
1352
1353
        $this->record->setLastQuery($sqlQuery);
1354
1355
        if (!self::getConnection()->exec($sqlQuery)) {
1356
            self::$logger->debug('<<makeTable');
1357
            throw new AlphaException('Failed to create the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getLastDatabaseError().']');
1358
        }
1359
1360
        // check the table indexes if any additional ones required
1361
        $this->checkIndexes();
1362
1363
        if ($this->record->getMaintainHistory()) {
1364
            $this->record->makeHistoryTable();
1365
        }
1366
1367
        self::$logger->debug('<<makeTable');
1368
    }
1369
1370
    /**
1371
     * (non-PHPdoc).
1372
     *
1373
     * @see Alpha\Model\ActiveRecordProviderInterface::makeHistoryTable()
1374
     */
1375
    public function makeHistoryTable()
1376
    {
1377
        self::$logger->debug('>>makeHistoryTable()');
1378
1379
        $sqlQuery = 'CREATE TABLE '.$this->record->getTableName().'_history (ID INTEGER NOT NULL,';
1380
1381
        // get the class attributes
1382
        $reflection = new ReflectionClass(get_class($this->record));
1383
        $properties = $reflection->getProperties();
1384
1385
        foreach ($properties as $propObj) {
1386
            $propName = $propObj->name;
1387
1388
            if (!in_array($propName, $this->record->getTransientAttributes()) && $propName != 'ID') {
1389
                $prop = $this->record->getPropObject($propName);
1390
1391
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1392
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().') NOT NULL,';
1393
                } elseif ($prop instanceof Integer) {
1394
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().'),';
1395
                } elseif ($prop instanceof Double) {
1396
                    $sqlQuery .= "$propName REAL(".$prop->getSize(true).'),';
1397
                } elseif ($prop instanceof SmallText) {
1398
                    $sqlQuery .= "$propName TEXT(".$prop->getSize().'),';
1399
                } elseif ($prop instanceof Text) {
1400
                    $sqlQuery .= "$propName TEXT,";
1401
                } elseif ($prop instanceof Boolean) {
1402
                    $sqlQuery .= "$propName INTEGER(1) DEFAULT '0',";
1403
                } elseif ($prop instanceof Date) {
1404
                    $sqlQuery .= "$propName TEXT,";
1405
                } elseif ($prop instanceof Timestamp) {
1406
                    $sqlQuery .= "$propName TEXT,";
1407
                } elseif ($prop instanceof Enum) {
1408
                    $sqlQuery .= "$propName TEXT,";
1409
                } elseif ($prop instanceof DEnum) {
1410
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1411
                    $denum->saveIfNew();
1412
                    $sqlQuery .= "$propName INTEGER(11),";
1413
                } elseif ($prop instanceof Relation) {
1414
                    $sqlQuery .= "$propName INTEGER(11),";
1415
                } else {
1416
                    $sqlQuery .= '';
1417
                }
1418
            }
1419
        }
1420
1421
        if ($this->record->isTableOverloaded()) {
1422
            $sqlQuery .= 'classname TEXT(100),';
1423
        }
1424
1425
        $sqlQuery .= 'PRIMARY KEY (ID, version_num));';
1426
1427
        $this->record->setLastQuery($sqlQuery);
1428
1429
        if (!$result = self::getConnection()->query($sqlQuery)) {
1430
            self::$logger->debug('<<makeHistoryTable');
1431
            throw new AlphaException('Failed to create the table ['.$this->record->getTableName().'_history] for the class ['.get_class($this->record).'], database error is ['.self::getLastDatabaseError().']');
1432
        }
1433
1434
        self::$logger->debug('<<makeHistoryTable');
1435
    }
1436
1437
    /**
1438
     * (non-PHPdoc).
1439
     *
1440
     * @see Alpha\Model\ActiveRecordProviderInterface::rebuildTable()
1441
     */
1442
    public function rebuildTable()
1443
    {
1444
        self::$logger->debug('>>rebuildTable()');
1445
1446
        // the use of "IF EXISTS" here requires SQLite 3.3.0 or above.
1447
        $sqlQuery = 'DROP TABLE IF EXISTS '.$this->record->getTableName().';';
1448
1449
        $this->record->setLastQuery($sqlQuery);
1450
1451
        if (!$result = self::getConnection()->query($sqlQuery)) {
1452
            self::$logger->debug('<<rebuildTable');
1453
            throw new AlphaException('Failed to drop the table ['.$this->record->getTableName().'] for the class ['.get_class($this->record).'], database error is ['.self::getLastDatabaseError().']');
1454
        }
1455
1456
        $this->record->makeTable();
1457
1458
        self::$logger->debug('<<rebuildTable');
1459
    }
1460
1461
    /**
1462
     * (non-PHPdoc).
1463
     *
1464
     * @see Alpha\Model\ActiveRecordProviderInterface::dropTable()
1465
     */
1466
    public function dropTable($tableName = null)
1467
    {
1468
        self::$logger->debug('>>dropTable()');
1469
1470
        if ($tableName === null) {
1471
            $tableName = $this->record->getTableName();
1472
        }
1473
1474
        // the use of "IF EXISTS" here requires SQLite 3.3.0 or above.
1475
        $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.';';
1476
1477
        $this->record->setLastQuery($sqlQuery);
1478
1479
        if (!$result = self::getConnection()->query($sqlQuery)) {
1480
            self::$logger->debug('<<dropTable');
1481
            throw new AlphaException('Failed to drop the table ['.$tableName.'] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1482
        }
1483
1484
        if ($this->record->getMaintainHistory()) {
1485
            $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.'_history;';
1486
1487
            $this->record->setLastQuery($sqlQuery);
1488
1489
            if (!$result = self::getConnection()->query($sqlQuery)) {
1490
                self::$logger->debug('<<dropTable');
1491
                throw new AlphaException('Failed to drop the table ['.$tableName.'_history] for the class ['.get_class($this->record).'], query is ['.$this->record->getLastQuery().']');
1492
            }
1493
        }
1494
1495
        self::$logger->debug('<<dropTable');
1496
    }
1497
1498
    /**
1499
     * (non-PHPdoc).
1500
     *
1501
     * @see Alpha\Model\ActiveRecordProviderInterface::addProperty()
1502
     */
1503
    public function addProperty($propName)
1504
    {
1505
        self::$logger->debug('>>addProperty(propName=['.$propName.'])');
1506
1507
        $sqlQuery = 'ALTER TABLE '.$this->record->getTableName().' ADD ';
1508
1509
        if ($this->isTableOverloaded() && $propName == 'classname') {
1510
            $sqlQuery .= 'classname TEXT(100)';
1511
        } else {
1512
            if (!in_array($propName, $this->record->getDefaultAttributes()) && !in_array($propName, $this->record->getTransientAttributes())) {
1513
                $prop = $this->record->getPropObject($propName);
1514
1515
                if ($prop instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) {
1516
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().') NOT NULL';
1517
                } elseif ($prop instanceof Integer) {
1518
                    $sqlQuery .= "$propName INTEGER(".$prop->getSize().')';
1519
                } elseif ($prop instanceof Double) {
1520
                    $sqlQuery .= "$propName REAL(".$prop->getSize(true).')';
1521
                } elseif ($prop instanceof SmallText) {
1522
                    $sqlQuery .= "$propName TEXT(".$prop->getSize().')';
1523
                } elseif ($prop instanceof Text) {
1524
                    $sqlQuery .= "$propName TEXT";
1525
                } elseif ($prop instanceof Boolean) {
1526
                    $sqlQuery .= "$propName INTEGER(1) DEFAULT '0'";
1527
                } elseif ($prop instanceof Date) {
1528
                    $sqlQuery .= "$propName TEXT";
1529
                } elseif ($prop instanceof Timestamp) {
1530
                    $sqlQuery .= "$propName TEXT";
1531
                } elseif ($prop instanceof Enum) {
1532
                    $sqlQuery .= "$propName TEXT";
1533
                } elseif ($prop instanceof DEnum) {
1534
                    $denum = new DEnum(get_class($this->record).'::'.$propName);
1535
                    $denum->saveIfNew();
1536
                    $sqlQuery .= "$propName INTEGER(11)";
1537
                } elseif ($prop instanceof Relation) {
1538
                    $sqlQuery .= "$propName INTEGER(11)";
1539
                } else {
1540
                    $sqlQuery .= '';
1541
                }
1542
            }
1543
        }
1544
1545
        $this->record->setLastQuery($sqlQuery);
1546
1547
        if (!$result = self::getConnection()->query($sqlQuery)) {
1548
            self::$logger->debug('<<addProperty');
1549
            throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'], query is ['.$this->record->getLastQuery().']');
1550
        } else {
1551
            self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'] table for the class ['.get_class($this->record).']');
1552
        }
1553
1554
        if ($this->record->getMaintainHistory()) {
1555
            $sqlQuery = str_replace($this->record->getTableName(), $this->record->getTableName().'_history', $sqlQuery);
1556
1557
            if (!$result = self::getConnection()->query($sqlQuery)) {
1558
                self::$logger->debug('<<addProperty');
1559
                throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->record->getTableName().'_history], query is ['.$this->record->getLastQuery().']');
1560
            } else {
1561
                self::$logger->info('Successfully added the ['.$propName.'] column onto the ['.$this->record->getTableName().'_history] table for the class ['.get_class($this->record).']');
1562
            }
1563
        }
1564
1565
        self::$logger->debug('<<addProperty');
1566
    }
1567
1568
    /**
1569
     * (non-PHPdoc).
1570
     *
1571
     * @see Alpha\Model\ActiveRecordProviderInterface::getMAX()
1572
     */
1573
    public function getMAX()
1574
    {
1575
        self::$logger->debug('>>getMAX()');
1576
1577
        $sqlQuery = 'SELECT MAX(ID) AS max_ID FROM '.$this->record->getTableName();
1578
1579
        $this->record->setLastQuery($sqlQuery);
1580
1581
        try {
1582
            $result = $this->record->query($sqlQuery);
1583
1584
            $row = $result[0];
1585
1586
            if (isset($row['max_ID'])) {
1587
                self::$logger->debug('<<getMAX ['.$row['max_ID'].']');
1588
1589
                return $row['max_ID'];
1590
            } else {
1591
                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().']');
1592
            }
1593
        } catch (Exception $e) {
1594
            self::$logger->debug('<<getMAX');
1595
            throw new AlphaException($e->getMessage());
1596
        }
1597
    }
1598
1599
    /**
1600
     * (non-PHPdoc).
1601
     *
1602
     * @see Alpha\Model\ActiveRecordProviderInterface::getCount()
1603
     */
1604
    public function getCount($attributes = array(), $values = array())
1605
    {
1606
        self::$logger->debug('>>getCount(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'])');
1607
1608
        if ($this->record->isTableOverloaded()) {
1609
            $whereClause = ' WHERE classname = \''.get_class($this->record).'\' AND';
1610
        } else {
1611
            $whereClause = ' WHERE';
1612
        }
1613
1614
        $count = count($attributes);
1615
1616
        for ($i = 0; $i < $count; ++$i) {
1617
            $whereClause .= ' '.$attributes[$i].' = \''.$values[$i].'\' AND';
1618
            self::$logger->debug($whereClause);
1619
        }
1620
        // remove the last " AND"
1621
        $whereClause = mb_substr($whereClause, 0, -4);
1622
1623
        if ($whereClause != ' WHERE') {
1624
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName().$whereClause;
1625
        } else {
1626
            $sqlQuery = 'SELECT COUNT(ID) AS class_count FROM '.$this->record->getTableName();
1627
        }
1628
1629
        $this->record->setLastQuery($sqlQuery);
1630
1631
        if (!$result = self::getConnection()->query($sqlQuery)) {
1632
            self::$logger->debug('<<getCount');
1633
            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().']');
1634
        } else {
1635
            $row = $result->fetchArray(SQLITE3_ASSOC);
1636
1637
            self::$logger->debug('<<getCount ['.$row['class_count'].']');
1638
1639
            return $row['class_count'];
1640
        }
1641
    }
1642
1643
    /**
1644
     * (non-PHPdoc).
1645
     *
1646
     * @see Alpha\Model\ActiveRecordProviderInterface::getHistoryCount()
1647
     */
1648
    public function getHistoryCount()
1649
    {
1650
        self::$logger->debug('>>getHistoryCount()');
1651
1652
        if (!$this->record->getMaintainHistory()) {
1653
            throw new AlphaException('getHistoryCount method called on a DAO where no history is maintained!');
1654
        }
1655
1656
        $sqlQuery = 'SELECT COUNT(ID) AS object_count FROM '.$this->record->getTableName().'_history WHERE ID='.$this->record->getID();
1657
1658
        $this->record->setLastQuery($sqlQuery);
1659
        self::$logger->debug('query ['.$sqlQuery.']');
1660
1661
        if (!$result = self::getConnection()->query($sqlQuery)) {
1662
            self::$logger->debug('<<getHistoryCount');
1663
            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().']');
1664
        } else {
1665
            $row = $result->fetchArray(SQLITE3_ASSOC);
1666
1667
            self::$logger->debug('<<getHistoryCount ['.$row['object_count'].']');
1668
1669
            return $row['object_count'];
1670
        }
1671
    }
1672
1673
    /**
1674
     * Given that Enum values are not saved in the database for SQLite, an implementation is not required here.
1675
     *
1676
     * (non-PHPdoc)
1677
     *
1678
     * @see Alpha\Model\ActiveRecordProviderInterface::setEnumOptions()
1679
     *
1680
     * @throws \Alpha\Exception\NotImplementedException
1681
     */
1682
    public function setEnumOptions()
1683
    {
1684
        throw new NotImplementedException('ActiveRecordProviderInterface::setEnumOptions() not implemented by the SQLite3 provider');
1685
    }
1686
1687
    /**
1688
     * (non-PHPdoc).
1689
     *
1690
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableExists()
1691
     */
1692
    public function checkTableExists($checkHistoryTable = false)
1693
    {
1694
        self::$logger->debug('>>checkTableExists(checkHistoryTable=['.$checkHistoryTable.'])');
1695
1696
        $tableExists = false;
1697
1698
        $sqlQuery = 'SELECT name FROM sqlite_master WHERE type = "table";';
1699
        $this->record->setLastQuery($sqlQuery);
1700
1701
        $result = self::getConnection()->query($sqlQuery);
1702
1703
        $tableName = ($checkHistoryTable ? $this->record->getTableName().'_history' : $this->record->getTableName());
1704
1705
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1706
            if (strtolower($row['name']) == mb_strtolower($tableName)) {
1707
                $tableExists = true;
1708
            }
1709
        }
1710
1711
        if ($result) {
1712
            self::$logger->debug('<<checkTableExists ['.$tableExists.']');
1713
1714
            return $tableExists;
1715
        } else {
1716
            self::$logger->debug('<<checkTableExists');
1717
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1718
        }
1719
    }
1720
1721
    /**
1722
     * (non-PHPdoc).
1723
     *
1724
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordTableExists()
1725
     */
1726
    public static function checkRecordTableExists($RecordClassName, $checkHistoryTable = false)
1727
    {
1728
        if (self::$logger == null) {
1729
            self::$logger = new Logger('ActiveRecordProviderSQLite');
1730
        }
1731
        self::$logger->debug('>>checkRecordTableExists(RecordClassName=['.$RecordClassName.'], checkHistoryTable=['.$checkHistoryTable.'])');
1732
1733
        if (!class_exists($RecordClassName)) {
1734
            throw new IllegalArguementException('The classname provided ['.$checkHistoryTable.'] is not defined!');
1735
        }
1736
1737
        $tableName = $RecordClassName::TABLE_NAME;
1738
1739
        if (empty($tableName)) {
1740
            $tableName = mb_substr($RecordClassName, 0, mb_strpos($RecordClassName, '_'));
1741
        }
1742
1743
        if ($checkHistoryTable) {
1744
            $tableName .= '_history';
1745
        }
1746
1747
        $tableExists = false;
1748
1749
        $sqlQuery = 'SELECT name FROM sqlite_master WHERE type = "table";';
1750
1751
        $result = self::getConnection()->query($sqlQuery);
1752
1753
        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1754
            if ($row['name'] == $tableName) {
1755
                $tableExists = true;
1756
            }
1757
        }
1758
1759
        if ($result) {
1760
            self::$logger->debug('<<checkRecordTableExists ['.($tableExists ? 'true' : 'false').']');
1761
1762
            return $tableExists;
1763
        } else {
1764
            self::$logger->debug('<<checkRecordTableExists');
1765
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1766
        }
1767
    }
1768
1769
    /**
1770
     * (non-PHPdoc).
1771
     *
1772
     * @see Alpha\Model\ActiveRecordProviderInterface::checkTableNeedsUpdate()
1773
     */
1774
    public function checkTableNeedsUpdate()
1775
    {
1776
        self::$logger->debug('>>checkTableNeedsUpdate()');
1777
1778
        if (!$this->record->checkTableExists()) {
1779
            return false;
1780
        }
1781
1782
        $updateRequired = false;
1783
1784
        $matchCount = 0;
1785
1786
        $query = 'PRAGMA table_info('.$this->record->getTableName().')';
1787
        $result = self::getConnection()->query($query);
1788
        $this->record->setLastQuery($query);
1789
1790
        // get the class attributes
1791
        $reflection = new ReflectionClass(get_class($this->record));
1792
        $properties = $reflection->getProperties();
1793
1794
        foreach ($properties as $propObj) {
1795
            $propName = $propObj->name;
1796
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1797
                $foundMatch = false;
1798
1799
                while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1800
                    if ($propName == $row['name']) {
1801
                        $foundMatch = true;
1802
                        break;
1803
                    }
1804
                }
1805
1806
                if (!$foundMatch) {
1807
                    --$matchCount;
1808
                }
1809
1810
                $result->reset();
1811
            }
1812
        }
1813
1814
        // check for the "classname" field in overloaded tables
1815
        if ($this->record->isTableOverloaded()) {
1816
            $foundMatch = false;
1817
1818
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1819
                if ('classname' == $row['name']) {
1820
                    $foundMatch = true;
1821
                    break;
1822
                }
1823
            }
1824
            if (!$foundMatch) {
1825
                --$matchCount;
1826
            }
1827
        }
1828
1829
        if ($matchCount != 0) {
1830
            $updateRequired = true;
1831
        }
1832
1833
        if (!$result) {
1834
            self::$logger->debug('<<checkTableNeedsUpdate');
1835
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1836
        } else {
1837
            // check the table indexes
1838
            try {
1839
                $this->checkIndexes();
1840
            } catch (AlphaException $ae) {
1841
                self::$logger->warn("Error while checking database indexes:\n\n".$ae->getMessage());
1842
            }
1843
1844
            self::$logger->debug('<<checkTableNeedsUpdate ['.$updateRequired.']');
1845
1846
            return $updateRequired;
1847
        }
1848
    }
1849
1850
    /**
1851
     * (non-PHPdoc).
1852
     *
1853
     * @see Alpha\Model\ActiveRecordProviderInterface::findMissingFields()
1854
     */
1855
    public function findMissingFields()
1856
    {
1857
        self::$logger->debug('>>findMissingFields()');
1858
1859
        $missingFields = array();
1860
        $matchCount = 0;
1861
1862
        $sqlQuery = 'PRAGMA table_info('.$this->record->getTableName().')';
1863
        $result = self::getConnection()->query($sqlQuery);
1864
        $this->record->setLastQuery($sqlQuery);
1865
1866
        // get the class attributes
1867
        $reflection = new ReflectionClass(get_class($this->record));
1868
        $properties = $reflection->getProperties();
1869
1870
        foreach ($properties as $propObj) {
1871
            $propName = $propObj->name;
1872
            if (!in_array($propName, $this->record->getTransientAttributes())) {
1873
                while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1874
                    if ($propName == $row['name']) {
1875
                        ++$matchCount;
1876
                        break;
1877
                    }
1878
                }
1879
                $result->reset();
1880
            } else {
1881
                ++$matchCount;
1882
            }
1883
1884
            if ($matchCount == 0) {
1885
                array_push($missingFields, $propName);
1886
            } else {
1887
                $matchCount = 0;
1888
            }
1889
        }
1890
1891
        // check for the "classname" field in overloaded tables
1892
        if ($this->record->isTableOverloaded()) {
1893
            $foundMatch = false;
1894
1895
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1896
                if ('classname' == $row['name']) {
1897
                    $foundMatch = true;
1898
                    break;
1899
                }
1900
            }
1901
            if (!$foundMatch) {
1902
                array_push($missingFields, 'classname');
1903
            }
1904
        }
1905
1906
        if (!$result) {
1907
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1908
        }
1909
1910
        self::$logger->debug('<<findMissingFields ['.var_export($missingFields, true).']');
1911
1912
        return $missingFields;
1913
    }
1914
1915
    /**
1916
     * (non-PHPdoc).
1917
     *
1918
     * @see Alpha\Model\ActiveRecordProviderInterface::getIndexes()
1919
     */
1920
    public function getIndexes()
1921
    {
1922
        self::$logger->debug('>>getIndexes()');
1923
1924
        $sqlQuery = "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='".$this->record->getTableName()."'";
1925
1926
        $this->record->setLastQuery($sqlQuery);
1927
1928
        $indexNames = array();
1929
1930
        if (!$result = self::getConnection()->query($sqlQuery)) {
1931
            throw new AlphaException('Failed to access the system database correctly, error is ['.self::getLastDatabaseError().']');
1932
        } else {
1933
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1934
                array_push($indexNames, $row['name']);
1935
            }
1936
        }
1937
1938
        // in SQLite foreign keys are not stored in sqlite_master, so we have to run a different query and append the results
1939
        $sqlQuery = 'PRAGMA foreign_key_list('.$this->record->getTableName().')';
1940
        
1941
        $this->record->setLastQuery($sqlQuery);
1942
1943
        if (!$result = self::getConnection()->query($sqlQuery)) {
1944
            self::$logger->warn('Error during pragma table foreign key lookup ['.self::getLastDatabaseError().']');
1945
        } else {
1946
            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
1947
                // SQLite does not name FK indexes, so we will return a fake name based the same convention used in MySQL
1948
                $fakeIndexName = $this->record->getTableName().'_'.$row['from'].'_fk_idx';
1949
                array_push($indexNames, $fakeIndexName);
1950
            }
1951
        }
1952
1953
        self::$logger->debug('<<getIndexes');
1954
1955
        return $indexNames;
1956
    }
1957
1958
    /**
1959
     * Checks to see if all of the indexes are in place for the record's table, creates those that are missing.
1960
     *
1961
     * @since 1.2
1962
     */
1963
    private function checkIndexes()
1964
    {
1965
        self::$logger->debug('>>checkIndexes()');
1966
1967
        $indexNames = $this->record->getIndexes();
1968
1969
        // process unique keys
1970
        foreach ($this->record->getUniqueAttributes() as $prop) {
1971
            // check for composite indexes
1972
            if (mb_strpos($prop, '+')) {
1973
                $attributes = explode('+', $prop);
1974
1975
                $index_exists = false;
1976
                foreach ($indexNames as $index) {
1977
                    if ($attributes[0].'_'.$attributes[1].'_unq_idx' == $index) {
1978
                        $index_exists = true;
1979
                    }
1980
                    if (count($attributes) == 3) {
1981
                        if ($attributes[0].'_'.$attributes[1].'_'.$attributes[2].'_unq_idx' == $index) {
1982
                            $index_exists = true;
1983
                        }
1984
                    }
1985
                }
1986
1987
                if (!$index_exists) {
1988
                    if (count($attributes) == 3) {
1989
                        $this->record->createUniqueIndex($attributes[0], $attributes[1], $attributes[2]);
1990
                    } else {
1991
                        $this->record->createUniqueIndex($attributes[0], $attributes[1]);
1992
                    }
1993
                }
1994
            } else {
1995
                $index_exists = false;
1996
                foreach ($indexNames as $index) {
1997
                    if ($prop.'_unq_idx' == $index) {
1998
                        $index_exists = true;
1999
                    }
2000
                }
2001
2002
                if (!$index_exists) {
2003
                    $this->createUniqueIndex($prop);
2004
                }
2005
            }
2006
        }
2007
2008
        self::$logger->debug('<<checkIndexes');
2009
    }
2010
2011
    /**
2012
     * Note that SQLite 3.6.19 is requrired for foreign key support.
2013
     *
2014
     * (non-PHPdoc)
2015
     *
2016
     * @see Alpha\Model\ActiveRecordProviderInterface::createForeignIndex()
2017
     */
2018
    public function createForeignIndex($attributeName, $relatedClass, $relatedClassAttribute, $indexName = null)
2019
    {
2020
        self::$logger->info('>>createForeignIndex(attributeName=['.$attributeName.'], relatedClass=['.$relatedClass.'], relatedClassAttribute=['.$relatedClassAttribute.'], indexName=['.$indexName.']');
2021
2022
        /*
2023
         * High-level approach
2024
         *
2025
         * 1. Rename the source table to [tablename]_temp
2026
         * 2. Create a new [tablename] table, with the new FK in place.
2027
         * 3. Copy all of the data from [tablename]_temp to [tablename].
2028
         * 4. Drop [tablename]_temp.
2029
         */
2030
        try {
2031
            ActiveRecord::begin($this->record);
2032
2033
            // rename the table to [tablename]_temp
2034
            $query = 'ALTER TABLE '.$this->record->getTableName().' RENAME TO '.$this->record->getTableName().'_temp;';
2035
            $this->record->setLastQuery($query);
2036
            self::getConnection()->query($query);
2037
2038
            self::$logger->info('Renamed the table ['.$this->record->getTableName().'] to ['.$this->record->getTableName().'_temp]');
2039
2040
            // now create the new table with the FK in place
2041
            $record = new $relatedClass();
2042
            $tableName = $record->getTableName();
2043
            $this->foreignKeys[$attributeName] = array($tableName, $relatedClassAttribute);
2044
2045
            $this->makeTable();
2046
2047
            self::$logger->info('Made a new copy of the table ['.$this->record->getTableName().']');
2048
2049
            // copy all of the old data to the new table
2050
            $query = 'INSERT INTO '.$this->record->getTableName().' SELECT * FROM '.$this->record->getTableName().'_temp;';
2051
            $this->record->setLastQuery($query);
2052
            self::getConnection()->query($query);
2053
2054
            self::$logger->info('Copied all of the data from ['.$this->record->getTableName().'] to ['.$this->record->getTableName().'_temp]');
2055
2056
            // finally, drop the _temp table and commit the changes
2057
            $this->record->dropTable($this->record->getTableName().'_temp');
2058
2059
            self::$logger->info('Dropped the table ['.$this->record->getTableName().'_temp]');
2060
2061
            ActiveRecord::commit($this->record);
2062
        } catch (Exception $e) {
2063
            ActiveRecord::rollback($this->record);
2064
2065
            throw new FailedIndexCreateException('Failed to create the index ['.$attributeName.'] on ['.$this->record->getTableName().'], error is ['.$e->getMessage().'], query ['.$this->record->getLastQuery().']');
2066
        }
2067
2068
        self::$logger->info('<<createForeignIndex');
2069
    }
2070
2071
    /**
2072
     * (non-PHPdoc).
2073
     *
2074
     * @see Alpha\Model\ActiveRecordProviderInterface::createUniqueIndex()
2075
     */
2076
    public function createUniqueIndex($attribute1Name, $attribute2Name = '', $attribute3Name = '')
2077
    {
2078
        self::$logger->debug('>>createUniqueIndex(attribute1Name=['.$attribute1Name.'], attribute2Name=['.$attribute2Name.'], attribute3Name=['.$attribute3Name.'])');
2079
2080
        $sqlQuery = '';
2081
2082
        if ($attribute2Name != '' && $attribute3Name != '') {
2083
            $sqlQuery = 'CREATE UNIQUE INDEX IF NOT EXISTS '.$attribute1Name.'_'.$attribute2Name.'_'.$attribute3Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.','.$attribute2Name.','.$attribute3Name.');';
2084
        }
2085
2086
        if ($attribute2Name != '' && $attribute3Name == '') {
2087
            $sqlQuery = 'CREATE UNIQUE INDEX IF NOT EXISTS '.$attribute1Name.'_'.$attribute2Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.','.$attribute2Name.');';
2088
        }
2089
2090
        if ($attribute2Name == '' && $attribute3Name == '') {
2091
            $sqlQuery = 'CREATE UNIQUE INDEX IF NOT EXISTS '.$attribute1Name.'_unq_idx ON '.$this->record->getTableName().' ('.$attribute1Name.');';
2092
        }
2093
2094
        $this->record->setLastQuery($sqlQuery);
2095
2096
        $result = self::getConnection()->query($sqlQuery);
2097
2098
        if ($result) {
2099
            self::$logger->debug('Successfully created the unique index on ['.$this->record->getTableName().']');
2100
        } else {
2101
            throw new FailedIndexCreateException('Failed to create the unique index on ['.$this->record->getTableName().'], error is ['.self::getConnection()->lastErrorMsg().']');
2102
        }
2103
2104
        self::$logger->debug('<<createUniqueIndex');
2105
    }
2106
2107
    /**
2108
     * (non-PHPdoc).
2109
     *
2110
     * @see Alpha\Model\ActiveRecordProviderInterface::reload()
2111
     */
2112
    public function reload()
2113
    {
2114
        self::$logger->debug('>>reload()');
2115
2116
        if (!$this->record->isTransient()) {
2117
            $this->record->load($this->record->getID());
2118
        } else {
2119
            throw new AlphaException('Cannot reload transient object from database!');
2120
        }
2121
2122
        self::$logger->debug('<<reload');
2123
    }
2124
2125
    /**
2126
     * (non-PHPdoc).
2127
     *
2128
     * @see Alpha\Model\ActiveRecordProviderInterface::checkRecordExists()
2129
     */
2130
    public function checkRecordExists($ID)
2131
    {
2132
        self::$logger->debug('>>checkRecordExists(ID=['.$ID.'])');
2133
2134
        $sqlQuery = 'SELECT ID FROM '.$this->record->getTableName().' WHERE ID = :ID;';
2135
        $this->record->setLastQuery($sqlQuery);
2136
        $stmt = self::getConnection()->prepare($sqlQuery);
2137
2138
        if ($stmt instanceof SQLite3Stmt) {
2139
            $stmt->bindValue(':ID', $ID, SQLITE3_INTEGER);
2140
2141
            $result = $stmt->execute();
2142
2143
            // there should only ever be one (or none)
2144
            $row = $result->fetchArray(SQLITE3_ASSOC);
2145
2146
            $stmt->close();
2147
        } else {
2148
            self::$logger->debug('<<checkRecordExists');
2149
            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().']');
2150
        }
2151
2152
        if (!isset($row['ID'])) {
2153
            self::$logger->debug('<<checkRecordExists [false]');
2154
2155
            return false;
2156
        } else {
2157
            self::$logger->debug('<<checkRecordExists [true]');
2158
2159
            return true;
2160
        }
2161
    }
2162
2163
    /**
2164
     * (non-PHPdoc).
2165
     *
2166
     * @see Alpha\Model\ActiveRecordProviderInterface::isTableOverloaded()
2167
     */
2168
    public function isTableOverloaded()
2169
    {
2170
        self::$logger->debug('>>isTableOverloaded()');
2171
2172
        $reflection = new ReflectionClass($this->record);
2173
        $classname = $reflection->getShortName();
2174
        $tablename = ucfirst($this->record->getTableName());
2175
2176
        // use reflection to check to see if we are dealing with a persistent type (e.g. DEnum) which are never overloaded
2177
        $implementedInterfaces = $reflection->getInterfaces();
2178
2179
        foreach ($implementedInterfaces as $interface) {
2180
            if ($interface->name == 'Alpha\Model\Type\TypeInterface') {
2181
                self::$logger->debug('<<isTableOverloaded [false]');
2182
2183
                return false;
2184
            }
2185
        }
2186
2187
        if ($classname != $tablename) {
2188
            // loop over all records to see if there is one using the same table as this record
2189
2190
            $Recordclasses = ActiveRecord::getRecordClassNames();
2191
2192
            foreach ($Recordclasses as $RecordclassName) {
2193
                $reflection = new ReflectionClass($RecordclassName);
2194
                $classname = $reflection->getShortName();
2195
                if ($tablename == $classname) {
2196
                    self::$logger->debug('<<isTableOverloaded [true]');
2197
2198
                    return true;
2199
                }
2200
            }
2201
            self::$logger->debug('<<isTableOverloaded');
2202
            throw new BadTableNameException('The table name ['.$tablename.'] for the class ['.$classname.'] is invalid as it does not match a Record definition in the system!');
2203
        } else {
2204
            // check to see if there is already a "classname" column in the database for this record
2205
            $sqlQuery = 'PRAGMA table_info('.$this->record->getTableName().')';
2206
            $result = self::getConnection()->query($sqlQuery);
2207
            $this->record->setLastQuery($sqlQuery);
2208
2209
            if (!$result) {
2210
                self::$logger->warn('Error during pragma table info lookup ['.self::getLastDatabaseError().']');
2211
            } else {
2212
                while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
2213
                    if ('classname' == $row['name']) {
2214
                        self::$logger->debug('<<isTableOverloaded [true]');
2215
2216
                        return true;
2217
                    }
2218
                }
2219
            }
2220
2221
            self::$logger->debug('<<isTableOverloaded [false]');
2222
2223
            return false;
2224
        }
2225
    }
2226
2227
    /**
2228
     * (non-PHPdoc).
2229
     *
2230
     * @see Alpha\Model\ActiveRecordProviderInterface::begin()
2231
     */
2232
    public static function begin()
2233
    {
2234
        if (self::$logger == null) {
2235
            self::$logger = new Logger('ActiveRecordProviderSQLite');
2236
        }
2237
        self::$logger->debug('>>begin()');
2238
2239
        if (!self::getConnection()->exec('BEGIN')) {
2240
            throw new AlphaException('Error beginning a new transaction, error is ['.self::getLastDatabaseError().']');
2241
        }
2242
2243
        self::$logger->debug('<<begin');
2244
    }
2245
2246
    /**
2247
     * (non-PHPdoc).
2248
     *
2249
     * @see Alpha\Model\ActiveRecordProviderInterface::commit()
2250
     */
2251
    public static function commit()
2252
    {
2253
        if (self::$logger == null) {
2254
            self::$logger = new Logger('ActiveRecordProviderSQLite');
2255
        }
2256
        self::$logger->debug('>>commit()');
2257
2258
        if (!self::getConnection()->exec('COMMIT')) {
2259
            throw new AlphaException('Error commiting a transaction, error is ['.self::getLastDatabaseError().']');
2260
        }
2261
2262
        self::$logger->debug('<<commit');
2263
    }
2264
2265
    /**
2266
     * (non-PHPdoc).
2267
     *
2268
     * @see Alpha\Model\ActiveRecordProviderInterface::rollback()
2269
     */
2270
    public static function rollback()
2271
    {
2272
        if (self::$logger == null) {
2273
            self::$logger = new Logger('ActiveRecordProviderSQLite');
2274
        }
2275
2276
        self::$logger->debug('>>rollback()');
2277
2278
        try {
2279
            self::getConnection()->exec('ROLLBACK');
2280
            self::disconnect();
2281
        } catch (Exception $e) {
2282
            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
2283
                throw new AlphaException('Error rolling back a transaction, error is ['.self::getLastDatabaseError().']');
2284
            }
2285
        }
2286
2287
        self::$logger->debug('<<rollback');
2288
    }
2289
2290
    /**
2291
     * (non-PHPdoc).
2292
     *
2293
     * @see Alpha\Model\ActiveRecordProviderInterface::setRecord()
2294
     */
2295
    public function setRecord($Record)
2296
    {
2297
        $this->record = $Record;
2298
    }
2299
2300
    /**
2301
     * (non-PHPdoc).
2302
     *
2303
     * @see Alpha\Model\ActiveRecordProviderInterface::checkDatabaseExists()
2304
     */
2305
    public static function checkDatabaseExists()
2306
    {
2307
        $config = ConfigProvider::getInstance();
2308
2309
        return file_exists($config->get('db.file.path'));
2310
    }
2311
2312
    /**
2313
     * (non-PHPdoc).
2314
     *
2315
     * @see Alpha\Model\ActiveRecordProviderInterface::createDatabase()
2316
     */
2317
    public static function createDatabase()
2318
    {
2319
        $config = ConfigProvider::getInstance();
2320
2321
        if (!self::checkDatabaseExists()) {
2322
            fopen($config->get('db.file.path'), 'x+');
2323
        }
2324
    }
2325
2326
    /**
2327
     * (non-PHPdoc).
2328
     *
2329
     * @see Alpha\Model\ActiveRecordProviderInterface::dropDatabase()
2330
     */
2331
    public static function dropDatabase()
2332
    {
2333
        $config = ConfigProvider::getInstance();
2334
2335
        if (self::checkDatabaseExists()) {
2336
            unlink($config->get('db.file.path'));
2337
        }
2338
    }
2339
2340
    /**
2341
     * (non-PHPdoc).
2342
     *
2343
     * @see Alpha\Model\ActiveRecordProviderInterface::backupDatabase()
2344
     */
2345
    public static function backupDatabase($targetFile)
2346
    {
2347
        $config = ConfigProvider::getInstance();
2348
2349
        exec('sqlite3 '.$config->get('db.file.path').' ".backup '.$targetFile.'"');
2350
    }
2351
}
2352