Issues (5)

src/Model/Model.php (5 issues)

1
<?php
2
3
namespace Freshsauce\Model;
4
5
/**
6
 * Model ORM
7
 *
8
 *
9
 *  A simple database abstraction layer for PHP 5.3+ with very minor configuration required
10
 *
11
 * database table columns are auto detected and made available as public members of the class
12
 * provides CRUD, dynamic counters/finders on a database table
13
 * uses PDO for data access and exposes PDO if required
14
 * class members used to do the magic are preceeded with an underscore, be careful of column names starting with _ in your database!
15
 * requires php >=5.3 as uses "Late Static Binding" and namespaces
16
 *
17
 *
18
 * @property string $created_at optional datatime in table that will automatically get updated on insert
19
 * @property string $updated_at optional datatime in table that will automatically get updated on insert/update
20
 *
21
 * @package default
22
 */
23
24
/**
25
 * Class Model
26
 *
27
 * @package Freshsauce\Model
28
 */
29
class Model
30
{
31
32
    // Class configuration
33
34
    /**
35
     * @var \PDO
36
     */
37
    public static $_db; // all models inherit this db connection
38
    // but can overide in a sub-class by calling subClass::connectDB(...)
39
    // sub class must also redeclare public static $_db;
40
41
    /**
42
     * @var \PDOStatement[]
43
     */
44
    protected static $_stmt = array(); // prepared statements cache
45
46
    /**
47
     * @var string
48
     */
49
    protected static $_identifier_quote_character; // character used to quote table & columns names
50
51
    /**
52
     * @var array
53
     */
54
    private static $_tableColumns = array(); // columns in database table populated dynamically
55
    // objects public members are created for each table columns dynamically
56
57
    /**
58
     * @var \stdClass all data is stored here
59
     */
60
    protected $data;
61
62
    /**
63
     * @var \stdClass whether a field value has changed (become dirty) is stored here
64
     */
65
    protected $dirty;
66
67
    /**
68
     * @var string primary key column name, set as appropriate in your sub-class
69
     */
70
    protected static $_primary_column_name = 'id'; // primary key column
71
72
    /**
73
     * @var string database table name, set as appropriate in your sub-class
74
     */
75
    protected static $_tableName = '_the_db_table_name_'; // database table name
76
77
    /**
78
     * Model constructor.
79
     *
80
     * @param array $data
81
     */
82
    public function __construct($data = array())
83
    {
84
        static::getFieldnames(); // only called once first time an object is created
85
        $this->clearDirtyFields();
86
        if (is_array($data)) {
0 ignored issues
show
The condition is_array($data) is always true.
Loading history...
87
            $this->hydrate($data);
88
        }
89
    }
90
91
    /**
92
     * check if this object has data attached
93
     *
94
     * @return bool
95
     */
96
    public function hasData()
97
    {
98
        return is_object($this->data);
99
    }
100
101
102
    /**
103
     * Returns true if data present else throws an Exception
104
     *
105
     * @return bool
106
     * @throws \Exception
107
     */
108
    public function dataPresent()
109
    {
110
        if (!$this->hasData()) {
111
            throw new \Exception('No data');
112
        }
113
114
        return true;
115
    }
116
117
    /**
118
     * Set field in data object if doesnt match a native object member
119
     * Initialise the data store if not an object
120
     *
121
     * @param string $name
122
     * @param mixed  $value
123
     *
124
     * @return void
125
     */
126
    public function __set($name, $value)
127
    {
128
        if (!$this->hasData()) {
129
            $this->data = new \stdClass();
130
        }
131
        $this->data->$name = $value;
132
        $this->markFieldDirty($name);
133
    }
134
135
    /**
136
     * Mark the field as dirty, so it will be set in inserts and updates
137
     *
138
     * @param string $name
139
     */
140
    public function markFieldDirty($name)
141
    {
142
        $this->dirty->$name = true; // field became dirty
143
    }
144
145
    /**
146
     * Return true if filed is dirty else false
147
     *
148
     * @param string $name
149
     *
150
     * @return bool
151
     */
152
    public function isFieldDirty($name)
153
    {
154
        return isset($this->dirty->$name) && ($this->dirty->$name == true);
155
    }
156
157
    /**
158
     * resets what fields have been considered dirty ie. been changed without being saved to the db
159
     */
160
    public function clearDirtyFields()
161
    {
162
        $this->dirty = new \stdClass();
163
    }
164
165
    /**
166
     * Try and get the object member from the data object
167
     * if it doesnt match a native object member
168
     *
169
     * @param string $name
170
     *
171
     * @return mixed
172
     * @throws \Exception
173
     */
174
    public function __get($name)
175
    {
176
        if (!$this->hasData()) {
177
            throw new \Exception("data property=$name has not been initialised", 1);
178
        }
179
180
        if (property_exists($this->data, $name)) {
181
            return $this->data->$name;
182
        }
183
184
        $trace = debug_backtrace();
185
        throw new \Exception(
186
            'Undefined property via __get(): ' . $name .
187
            ' in ' . $trace[0]['file'] .
188
            ' on line ' . $trace[0]['line'],
189
            1
190
        );
191
    }
192
193
    /**
194
     * Test the existence of the object member from the data object
195
     * if it doesnt match a native object member
196
     *
197
     * @param string $name
198
     *
199
     * @return bool
200
     */
201
    public function __isset($name)
202
    {
203
        if ($this->hasData() && property_exists($this->data, $name)) {
204
            return true;
205
        }
206
207
        return false;
208
    }
209
210
    /**
211
     * set the db connection for this and all sub-classes to use
212
     * if a sub class overrides $_db it can have it's own db connection if required
213
     * params are as new PDO(...)
214
     * set PDO to throw exceptions on error
215
     *
216
     * @param string $dsn
217
     * @param string $username
218
     * @param string $password
219
     * @param array  $driverOptions
220
     *
221
     * @throws \Exception
222
     */
223
    public static function connectDb($dsn, $username, $password, $driverOptions = array())
224
    {
225
        static::$_db = new \PDO($dsn, $username, $password, $driverOptions);
226
        static::$_db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); // Set Errorhandling to Exception
227
        static::_setup_identifier_quote_character();
228
    }
229
230
    /**
231
     * Detect and initialise the character used to quote identifiers
232
     * (table names, column names etc).
233
     *
234
     * @return void
235
     * @throws \Exception
236
     */
237
    public static function _setup_identifier_quote_character()
238
    {
239
        if (is_null(static::$_identifier_quote_character)) {
0 ignored issues
show
The condition is_null(static::_identifier_quote_character) is always false.
Loading history...
240
            static::$_identifier_quote_character = static::_detect_identifier_quote_character();
241
        }
242
    }
243
244
    /**
245
     * Return the correct character used to quote identifiers (table
246
     * names, column names etc) by looking at the driver being used by PDO.
247
     *
248
     * @return string
249
     * @throws \Exception
250
     */
251
    protected static function _detect_identifier_quote_character()
252
    {
253
        switch (static::getDriverName()) {
254
            case 'pgsql':
255
            case 'sqlsrv':
256
            case 'dblib':
257
            case 'mssql':
258
            case 'sybase':
259
                return '"';
260
            case 'mysql':
261
            case 'sqlite':
262
            case 'sqlite2':
263
            default:
264
                return '`';
265
        }
266
    }
267
268
    /**
269
     * return the driver name for the current database connection
270
     *
271
     * @return string
272
     * @throws \Exception
273
     */
274
    protected static function getDriverName()
275
    {
276
        if (!static::$_db) {
277
            throw new \Exception('No database connection setup');
278
        }
279
        return static::$_db->getAttribute(\PDO::ATTR_DRIVER_NAME);
280
    }
281
282
    /**
283
     * Quote a string that is used as an identifier
284
     * (table names, column names etc). This method can
285
     * also deal with dot-separated identifiers eg table.column
286
     *
287
     * @param string $identifier
288
     *
289
     * @return string
290
     */
291
    protected static function _quote_identifier($identifier)
292
    {
293
        $class = get_called_class();
294
        $parts = explode('.', $identifier);
295
        $parts = array_map(array(
296
            $class,
297
            '_quote_identifier_part'
298
        ), $parts);
299
        return join('.', $parts);
300
    }
301
302
303
    /**
304
     * This method performs the actual quoting of a single
305
     * part of an identifier, using the identifier quote
306
     * character specified in the config (or autodetected).
307
     *
308
     * @param string $part
309
     *
310
     * @return string
311
     */
312
    protected static function _quote_identifier_part($part)
313
    {
314
        if ($part === '*') {
315
            return $part;
316
        }
317
        return static::$_identifier_quote_character . $part . static::$_identifier_quote_character;
318
    }
319
320
    /**
321
     * Get and cache on first call the column names assocaited with the current table
322
     *
323
     * @return array of column names for the current table
324
     */
325
    protected static function getFieldnames()
326
    {
327
        $class = get_called_class();
328
        if (!isset(self::$_tableColumns[$class])) {
329
            $st                          = static::execute('DESCRIBE ' . static::_quote_identifier(static::$_tableName));
330
            self::$_tableColumns[$class] = $st->fetchAll(\PDO::FETCH_COLUMN);
331
        }
332
        return self::$_tableColumns[$class];
333
    }
334
335
    /**
336
     * Given an associative array of key value pairs
337
     * set the corresponding member value if associated with a table column
338
     * ignore keys which dont match a table column name
339
     *
340
     * @return void
341
     */
342
    public function hydrate($data)
343
    {
344
        foreach (static::getFieldnames() as $fieldname) {
345
            if (isset($data[$fieldname])) {
346
                $this->$fieldname = $data[$fieldname];
347
            } else if (!isset($this->$fieldname)) { // PDO pre populates fields before calling the constructor, so dont null unless not set
348
                $this->$fieldname = null;
349
            }
350
        }
351
    }
352
353
    /**
354
     * set all members to null that are associated with table columns
355
     *
356
     * @return void
357
     */
358
    public function clear()
359
    {
360
        foreach (static::getFieldnames() as $fieldname) {
361
            $this->$fieldname = null;
362
        }
363
        $this->clearDirtyFields();
364
    }
365
366
    /**
367
     * @return array
368
     */
369
    public function __sleep()
370
    {
371
        return static::getFieldnames();
372
    }
373
374
    /**
375
     * @return array
376
     */
377
    public function toArray()
378
    {
379
        $a = array();
380
        foreach (static::getFieldnames() as $fieldname) {
381
            $a[$fieldname] = $this->$fieldname;
382
        }
383
        return $a;
384
    }
385
386
    /**
387
     * Get the record with the matching primary key
388
     *
389
     * @param string $id
390
     *
391
     * @return Object
392
     */
393
    static public function getById($id)
394
    {
395
        return static::fetchOneWhere(static::_quote_identifier(static::$_primary_column_name) . ' = ?', array($id));
396
    }
397
398
    /**
399
     * Get the first record in the table
400
     *
401
     * @return Object
402
     */
403
    static public function first()
404
    {
405
        return static::fetchOneWhere('1=1 ORDER BY ' . static::_quote_identifier(static::$_primary_column_name) . ' ASC');
406
    }
407
408
    /**
409
     * Get the last record in the table
410
     *
411
     * @return Object
412
     */
413
    static public function last()
414
    {
415
        return static::fetchOneWhere('1=1 ORDER BY ' . static::_quote_identifier(static::$_primary_column_name) . ' DESC');
416
    }
417
418
    /**
419
     * Find records with the matching primary key
420
     *
421
     * @param string $id
422
     *
423
     * @return object[] of objects for matching records
424
     */
425
    static public function find($id)
426
    {
427
        $find_by_method = 'find_by_' . (static::$_primary_column_name);
428
        static::$find_by_method($id);
429
    }
430
431
    /**
432
     * handles calls to non-existant static methods, used to implement dynamic finder and counters ie.
433
     *  find_by_name('tom')
434
     *  find_by_title('a great book')
435
     *  count_by_name('tom')
436
     *  count_by_title('a great book')
437
     *
438
     * @param string $name
439
     * @param string $arguments
440
     *
441
     * @return mixed int|object[]|object
442
     * @throws \Exception
443
     */
444
    static public function __callStatic($name, $arguments)
445
    {
446
        // Note: value of $name is case sensitive.
447
        if (preg_match('/^find_by_/', $name) == 1) {
448
            // it's a find_by_{fieldname} dynamic method
449
            $fieldname = substr($name, 8); // remove find by
450
            $match     = $arguments[0];
451
            return static::fetchAllWhereMatchingSingleField($fieldname, $match);
452
        } else if (preg_match('/^findOne_by_/', $name) == 1) {
453
            // it's a findOne_by_{fieldname} dynamic method
454
            $fieldname = substr($name, 11); // remove findOne_by_
455
            $match     = $arguments[0];
456
            return static::fetchOneWhereMatchingSingleField($fieldname, $match, 'ASC');
457
        } else if (preg_match('/^first_by_/', $name) == 1) {
458
            // it's a first_by_{fieldname} dynamic method
459
            $fieldname = substr($name, 9); // remove first_by_
460
            $match     = $arguments[0];
461
            return static::fetchOneWhereMatchingSingleField($fieldname, $match, 'ASC');
462
        } else if (preg_match('/^last_by_/', $name) == 1) {
463
            // it's a last_by_{fieldname} dynamic method
464
            $fieldname = substr($name, 8); // remove last_by_
465
            $match     = $arguments[0];
466
            return static::fetchOneWhereMatchingSingleField($fieldname, $match, 'DESC');
467
        } else if (preg_match('/^count_by_/', $name) == 1) {
468
            // it's a count_by_{fieldname} dynamic method
469
            $fieldname = substr($name, 9); // remove find by
470
            $match     = $arguments[0];
471
            if (is_array($match)) {
472
                return static::countAllWhere(static::_quote_identifier($fieldname) . ' IN (' . static::createInClausePlaceholders($match) . ')', $match);
473
            } else {
474
                return static::countAllWhere(static::_quote_identifier($fieldname) . ' = ?', array($match));
475
            }
476
        }
477
        throw new \Exception(__CLASS__ . ' not such static method[' . $name . ']');
478
    }
479
480
    /**
481
     * find one match based on a single field and match criteria
482
     *
483
     * @param string       $fieldname
484
     * @param string|array $match
485
     * @param string       $order ASC|DESC
486
     *
487
     * @return object of calling class
488
     */
489
    public static function fetchOneWhereMatchingSingleField($fieldname, $match, $order)
490
    {
491
        if (is_array($match)) {
492
            return static::fetchOneWhere(static::_quote_identifier($fieldname) . ' IN (' . static::createInClausePlaceholders($match) . ') ORDER BY ' . static::_quote_identifier($fieldname) . ' ' . $order, $match);
493
        } else {
494
            return static::fetchOneWhere(static::_quote_identifier($fieldname) . ' = ? ORDER BY ' . static::_quote_identifier($fieldname) . ' ' . $order, array($match));
495
        }
496
    }
497
498
499
    /**
500
     * find multiple matches based on a single field and match criteria
501
     *
502
     * @param string       $fieldname
503
     * @param string|array $match
504
     *
505
     * @return object[] of objects of calling class
506
     */
507
    public static function fetchAllWhereMatchingSingleField($fieldname, $match)
508
    {
509
        if (is_array($match)) {
510
            return static::fetchAllWhere(static::_quote_identifier($fieldname) . ' IN (' . static::createInClausePlaceholders($match) . ')', $match);
511
        } else {
512
            return static::fetchAllWhere(static::_quote_identifier($fieldname) . ' = ?', array($match));
513
        }
514
    }
515
516
    /**
517
     * for a given array of params to be passed to an IN clause return a string placeholder
518
     *
519
     * @param array $params
520
     *
521
     * @return string
522
     */
523
    static public function createInClausePlaceholders($params)
524
    {
525
        return implode(',', array_fill(0, count($params), '?'));
526
    }
527
528
    /**
529
     * returns number of rows in the table
530
     *
531
     * @return int
532
     */
533
    static public function count()
534
    {
535
        $st = static::execute('SELECT COUNT(*) FROM ' . static::_quote_identifier(static::$_tableName));
536
        return (int)$st->fetchColumn(0);
537
    }
538
539
    /**
540
     * returns an integer count of matching rows
541
     *
542
     * @param string $SQLfragment conditions, grouping to apply (to right of WHERE keyword)
543
     * @param array  $params      optional params to be escaped and injected into the SQL query (standrd PDO syntax)
544
     *
545
     * @return integer count of rows matching conditions
546
     */
547
    static public function countAllWhere($SQLfragment = '', $params = array())
548
    {
549
        $SQLfragment = self::addWherePrefix($SQLfragment);
550
        $st          = static::execute('SELECT COUNT(*) FROM ' . static::_quote_identifier(static::$_tableName) . $SQLfragment, $params);
551
        return (int)$st->fetchColumn(0);
552
    }
553
554
    /**
555
     * if $SQLfragment is not empty prefix with the WHERE keyword
556
     *
557
     * @param string $SQLfragment
558
     *
559
     * @return string
560
     */
561
    static protected function addWherePrefix($SQLfragment)
562
    {
563
        return $SQLfragment ? ' WHERE ' . $SQLfragment : $SQLfragment;
564
    }
565
566
567
    /**
568
     * returns an array of objects of the sub-class which match the conditions
569
     *
570
     * @param string $SQLfragment conditions, sorting, grouping and limit to apply (to right of WHERE keywords)
571
     * @param array  $params      optional params to be escaped and injected into the SQL query (standrd PDO syntax)
572
     * @param bool   $limitOne    if true the first match will be returned
573
     *
574
     * @return mixed object[]|object of objects of calling class
575
     */
576
    static public function fetchWhere($SQLfragment = '', $params = array(), $limitOne = false)
577
    {
578
        $class       = get_called_class();
579
        $SQLfragment = self::addWherePrefix($SQLfragment);
580
        $st          = static::execute(
581
            'SELECT * FROM ' . static::_quote_identifier(static::$_tableName) . $SQLfragment . ($limitOne ? ' LIMIT 1' : ''),
582
            $params
583
        );
584
        $st->setFetchMode(\PDO::FETCH_ASSOC);
585
        if ($limitOne) {
586
            $instance = new $class($st->fetch());
587
            $instance->clearDirtyFields();
588
            return $instance;
589
        }
590
        $results = [];
591
        while ($row = $st->fetch()) {
592
            $instance = new $class($row);
593
            $instance->clearDirtyFields();
594
            $results[] = $instance;
595
        }
596
        return $results;
597
    }
598
599
    /**
600
     * returns an array of objects of the sub-class which match the conditions
601
     *
602
     * @param string $SQLfragment conditions, sorting, grouping and limit to apply (to right of WHERE keywords)
603
     * @param array  $params      optional params to be escaped and injected into the SQL query (standrd PDO syntax)
604
     *
605
     * @return object[] of objects of calling class
606
     */
607
    static public function fetchAllWhere($SQLfragment = '', $params = array())
608
    {
609
        return static::fetchWhere($SQLfragment, $params, false);
610
    }
611
612
    /**
613
     * returns an object of the sub-class which matches the conditions
614
     *
615
     * @param string $SQLfragment conditions, sorting, grouping and limit to apply (to right of WHERE keywords)
616
     * @param array  $params      optional params to be escaped and injected into the SQL query (standrd PDO syntax)
617
     *
618
     * @return object of calling class
619
     */
620
    static public function fetchOneWhere($SQLfragment = '', $params = array())
621
    {
622
        return static::fetchWhere($SQLfragment, $params, true);
623
    }
624
625
    /**
626
     * Delete a record by its primary key
627
     *
628
     * @return boolean indicating success
629
     */
630
    static public function deleteById($id)
631
    {
632
        $st = static::execute(
633
            'DELETE FROM ' . static::_quote_identifier(static::$_tableName) . ' WHERE ' . static::_quote_identifier(static::$_primary_column_name) . ' = ? LIMIT 1',
634
            array($id)
635
        );
636
        return ($st->rowCount() == 1);
637
    }
638
639
    /**
640
     * Delete the current record
641
     *
642
     * @return boolean indicating success
643
     */
644
    public function delete()
645
    {
646
        return self::deleteById($this->{static::$_primary_column_name});
647
    }
648
649
    /**
650
     * Delete records based on an SQL conditions
651
     *
652
     * @param string $where  SQL fragment of conditions
653
     * @param array  $params optional params to be escaped and injected into the SQL query (standrd PDO syntax)
654
     *
655
     * @return \PDOStatement
656
     */
657
    static public function deleteAllWhere($where, $params = array())
658
    {
659
        $st = static::execute(
660
            'DELETE FROM ' . static::_quote_identifier(static::$_tableName) . ' WHERE ' . $where,
661
            $params
662
        );
663
        return $st;
664
    }
665
666
    /**
667
     * do any validation in this function called before update and insert
668
     * should throw errors on validation failure.
669
     *
670
     * @return boolean true or throws exception on error
671
     */
672
    static public function validate()
673
    {
674
        return true;
675
    }
676
677
    /**
678
     * insert a row into the database table, and update the primary key field with the one generated on insert
679
     *
680
     * @param boolean $autoTimestamp      true by default will set updated_at & created_at fields if present
681
     * @param boolean $allowSetPrimaryKey if true include primary key field in insert (ie. you want to set it yourself)
682
     *
683
     * @return boolean indicating success
684
     */
685
    public function insert($autoTimestamp = true, $allowSetPrimaryKey = false)
686
    {
687
        $pk      = static::$_primary_column_name;
688
        $timeStr = gmdate('Y-m-d H:i:s');
689
        if ($autoTimestamp && in_array('created_at', static::getFieldnames())) {
690
            $this->created_at = $timeStr;
0 ignored issues
show
Bug Best Practice introduced by
The property created_at does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
691
        }
692
        if ($autoTimestamp && in_array('updated_at', static::getFieldnames())) {
693
            $this->updated_at = $timeStr;
0 ignored issues
show
Bug Best Practice introduced by
The property updated_at does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
694
        }
695
        $this->validate();
696
        if ($allowSetPrimaryKey !== true) {
697
            $this->$pk = null; // ensure id is null
698
        }
699
        $set   = $this->setString(!$allowSetPrimaryKey);
700
        $query = 'INSERT INTO ' . static::_quote_identifier(static::$_tableName) . ' SET ' . $set['sql'];
701
        $st    = static::execute($query, $set['params']);
702
        if ($st->rowCount() == 1) {
703
            $this->{static::$_primary_column_name} = static::$_db->lastInsertId();
704
            $this->clearDirtyFields();
705
        }
706
        return ($st->rowCount() == 1);
707
    }
708
709
    /**
710
     * update the current record
711
     *
712
     * @param boolean $autoTimestamp true by default will set updated_at field if present
713
     *
714
     * @return boolean indicating success
715
     */
716
    public function update($autoTimestamp = true)
717
    {
718
        if ($autoTimestamp && in_array('updated_at', static::getFieldnames())) {
719
            $this->updated_at = gmdate('Y-m-d H:i:s');
0 ignored issues
show
Bug Best Practice introduced by
The property updated_at does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
720
        }
721
        $this->validate();
722
        $set             = $this->setString();
723
        $query           = 'UPDATE ' . static::_quote_identifier(static::$_tableName) . ' SET ' . $set['sql'] . ' WHERE ' . static::_quote_identifier(static::$_primary_column_name) . ' = ? LIMIT 1';
724
        $set['params'][] = $this->{static::$_primary_column_name};
725
        $st              = static::execute(
726
            $query,
727
            $set['params']
728
        );
729
        if ($st->rowCount() == 1) {
730
            $this->clearDirtyFields();
731
        }
732
        return ($st->rowCount() == 1);
733
    }
734
735
    /**
736
     * execute
737
     * convenience function for setting preparing and running a database query
738
     * which also uses the statement cache
739
     *
740
     * @param string $query  database statement with parameter place holders as PDO driver
741
     * @param array  $params array of parameters to replace the placeholders in the statement
742
     *
743
     * @return \PDOStatement handle
744
     */
745
    public static function execute($query, $params = array())
746
    {
747
        $st = static::_prepare($query);
748
        $st->execute($params);
749
        return $st;
750
    }
751
752
    /**
753
     * prepare an SQL query via PDO
754
     *
755
     * @param string $query
756
     *
757
     * @return \PDOStatement
758
     */
759
    protected static function _prepare($query)
760
    {
761
        if (!isset(static::$_stmt[$query])) {
762
            // cache prepared query if not seen before
763
            static::$_stmt[$query] = static::$_db->prepare($query);
764
        }
765
        return static::$_stmt[$query]; // return cache copy
766
    }
767
768
    /**
769
     * call update if primary key field is present, else call insert
770
     *
771
     * @return boolean indicating success
772
     */
773
    public function save()
774
    {
775
        if ($this->{static::$_primary_column_name}) {
776
            return $this->update();
777
        } else {
778
            return $this->insert();
779
        }
780
    }
781
782
    /**
783
     * Create an SQL fragment to be used after the SET keyword in an SQL UPDATE
784
     * escaping parameters as necessary.
785
     * by default the primary key is not added to the SET string, but passing $ignorePrimary as false will add it
786
     *
787
     * @param boolean $ignorePrimary
788
     *
789
     * @return array ['sql' => string, 'params' => mixed[] ]
790
     */
791
    protected function setString($ignorePrimary = true)
792
    {
793
        // escapes and builds mysql SET string returning false, empty string or `field` = 'val'[, `field` = 'val']...
794
        /**
795
         * @var array $fragments individual SQL assignments
796
         */
797
        $fragments = array();
798
        /**
799
         * @var array $params values in order to insert into SQl assignment fragments
800
         */
801
        $params = [];
802
        foreach (static::getFieldnames() as $field) {
803
            if ($ignorePrimary && $field == static::$_primary_column_name) {
804
                continue;
805
            }
806
            if (isset($this->$field) && $this->isFieldDirty($field)) { // Only if dirty
807
                if ($this->$field === null) {
808
                    // if empty set to NULL
809
                    $fragments[] = static::_quote_identifier($field) . ' = NULL';
810
                } else {
811
                    // Just set value normally as not empty string with NULL allowed
812
                    $fragments[] = static::_quote_identifier($field) . ' = ?';
813
                    $params[]    = $this->$field;
814
                }
815
            }
816
        }
817
        $sqlFragment = implode(", ", $fragments);
818
        return [
819
            'sql'    => $sqlFragment,
820
            'params' => $params
821
        ];
822
    }
823
824
    /**
825
     * convert a date string or timestamp into a string suitable for assigning to a SQl datetime or timestamp field
826
     *
827
     * @param mixed $dt a date string or a unix timestamp
828
     *
829
     * @return string
830
     */
831
    public static function datetimeToMysqldatetime($dt)
832
    {
833
        $dt = (is_string($dt)) ? strtotime($dt) : $dt;
834
        return date('Y-m-d H:i:s', $dt);
835
    }
836
}
837
838
839