Completed
Branch TASK/update-about-page (5cee29)
by
unknown
34:34 queued 26:08
created

EE_Base_Class::updateFieldsInDB()   B

Complexity

Conditions 7
Paths 20

Size

Total Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 20
nop 2
dl 0
loc 75
rs 7.6121
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
use EventEspresso\core\exceptions\InvalidDataTypeException;
4
use EventEspresso\core\exceptions\InvalidInterfaceException;
5
6
/**
7
 * EE_Base_Class class
8
 *
9
 * @package     Event Espresso
10
 * @subpackage  includes/classes/EE_Base_Class.class.php
11
 * @author      Michael Nelson
12
 */
13
abstract class EE_Base_Class
14
{
15
16
    /**
17
     * This is an array of the original properties and values provided during construction
18
     * of this model object. (keys are model field names, values are their values).
19
     * This list is important to remember so that when we are merging data from the db, we know
20
     * which values to override and which to not override.
21
     *
22
     * @var array
23
     */
24
    protected $_props_n_values_provided_in_constructor;
25
26
    /**
27
     * Timezone
28
     * This gets set by the "set_timezone()" method so that we know what timezone incoming strings|timestamps are in.
29
     * This can also be used before a get to set what timezone you want strings coming out of the object to be in.  NOT
30
     * all EE_Base_Class child classes use this property but any that use a EE_Datetime_Field data type will have
31
     * access to it.
32
     *
33
     * @var string
34
     */
35
    protected $_timezone;
36
37
    /**
38
     * date format
39
     * pattern or format for displaying dates
40
     *
41
     * @var string $_dt_frmt
42
     */
43
    protected $_dt_frmt;
44
45
    /**
46
     * time format
47
     * pattern or format for displaying time
48
     *
49
     * @var string $_tm_frmt
50
     */
51
    protected $_tm_frmt;
52
53
    /**
54
     * This property is for holding a cached array of object properties indexed by property name as the key.
55
     * The purpose of this is for setting a cache on properties that may have calculated values after a
56
     * prepare_for_get.  That way the cache can be checked first and the calculated property returned instead of having
57
     * to recalculate. Used by _set_cached_property() and _get_cached_property() methods.
58
     *
59
     * @var array
60
     */
61
    protected $_cached_properties = array();
62
63
    /**
64
     * An array containing keys of the related model, and values are either an array of related mode objects or a
65
     * single
66
     * related model object. see the model's _model_relations. The keys should match those specified. And if the
67
     * relation is of type EE_Belongs_To (or one of its children), then there should only be ONE related model object,
68
     * all others have an array)
69
     *
70
     * @var array
71
     */
72
    protected $_model_relations = array();
73
74
    /**
75
     * Array where keys are field names (see the model's _fields property) and values are their values. To see what
76
     * their types should be, look at what that field object returns on its prepare_for_get and prepare_for_set methods)
77
     *
78
     * @var array
79
     */
80
    protected $_fields = array();
81
82
    /**
83
     * @var boolean indicating whether or not this model object is intended to ever be saved
84
     * For example, we might create model objects intended to only be used for the duration
85
     * of this request and to be thrown away, and if they were accidentally saved
86
     * it would be a bug.
87
     */
88
    protected $_allow_persist = true;
89
90
    /**
91
     * @var boolean indicating whether or not this model object's properties have changed since construction
92
     */
93
    protected $_has_changes = false;
94
95
    /**
96
     * @var EEM_Base
97
     */
98
    protected $_model;
99
100
    /**
101
     * This is a cache of results from custom selections done on a query that constructs this entity. The only purpose
102
     * for these values is for retrieval of the results, they are not further queryable and they are not persisted to
103
     * the db.  They also do not automatically update if there are any changes to the data that produced their results.
104
     * The format is a simple array of field_alias => field_value.  So for instance if a custom select was something
105
     * like,  "Select COUNT(Registration.REG_ID) as Registration_Count ...", then the resulting value will be in this
106
     * array as:
107
     * array(
108
     *  'Registration_Count' => 24
109
     * );
110
     * Note: if the custom select configuration for the query included a data type, the value will be in the data type
111
     * provided for the query (@see EventEspresso\core\domain\values\model\CustomSelects::__construct phpdocs for more
112
     * info)
113
     *
114
     * @var array
115
     */
116
    protected $custom_selection_results = array();
117
118
119
    /**
120
     * basic constructor for Event Espresso classes, performs any necessary initialization, and verifies it's children
121
     * play nice
122
     *
123
     * @param array   $fieldValues                             where each key is a field (ie, array key in the 2nd
124
     *                                                         layer of the model's _fields array, (eg, EVT_ID,
125
     *                                                         TXN_amount, QST_name, etc) and values are their values
126
     * @param boolean $bydb                                    a flag for setting if the class is instantiated by the
127
     *                                                         corresponding db model or not.
128
     * @param string  $timezone                                indicate what timezone you want any datetime fields to
129
     *                                                         be in when instantiating a EE_Base_Class object.
130
     * @param array   $date_formats                            An array of date formats to set on construct where first
131
     *                                                         value is the date_format and second value is the time
132
     *                                                         format.
133
     * @throws InvalidArgumentException
134
     * @throws InvalidInterfaceException
135
     * @throws InvalidDataTypeException
136
     * @throws EE_Error
137
     * @throws ReflectionException
138
     */
139
    protected function __construct($fieldValues = array(), $bydb = false, $timezone = '', $date_formats = array())
140
    {
141
        $className = get_class($this);
142
        do_action("AHEE__{$className}__construct", $this, $fieldValues);
143
        $model = $this->get_model();
144
        $model_fields = $model->field_settings(false);
145
        // ensure $fieldValues is an array
146
        $fieldValues = is_array($fieldValues) ? $fieldValues : array($fieldValues);
147
        // verify client code has not passed any invalid field names
148
        foreach ($fieldValues as $field_name => $field_value) {
149
            if (! isset($model_fields[ $field_name ])) {
150
                throw new EE_Error(
151
                    sprintf(
152
                        esc_html__(
153
                            'Invalid field (%s) passed to constructor of %s. Allowed fields are :%s',
154
                            'event_espresso'
155
                        ),
156
                        $field_name,
157
                        get_class($this),
158
                        implode(', ', array_keys($model_fields))
159
                    )
160
                );
161
            }
162
        }
163
        $this->_timezone = EEH_DTT_Helper::get_valid_timezone_string($timezone);
164
        if (! empty($date_formats) && is_array($date_formats)) {
165
            list($this->_dt_frmt, $this->_tm_frmt) = $date_formats;
166
        } else {
167
            // set default formats for date and time
168
            $this->_dt_frmt = (string) get_option('date_format', 'Y-m-d');
169
            $this->_tm_frmt = (string) get_option('time_format', 'g:i a');
170
        }
171
        // if db model is instantiating
172
        if ($bydb) {
173
            // client code has indicated these field values are from the database
174 View Code Duplication
            foreach ($model_fields as $fieldName => $field) {
175
                $this->set_from_db(
176
                    $fieldName,
177
                    isset($fieldValues[ $fieldName ]) ? $fieldValues[ $fieldName ] : null
178
                );
179
            }
180
        } else {
181
            // we're constructing a brand
182
            // new instance of the model object. Generally, this means we'll need to do more field validation
183 View Code Duplication
            foreach ($model_fields as $fieldName => $field) {
184
                $this->set(
185
                    $fieldName,
186
                    isset($fieldValues[ $fieldName ]) ? $fieldValues[ $fieldName ] : null,
187
                    true
188
                );
189
            }
190
        }
191
        // remember what values were passed to this constructor
192
        $this->_props_n_values_provided_in_constructor = $fieldValues;
193
        // remember in entity mapper
194
        if (! $bydb && $model->has_primary_key_field() && $this->ID()) {
195
            $model->add_to_entity_map($this);
196
        }
197
        // setup all the relations
198
        foreach ($model->relation_settings() as $relation_name => $relation_obj) {
199
            if ($relation_obj instanceof EE_Belongs_To_Relation) {
200
                $this->_model_relations[ $relation_name ] = null;
201
            } else {
202
                $this->_model_relations[ $relation_name ] = array();
203
            }
204
        }
205
        /**
206
         * Action done at the end of each model object construction
207
         *
208
         * @param EE_Base_Class $this the model object just created
209
         */
210
        do_action('AHEE__EE_Base_Class__construct__finished', $this);
211
    }
212
213
214
    /**
215
     * Gets whether or not this model object is allowed to persist/be saved to the database.
216
     *
217
     * @return boolean
218
     */
219
    public function allow_persist()
220
    {
221
        return $this->_allow_persist;
222
    }
223
224
225
    /**
226
     * Sets whether or not this model object should be allowed to be saved to the DB.
227
     * Normally once this is set to FALSE you wouldn't set it back to TRUE, unless
228
     * you got new information that somehow made you change your mind.
229
     *
230
     * @param boolean $allow_persist
231
     * @return boolean
232
     */
233
    public function set_allow_persist($allow_persist)
234
    {
235
        return $this->_allow_persist = $allow_persist;
236
    }
237
238
239
    /**
240
     * Gets the field's original value when this object was constructed during this request.
241
     * This can be helpful when determining if a model object has changed or not
242
     *
243
     * @param string $field_name
244
     * @return mixed|null
245
     * @throws ReflectionException
246
     * @throws InvalidArgumentException
247
     * @throws InvalidInterfaceException
248
     * @throws InvalidDataTypeException
249
     * @throws EE_Error
250
     */
251
    public function get_original($field_name)
252
    {
253
        if (isset($this->_props_n_values_provided_in_constructor[ $field_name ])
254
            && $field_settings = $this->get_model()->field_settings_for($field_name)
255
        ) {
256
            return $field_settings->prepare_for_get($this->_props_n_values_provided_in_constructor[ $field_name ]);
257
        }
258
        return null;
259
    }
260
261
262
    /**
263
     * @param EE_Base_Class $obj
264
     * @return string
265
     */
266
    public function get_class($obj)
267
    {
268
        return get_class($obj);
269
    }
270
271
272
    /**
273
     * Overrides parent because parent expects old models.
274
     * This also doesn't do any validation, and won't work for serialized arrays
275
     *
276
     * @param    string $field_name
277
     * @param    mixed  $field_value
278
     * @param bool      $use_default
279
     * @throws InvalidArgumentException
280
     * @throws InvalidInterfaceException
281
     * @throws InvalidDataTypeException
282
     * @throws EE_Error
283
     * @throws ReflectionException
284
     * @throws ReflectionException
285
     * @throws ReflectionException
286
     */
287
    public function set($field_name, $field_value, $use_default = false)
288
    {
289
        // if not using default and nothing has changed, and object has already been setup (has ID),
290
        // then don't do anything
291
        if (! $use_default
292
            && $this->_fields[ $field_name ] === $field_value
293
            && $this->ID()
294
        ) {
295
            return;
296
        }
297
        $model = $this->get_model();
298
        $this->_has_changes = true;
299
        $field_obj = $model->field_settings_for($field_name);
300
        if ($field_obj instanceof EE_Model_Field_Base) {
301
            // if ( method_exists( $field_obj, 'set_timezone' )) {
302
            if ($field_obj instanceof EE_Datetime_Field) {
303
                $field_obj->set_timezone($this->_timezone);
304
                $field_obj->set_date_format($this->_dt_frmt);
305
                $field_obj->set_time_format($this->_tm_frmt);
306
            }
307
            $holder_of_value = $field_obj->prepare_for_set($field_value);
308
            // should the value be null?
309
            if (($field_value === null || $holder_of_value === null || $holder_of_value === '') && $use_default) {
310
                $this->_fields[ $field_name ] = $field_obj->get_default_value();
311
                /**
312
                 * To save having to refactor all the models, if a default value is used for a
313
                 * EE_Datetime_Field, and that value is not null nor is it a DateTime
314
                 * object.  Then let's do a set again to ensure that it becomes a DateTime
315
                 * object.
316
                 *
317
                 * @since 4.6.10+
318
                 */
319
                if ($field_obj instanceof EE_Datetime_Field
320
                    && $this->_fields[ $field_name ] !== null
321
                    && ! $this->_fields[ $field_name ] instanceof DateTime
322
                ) {
323
                    empty($this->_fields[ $field_name ])
324
                        ? $this->set($field_name, time())
325
                        : $this->set($field_name, $this->_fields[ $field_name ]);
326
                }
327
            } else {
328
                $this->_fields[ $field_name ] = $holder_of_value;
329
            }
330
            // if we're not in the constructor...
331
            // now check if what we set was a primary key
332
            if (// note: props_n_values_provided_in_constructor is only set at the END of the constructor
333
                $this->_props_n_values_provided_in_constructor
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_props_n_values_provided_in_constructor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
334
                && $field_value
335
                && $field_name === $model->primary_key_name()
336
            ) {
337
                // if so, we want all this object's fields to be filled either with
338
                // what we've explicitly set on this model
339
                // or what we have in the db
340
                // echo "setting primary key!";
341
                $fields_on_model = self::_get_model(get_class($this))->field_settings();
342
                $obj_in_db = self::_get_model(get_class($this))->get_one_by_ID($field_value);
343
                foreach ($fields_on_model as $field_obj) {
344
                    if (! array_key_exists($field_obj->get_name(), $this->_props_n_values_provided_in_constructor)
345
                        && $field_obj->get_name() !== $field_name
346
                    ) {
347
                        $this->set($field_obj->get_name(), $obj_in_db->get($field_obj->get_name()));
348
                    }
349
                }
350
                // oh this model object has an ID? well make sure its in the entity mapper
351
                $model->add_to_entity_map($this);
352
            }
353
            // let's unset any cache for this field_name from the $_cached_properties property.
354
            $this->_clear_cached_property($field_name);
355
        } else {
356
            throw new EE_Error(
357
                sprintf(
358
                    esc_html__(
359
                        'A valid EE_Model_Field_Base could not be found for the given field name: %s',
360
                        'event_espresso'
361
                    ),
362
                    $field_name
363
                )
364
            );
365
        }
366
    }
367
368
369
    /**
370
     * Set custom select values for model.
371
     *
372
     * @param array $custom_select_values
373
     */
374
    public function setCustomSelectsValues(array $custom_select_values)
375
    {
376
        $this->custom_selection_results = $custom_select_values;
377
    }
378
379
380
    /**
381
     * Returns the custom select value for the provided alias if its set.
382
     * If not set, returns null.
383
     *
384
     * @param string $alias
385
     * @return string|int|float|null
386
     */
387
    public function getCustomSelect($alias)
388
    {
389
        return isset($this->custom_selection_results[ $alias ])
390
            ? $this->custom_selection_results[ $alias ]
391
            : null;
392
    }
393
394
395
    /**
396
     * This sets the field value on the db column if it exists for the given $column_name or
397
     * saves it to EE_Extra_Meta if the given $column_name does not match a db column.
398
     *
399
     * @see EE_message::get_column_value for related documentation on the necessity of this method.
400
     * @param string $field_name  Must be the exact column name.
401
     * @param mixed  $field_value The value to set.
402
     * @return int|bool @see EE_Base_Class::update_extra_meta() for return docs.
403
     * @throws InvalidArgumentException
404
     * @throws InvalidInterfaceException
405
     * @throws InvalidDataTypeException
406
     * @throws EE_Error
407
     * @throws ReflectionException
408
     */
409
    public function set_field_or_extra_meta($field_name, $field_value)
410
    {
411
        if ($this->get_model()->has_field($field_name)) {
412
            $this->set($field_name, $field_value);
413
            return true;
414
        }
415
        // ensure this object is saved first so that extra meta can be properly related.
416
        $this->save();
417
        return $this->update_extra_meta($field_name, $field_value);
418
    }
419
420
421
    /**
422
     * This retrieves the value of the db column set on this class or if that's not present
423
     * it will attempt to retrieve from extra_meta if found.
424
     * Example Usage:
425
     * Via EE_Message child class:
426
     * Due to the dynamic nature of the EE_messages system, EE_messengers will always have a "to",
427
     * "from", "subject", and "content" field (as represented in the EE_Message schema), however they may
428
     * also have additional main fields specific to the messenger.  The system accommodates those extra
429
     * fields through the EE_Extra_Meta table.  This method allows for EE_messengers to retrieve the
430
     * value for those extra fields dynamically via the EE_message object.
431
     *
432
     * @param  string $field_name expecting the fully qualified field name.
433
     * @return mixed|null  value for the field if found.  null if not found.
434
     * @throws ReflectionException
435
     * @throws InvalidArgumentException
436
     * @throws InvalidInterfaceException
437
     * @throws InvalidDataTypeException
438
     * @throws EE_Error
439
     */
440
    public function get_field_or_extra_meta($field_name)
441
    {
442
        if ($this->get_model()->has_field($field_name)) {
443
            $column_value = $this->get($field_name);
444
        } else {
445
            // This isn't a column in the main table, let's see if it is in the extra meta.
446
            $column_value = $this->get_extra_meta($field_name, true, null);
447
        }
448
        return $column_value;
449
    }
450
451
452
    /**
453
     * See $_timezone property for description of what the timezone property is for.  This SETS the timezone internally
454
     * for being able to reference what timezone we are running conversions on when converting TO the internal timezone
455
     * (UTC Unix Timestamp) for the object OR when converting FROM the internal timezone (UTC Unix Timestamp). This is
456
     * available to all child classes that may be using the EE_Datetime_Field for a field data type.
457
     *
458
     * @access public
459
     * @param string $timezone A valid timezone string as described by @link http://www.php.net/manual/en/timezones.php
460
     * @return void
461
     * @throws InvalidArgumentException
462
     * @throws InvalidInterfaceException
463
     * @throws InvalidDataTypeException
464
     * @throws EE_Error
465
     * @throws ReflectionException
466
     */
467
    public function set_timezone($timezone = '')
468
    {
469
        $this->_timezone = EEH_DTT_Helper::get_valid_timezone_string($timezone);
470
        // make sure we clear all cached properties because they won't be relevant now
471
        $this->_clear_cached_properties();
472
        // make sure we update field settings and the date for all EE_Datetime_Fields
473
        $model_fields = $this->get_model()->field_settings(false);
474
        foreach ($model_fields as $field_name => $field_obj) {
475
            if ($field_obj instanceof EE_Datetime_Field) {
476
                $field_obj->set_timezone($this->_timezone);
477
                if (isset($this->_fields[ $field_name ]) && $this->_fields[ $field_name ] instanceof DateTime) {
478
                    EEH_DTT_Helper::setTimezone($this->_fields[ $field_name ], new DateTimeZone($this->_timezone));
479
                }
480
            }
481
        }
482
    }
483
484
485
    /**
486
     * This just returns whatever is set for the current timezone.
487
     *
488
     * @access public
489
     * @return string timezone string
490
     */
491
    public function get_timezone()
492
    {
493
        return $this->_timezone;
494
    }
495
496
497
    /**
498
     * This sets the internal date format to what is sent in to be used as the new default for the class
499
     * internally instead of wp set date format options
500
     *
501
     * @since 4.6
502
     * @param string $format should be a format recognizable by PHP date() functions.
503
     */
504
    public function set_date_format($format)
505
    {
506
        $this->_dt_frmt = $format;
507
        // clear cached_properties because they won't be relevant now.
508
        $this->_clear_cached_properties();
509
    }
510
511
512
    /**
513
     * This sets the internal time format string to what is sent in to be used as the new default for the
514
     * class internally instead of wp set time format options.
515
     *
516
     * @since 4.6
517
     * @param string $format should be a format recognizable by PHP date() functions.
518
     */
519
    public function set_time_format($format)
520
    {
521
        $this->_tm_frmt = $format;
522
        // clear cached_properties because they won't be relevant now.
523
        $this->_clear_cached_properties();
524
    }
525
526
527
    /**
528
     * This returns the current internal set format for the date and time formats.
529
     *
530
     * @param bool $full           if true (default), then return the full format.  Otherwise will return an array
531
     *                             where the first value is the date format and the second value is the time format.
532
     * @return mixed string|array
533
     */
534
    public function get_format($full = true)
535
    {
536
        return $full ? $this->_dt_frmt . ' ' . $this->_tm_frmt : array($this->_dt_frmt, $this->_tm_frmt);
537
    }
538
539
540
    /**
541
     * cache
542
     * stores the passed model object on the current model object.
543
     * In certain circumstances, we can use this cached model object instead of querying for another one entirely.
544
     *
545
     * @param string        $relationName    one of the keys in the _model_relations array on the model. Eg
546
     *                                       'Registration' associated with this model object
547
     * @param EE_Base_Class $object_to_cache that has a relation to this model object. (Eg, if this is a Transaction,
548
     *                                       that could be a payment or a registration)
549
     * @param null          $cache_id        a string or number that will be used as the key for any Belongs_To_Many
550
     *                                       items which will be stored in an array on this object
551
     * @throws ReflectionException
552
     * @throws InvalidArgumentException
553
     * @throws InvalidInterfaceException
554
     * @throws InvalidDataTypeException
555
     * @throws EE_Error
556
     * @return mixed    index into cache, or just TRUE if the relation is of type Belongs_To (because there's only one
557
     *                                       related thing, no array)
558
     */
559
    public function cache($relationName = '', $object_to_cache = null, $cache_id = null)
560
    {
561
        // its entirely possible that there IS no related object yet in which case there is nothing to cache.
562
        if (! $object_to_cache instanceof EE_Base_Class) {
563
            return false;
564
        }
565
        // also get "how" the object is related, or throw an error
566
        if (! $relationship_to_model = $this->get_model()->related_settings_for($relationName)) {
567
            throw new EE_Error(
568
                sprintf(
569
                    esc_html__('There is no relationship to %s on a %s. Cannot cache it', 'event_espresso'),
570
                    $relationName,
571
                    get_class($this)
572
                )
573
            );
574
        }
575
        // how many things are related ?
576
        if ($relationship_to_model instanceof EE_Belongs_To_Relation) {
577
            // if it's a "belongs to" relationship, then there's only one related model object
578
            // eg, if this is a registration, there's only 1 attendee for it
579
            // so for these model objects just set it to be cached
580
            $this->_model_relations[ $relationName ] = $object_to_cache;
581
            $return = true;
582
        } else {
583
            // otherwise, this is the "many" side of a one to many relationship,
584
            // so we'll add the object to the array of related objects for that type.
585
            // eg: if this is an event, there are many registrations for that event,
586
            // so we cache the registrations in an array
587
            if (! is_array($this->_model_relations[ $relationName ])) {
588
                // if for some reason, the cached item is a model object,
589
                // then stick that in the array, otherwise start with an empty array
590
                $this->_model_relations[ $relationName ] = $this->_model_relations[ $relationName ]
591
                                                           instanceof
592
                                                           EE_Base_Class
593
                    ? array($this->_model_relations[ $relationName ]) : array();
594
            }
595
            // first check for a cache_id which is normally empty
596
            if (! empty($cache_id)) {
597
                // if the cache_id exists, then it means we are purposely trying to cache this
598
                // with a known key that can then be used to retrieve the object later on
599
                $this->_model_relations[ $relationName ][ $cache_id ] = $object_to_cache;
600
                $return = $cache_id;
601
            } elseif ($object_to_cache->ID()) {
602
                // OR the cached object originally came from the db, so let's just use it's PK for an ID
603
                $this->_model_relations[ $relationName ][ $object_to_cache->ID() ] = $object_to_cache;
604
                $return = $object_to_cache->ID();
605
            } else {
606
                // OR it's a new object with no ID, so just throw it in the array with an auto-incremented ID
607
                $this->_model_relations[ $relationName ][] = $object_to_cache;
608
                // move the internal pointer to the end of the array
609
                end($this->_model_relations[ $relationName ]);
610
                // and grab the key so that we can return it
611
                $return = key($this->_model_relations[ $relationName ]);
612
            }
613
        }
614
        return $return;
615
    }
616
617
618
    /**
619
     * For adding an item to the cached_properties property.
620
     *
621
     * @access protected
622
     * @param string      $fieldname the property item the corresponding value is for.
623
     * @param mixed       $value     The value we are caching.
624
     * @param string|null $cache_type
625
     * @return void
626
     * @throws ReflectionException
627
     * @throws InvalidArgumentException
628
     * @throws InvalidInterfaceException
629
     * @throws InvalidDataTypeException
630
     * @throws EE_Error
631
     */
632
    protected function _set_cached_property($fieldname, $value, $cache_type = null)
633
    {
634
        // first make sure this property exists
635
        $this->get_model()->field_settings_for($fieldname);
636
        $cache_type = empty($cache_type) ? 'standard' : $cache_type;
637
        $this->_cached_properties[ $fieldname ][ $cache_type ] = $value;
638
    }
639
640
641
    /**
642
     * This returns the value cached property if it exists OR the actual property value if the cache doesn't exist.
643
     * This also SETS the cache if we return the actual property!
644
     *
645
     * @param string $fieldname        the name of the property we're trying to retrieve
646
     * @param bool   $pretty
647
     * @param string $extra_cache_ref  This allows the user to specify an extra cache ref for the given property
648
     *                                 (in cases where the same property may be used for different outputs
649
     *                                 - i.e. datetime, money etc.)
650
     *                                 It can also accept certain pre-defined "schema" strings
651
     *                                 to define how to output the property.
652
     *                                 see the field's prepare_for_pretty_echoing for what strings can be used
653
     * @return mixed                   whatever the value for the property is we're retrieving
654
     * @throws ReflectionException
655
     * @throws InvalidArgumentException
656
     * @throws InvalidInterfaceException
657
     * @throws InvalidDataTypeException
658
     * @throws EE_Error
659
     */
660
    protected function _get_cached_property($fieldname, $pretty = false, $extra_cache_ref = null)
661
    {
662
        // verify the field exists
663
        $model = $this->get_model();
664
        $model->field_settings_for($fieldname);
665
        $cache_type = $pretty ? 'pretty' : 'standard';
666
        $cache_type .= ! empty($extra_cache_ref) ? '_' . $extra_cache_ref : '';
667
        if (isset($this->_cached_properties[ $fieldname ][ $cache_type ])) {
668
            return $this->_cached_properties[ $fieldname ][ $cache_type ];
669
        }
670
        $value = $this->_get_fresh_property($fieldname, $pretty, $extra_cache_ref);
671
        $this->_set_cached_property($fieldname, $value, $cache_type);
672
        return $value;
673
    }
674
675
676
    /**
677
     * If the cache didn't fetch the needed item, this fetches it.
678
     *
679
     * @param string $fieldname
680
     * @param bool   $pretty
681
     * @param string $extra_cache_ref
682
     * @return mixed
683
     * @throws InvalidArgumentException
684
     * @throws InvalidInterfaceException
685
     * @throws InvalidDataTypeException
686
     * @throws EE_Error
687
     * @throws ReflectionException
688
     */
689
    protected function _get_fresh_property($fieldname, $pretty = false, $extra_cache_ref = null)
690
    {
691
        $field_obj = $this->get_model()->field_settings_for($fieldname);
692
        // If this is an EE_Datetime_Field we need to make sure timezone, formats, and output are correct
693
        if ($field_obj instanceof EE_Datetime_Field) {
694
            $this->_prepare_datetime_field($field_obj, $pretty, $extra_cache_ref);
695
        }
696
        if (! isset($this->_fields[ $fieldname ])) {
697
            $this->_fields[ $fieldname ] = null;
698
        }
699
        $value = $pretty
700
            ? $field_obj->prepare_for_pretty_echoing($this->_fields[ $fieldname ], $extra_cache_ref)
701
            : $field_obj->prepare_for_get($this->_fields[ $fieldname ]);
702
        return $value;
703
    }
704
705
706
    /**
707
     * set timezone, formats, and output for EE_Datetime_Field objects
708
     *
709
     * @param \EE_Datetime_Field $datetime_field
710
     * @param bool               $pretty
711
     * @param null               $date_or_time
712
     * @return void
713
     * @throws InvalidArgumentException
714
     * @throws InvalidInterfaceException
715
     * @throws InvalidDataTypeException
716
     * @throws EE_Error
717
     */
718
    protected function _prepare_datetime_field(
719
        EE_Datetime_Field $datetime_field,
720
        $pretty = false,
721
        $date_or_time = null
722
    ) {
723
        $datetime_field->set_timezone($this->_timezone);
724
        $datetime_field->set_date_format($this->_dt_frmt, $pretty);
725
        $datetime_field->set_time_format($this->_tm_frmt, $pretty);
726
        // set the output returned
727
        switch ($date_or_time) {
728
            case 'D':
729
                $datetime_field->set_date_time_output('date');
730
                break;
731
            case 'T':
732
                $datetime_field->set_date_time_output('time');
733
                break;
734
            default:
735
                $datetime_field->set_date_time_output();
736
        }
737
    }
738
739
740
    /**
741
     * This just takes care of clearing out the cached_properties
742
     *
743
     * @return void
744
     */
745
    protected function _clear_cached_properties()
746
    {
747
        $this->_cached_properties = array();
748
    }
749
750
751
    /**
752
     * This just clears out ONE property if it exists in the cache
753
     *
754
     * @param  string $property_name the property to remove if it exists (from the _cached_properties array)
755
     * @return void
756
     */
757
    protected function _clear_cached_property($property_name)
758
    {
759
        if (isset($this->_cached_properties[ $property_name ])) {
760
            unset($this->_cached_properties[ $property_name ]);
761
        }
762
    }
763
764
765
    /**
766
     * Ensures that this related thing is a model object.
767
     *
768
     * @param mixed  $object_or_id EE_base_Class/int/string either a related model object, or its ID
769
     * @param string $model_name   name of the related thing, eg 'Attendee',
770
     * @return EE_Base_Class
771
     * @throws ReflectionException
772
     * @throws InvalidArgumentException
773
     * @throws InvalidInterfaceException
774
     * @throws InvalidDataTypeException
775
     * @throws EE_Error
776
     */
777
    protected function ensure_related_thing_is_model_obj($object_or_id, $model_name)
778
    {
779
        $other_model_instance = self::_get_model_instance_with_name(
780
            self::_get_model_classname($model_name),
781
            $this->_timezone
782
        );
783
        return $other_model_instance->ensure_is_obj($object_or_id);
784
    }
785
786
787
    /**
788
     * Forgets the cached model of the given relation Name. So the next time we request it,
789
     * we will fetch it again from the database. (Handy if you know it's changed somehow).
790
     * If a specific object is supplied, and the relationship to it is either a HasMany or HABTM,
791
     * then only remove that one object from our cached array. Otherwise, clear the entire list
792
     *
793
     * @param string $relationName                         one of the keys in the _model_relations array on the model.
794
     *                                                     Eg 'Registration'
795
     * @param mixed  $object_to_remove_or_index_into_array or an index into the array of cached things, or NULL
796
     *                                                     if you intend to use $clear_all = TRUE, or the relation only
797
     *                                                     has 1 object anyways (ie, it's a BelongsToRelation)
798
     * @param bool   $clear_all                            This flags clearing the entire cache relation property if
799
     *                                                     this is HasMany or HABTM.
800
     * @throws ReflectionException
801
     * @throws InvalidArgumentException
802
     * @throws InvalidInterfaceException
803
     * @throws InvalidDataTypeException
804
     * @throws EE_Error
805
     * @return EE_Base_Class | boolean from which was cleared from the cache, or true if we requested to remove a
806
     *                                                     relation from all
807
     */
808
    public function clear_cache($relationName, $object_to_remove_or_index_into_array = null, $clear_all = false)
809
    {
810
        $relationship_to_model = $this->get_model()->related_settings_for($relationName);
811
        $index_in_cache = '';
812
        if (! $relationship_to_model) {
813
            throw new EE_Error(
814
                sprintf(
815
                    esc_html__('There is no relationship to %s on a %s. Cannot clear that cache', 'event_espresso'),
816
                    $relationName,
817
                    get_class($this)
818
                )
819
            );
820
        }
821
        if ($clear_all) {
822
            $obj_removed = true;
823
            $this->_model_relations[ $relationName ] = null;
824
        } elseif ($relationship_to_model instanceof EE_Belongs_To_Relation) {
825
            $obj_removed = $this->_model_relations[ $relationName ];
826
            $this->_model_relations[ $relationName ] = null;
827
        } else {
828
            if ($object_to_remove_or_index_into_array instanceof EE_Base_Class
829
                && $object_to_remove_or_index_into_array->ID()
830
            ) {
831
                $index_in_cache = $object_to_remove_or_index_into_array->ID();
832
                if (is_array($this->_model_relations[ $relationName ])
833
                    && ! isset($this->_model_relations[ $relationName ][ $index_in_cache ])
834
                ) {
835
                    $index_found_at = null;
836
                    // find this object in the array even though it has a different key
837
                    foreach ($this->_model_relations[ $relationName ] as $index => $obj) {
838
                        /** @noinspection TypeUnsafeComparisonInspection */
839
                        if ($obj instanceof EE_Base_Class
840
                            && (
841
                                $obj == $object_to_remove_or_index_into_array
842
                                || $obj->ID() === $object_to_remove_or_index_into_array->ID()
843
                            )
844
                        ) {
845
                            $index_found_at = $index;
846
                            break;
847
                        }
848
                    }
849
                    if ($index_found_at) {
850
                        $index_in_cache = $index_found_at;
851
                    } else {
852
                        // it wasn't found. huh. well obviously it doesn't need to be removed from teh cache
853
                        // if it wasn't in it to begin with. So we're done
854
                        return $object_to_remove_or_index_into_array;
855
                    }
856
                }
857
            } elseif ($object_to_remove_or_index_into_array instanceof EE_Base_Class) {
858
                // so they provided a model object, but it's not yet saved to the DB... so let's go hunting for it!
859
                foreach ($this->get_all_from_cache($relationName) as $index => $potentially_obj_we_want) {
860
                    /** @noinspection TypeUnsafeComparisonInspection */
861
                    if ($potentially_obj_we_want == $object_to_remove_or_index_into_array) {
862
                        $index_in_cache = $index;
863
                    }
864
                }
865
            } else {
866
                $index_in_cache = $object_to_remove_or_index_into_array;
867
            }
868
            // supposedly we've found it. But it could just be that the client code
869
            // provided a bad index/object
870
            if (isset($this->_model_relations[ $relationName ][ $index_in_cache ])) {
871
                $obj_removed = $this->_model_relations[ $relationName ][ $index_in_cache ];
872
                unset($this->_model_relations[ $relationName ][ $index_in_cache ]);
873
            } else {
874
                // that thing was never cached anyways.
875
                $obj_removed = null;
876
            }
877
        }
878
        return $obj_removed;
879
    }
880
881
882
    /**
883
     * update_cache_after_object_save
884
     * Allows a cached item to have it's cache ID (within the array of cached items) reset using the new ID it has
885
     * obtained after being saved to the db
886
     *
887
     * @param string        $relationName       - the type of object that is cached
888
     * @param EE_Base_Class $newly_saved_object - the newly saved object to be re-cached
889
     * @param string        $current_cache_id   - the ID that was used when originally caching the object
890
     * @return boolean TRUE on success, FALSE on fail
891
     * @throws ReflectionException
892
     * @throws InvalidArgumentException
893
     * @throws InvalidInterfaceException
894
     * @throws InvalidDataTypeException
895
     * @throws EE_Error
896
     */
897
    public function update_cache_after_object_save(
898
        $relationName,
899
        EE_Base_Class $newly_saved_object,
900
        $current_cache_id = ''
901
    ) {
902
        // verify that incoming object is of the correct type
903
        $obj_class = 'EE_' . $relationName;
904
        if ($newly_saved_object instanceof $obj_class) {
905
            /* @type EE_Base_Class $newly_saved_object */
906
            // now get the type of relation
907
            $relationship_to_model = $this->get_model()->related_settings_for($relationName);
908
            // if this is a 1:1 relationship
909
            if ($relationship_to_model instanceof EE_Belongs_To_Relation) {
910
                // then just replace the cached object with the newly saved object
911
                $this->_model_relations[ $relationName ] = $newly_saved_object;
912
                return true;
913
                // or if it's some kind of sordid feral polyamorous relationship...
914
            }
915
            if (is_array($this->_model_relations[ $relationName ])
916
                && isset($this->_model_relations[ $relationName ][ $current_cache_id ])
917
            ) {
918
                // then remove the current cached item
919
                unset($this->_model_relations[ $relationName ][ $current_cache_id ]);
920
                // and cache the newly saved object using it's new ID
921
                $this->_model_relations[ $relationName ][ $newly_saved_object->ID() ] = $newly_saved_object;
922
                return true;
923
            }
924
        }
925
        return false;
926
    }
927
928
929
    /**
930
     * Fetches a single EE_Base_Class on that relation. (If the relation is of type
931
     * BelongsTo, it will only ever have 1 object. However, other relations could have an array of objects)
932
     *
933
     * @param string $relationName
934
     * @return EE_Base_Class
935
     */
936
    public function get_one_from_cache($relationName)
937
    {
938
        $cached_array_or_object = isset($this->_model_relations[ $relationName ])
939
            ? $this->_model_relations[ $relationName ]
940
            : null;
941
        if (is_array($cached_array_or_object)) {
942
            return array_shift($cached_array_or_object);
943
        }
944
        return $cached_array_or_object;
945
    }
946
947
948
    /**
949
     * Fetches a single EE_Base_Class on that relation. (If the relation is of type
950
     * BelongsTo, it will only ever have 1 object. However, other relations could have an array of objects)
951
     *
952
     * @param string $relationName
953
     * @throws ReflectionException
954
     * @throws InvalidArgumentException
955
     * @throws InvalidInterfaceException
956
     * @throws InvalidDataTypeException
957
     * @throws EE_Error
958
     * @return EE_Base_Class[] NOT necessarily indexed by primary keys
959
     */
960
    public function get_all_from_cache($relationName)
961
    {
962
        $objects = isset($this->_model_relations[ $relationName ]) ? $this->_model_relations[ $relationName ] : array();
963
        // if the result is not an array, but exists, make it an array
964
        $objects = is_array($objects) ? $objects : array($objects);
965
        // bugfix for https://events.codebasehq.com/projects/event-espresso/tickets/7143
966
        // basically, if this model object was stored in the session, and these cached model objects
967
        // already have IDs, let's make sure they're in their model's entity mapper
968
        // otherwise we will have duplicates next time we call
969
        // EE_Registry::instance()->load_model( $relationName )->get_one_by_ID( $result->ID() );
970
        $model = EE_Registry::instance()->load_model($relationName);
971
        foreach ($objects as $model_object) {
972
            if ($model instanceof EEM_Base && $model_object instanceof EE_Base_Class) {
973
                // ensure its in the map if it has an ID; otherwise it will be added to the map when its saved
974
                if ($model_object->ID()) {
975
                    $model->add_to_entity_map($model_object);
976
                }
977
            } else {
978
                throw new EE_Error(
979
                    sprintf(
980
                        esc_html__(
981
                            'Error retrieving related model objects. Either $1%s is not a model or $2%s is not a model object',
982
                            'event_espresso'
983
                        ),
984
                        $relationName,
985
                        gettype($model_object)
986
                    )
987
                );
988
            }
989
        }
990
        return $objects;
991
    }
992
993
994
    /**
995
     * Returns the next x number of EE_Base_Class objects in sequence from this object as found in the database
996
     * matching the given query conditions.
997
     *
998
     * @param null  $field_to_order_by  What field is being used as the reference point.
999
     * @param int   $limit              How many objects to return.
1000
     * @param array $query_params       Any additional conditions on the query.
1001
     * @param null  $columns_to_select  If left null, then an array of EE_Base_Class objects is returned, otherwise
1002
     *                                  you can indicate just the columns you want returned
1003
     * @return array|EE_Base_Class[]
1004
     * @throws ReflectionException
1005
     * @throws InvalidArgumentException
1006
     * @throws InvalidInterfaceException
1007
     * @throws InvalidDataTypeException
1008
     * @throws EE_Error
1009
     */
1010 View Code Duplication
    public function next_x($field_to_order_by = null, $limit = 1, $query_params = array(), $columns_to_select = null)
1011
    {
1012
        $model = $this->get_model();
1013
        $field = empty($field_to_order_by) && $model->has_primary_key_field()
1014
            ? $model->get_primary_key_field()->get_name()
1015
            : $field_to_order_by;
1016
        $current_value = ! empty($field) ? $this->get($field) : null;
1017
        if (empty($field) || empty($current_value)) {
1018
            return array();
1019
        }
1020
        return $model->next_x($current_value, $field, $limit, $query_params, $columns_to_select);
1021
    }
1022
1023
1024
    /**
1025
     * Returns the previous x number of EE_Base_Class objects in sequence from this object as found in the database
1026
     * matching the given query conditions.
1027
     *
1028
     * @param null  $field_to_order_by  What field is being used as the reference point.
1029
     * @param int   $limit              How many objects to return.
1030
     * @param array $query_params       Any additional conditions on the query.
1031
     * @param null  $columns_to_select  If left null, then an array of EE_Base_Class objects is returned, otherwise
1032
     *                                  you can indicate just the columns you want returned
1033
     * @return array|EE_Base_Class[]
1034
     * @throws ReflectionException
1035
     * @throws InvalidArgumentException
1036
     * @throws InvalidInterfaceException
1037
     * @throws InvalidDataTypeException
1038
     * @throws EE_Error
1039
     */
1040 View Code Duplication
    public function previous_x(
1041
        $field_to_order_by = null,
1042
        $limit = 1,
1043
        $query_params = array(),
1044
        $columns_to_select = null
1045
    ) {
1046
        $model = $this->get_model();
1047
        $field = empty($field_to_order_by) && $model->has_primary_key_field()
1048
            ? $model->get_primary_key_field()->get_name()
1049
            : $field_to_order_by;
1050
        $current_value = ! empty($field) ? $this->get($field) : null;
1051
        if (empty($field) || empty($current_value)) {
1052
            return array();
1053
        }
1054
        return $model->previous_x($current_value, $field, $limit, $query_params, $columns_to_select);
1055
    }
1056
1057
1058
    /**
1059
     * Returns the next EE_Base_Class object in sequence from this object as found in the database
1060
     * matching the given query conditions.
1061
     *
1062
     * @param null  $field_to_order_by  What field is being used as the reference point.
1063
     * @param array $query_params       Any additional conditions on the query.
1064
     * @param null  $columns_to_select  If left null, then an array of EE_Base_Class objects is returned, otherwise
1065
     *                                  you can indicate just the columns you want returned
1066
     * @return array|EE_Base_Class
1067
     * @throws ReflectionException
1068
     * @throws InvalidArgumentException
1069
     * @throws InvalidInterfaceException
1070
     * @throws InvalidDataTypeException
1071
     * @throws EE_Error
1072
     */
1073 View Code Duplication
    public function next($field_to_order_by = null, $query_params = array(), $columns_to_select = null)
1074
    {
1075
        $model = $this->get_model();
1076
        $field = empty($field_to_order_by) && $model->has_primary_key_field()
1077
            ? $model->get_primary_key_field()->get_name()
1078
            : $field_to_order_by;
1079
        $current_value = ! empty($field) ? $this->get($field) : null;
1080
        if (empty($field) || empty($current_value)) {
1081
            return array();
1082
        }
1083
        return $model->next($current_value, $field, $query_params, $columns_to_select);
1084
    }
1085
1086
1087
    /**
1088
     * Returns the previous EE_Base_Class object in sequence from this object as found in the database
1089
     * matching the given query conditions.
1090
     *
1091
     * @param null  $field_to_order_by  What field is being used as the reference point.
1092
     * @param array $query_params       Any additional conditions on the query.
1093
     * @param null  $columns_to_select  If left null, then an EE_Base_Class object is returned, otherwise
1094
     *                                  you can indicate just the column you want returned
1095
     * @return array|EE_Base_Class
1096
     * @throws ReflectionException
1097
     * @throws InvalidArgumentException
1098
     * @throws InvalidInterfaceException
1099
     * @throws InvalidDataTypeException
1100
     * @throws EE_Error
1101
     */
1102 View Code Duplication
    public function previous($field_to_order_by = null, $query_params = array(), $columns_to_select = null)
1103
    {
1104
        $model = $this->get_model();
1105
        $field = empty($field_to_order_by) && $model->has_primary_key_field()
1106
            ? $model->get_primary_key_field()->get_name()
1107
            : $field_to_order_by;
1108
        $current_value = ! empty($field) ? $this->get($field) : null;
1109
        if (empty($field) || empty($current_value)) {
1110
            return array();
1111
        }
1112
        return $model->previous($current_value, $field, $query_params, $columns_to_select);
1113
    }
1114
1115
1116
    /**
1117
     * Overrides parent because parent expects old models.
1118
     * This also doesn't do any validation, and won't work for serialized arrays
1119
     *
1120
     * @param string $field_name
1121
     * @param mixed  $field_value_from_db
1122
     * @throws ReflectionException
1123
     * @throws InvalidArgumentException
1124
     * @throws InvalidInterfaceException
1125
     * @throws InvalidDataTypeException
1126
     * @throws EE_Error
1127
     */
1128
    public function set_from_db($field_name, $field_value_from_db)
1129
    {
1130
        $field_obj = $this->get_model()->field_settings_for($field_name);
1131
        if ($field_obj instanceof EE_Model_Field_Base) {
1132
            // you would think the DB has no NULLs for non-null label fields right? wrong!
1133
            // eg, a CPT model object could have an entry in the posts table, but no
1134
            // entry in the meta table. Meaning that all its columns in the meta table
1135
            // are null! yikes! so when we find one like that, use defaults for its meta columns
1136
            if ($field_value_from_db === null) {
1137
                if ($field_obj->is_nullable()) {
1138
                    // if the field allows nulls, then let it be null
1139
                    $field_value = null;
1140
                } else {
1141
                    $field_value = $field_obj->get_default_value();
1142
                }
1143
            } else {
1144
                $field_value = $field_obj->prepare_for_set_from_db($field_value_from_db);
1145
            }
1146
            $this->_fields[ $field_name ] = $field_value;
1147
            $this->_clear_cached_property($field_name);
1148
        }
1149
    }
1150
1151
1152
    /**
1153
     * verifies that the specified field is of the correct type
1154
     *
1155
     * @param string $field_name
1156
     * @param string $extra_cache_ref This allows the user to specify an extra cache ref for the given property
1157
     *                                (in cases where the same property may be used for different outputs
1158
     *                                - i.e. datetime, money etc.)
1159
     * @return mixed
1160
     * @throws ReflectionException
1161
     * @throws InvalidArgumentException
1162
     * @throws InvalidInterfaceException
1163
     * @throws InvalidDataTypeException
1164
     * @throws EE_Error
1165
     */
1166
    public function get($field_name, $extra_cache_ref = null)
1167
    {
1168
        return $this->_get_cached_property($field_name, false, $extra_cache_ref);
1169
    }
1170
1171
1172
    /**
1173
     * This method simply returns the RAW unprocessed value for the given property in this class
1174
     *
1175
     * @param  string $field_name A valid fieldname
1176
     * @return mixed              Whatever the raw value stored on the property is.
1177
     * @throws ReflectionException
1178
     * @throws InvalidArgumentException
1179
     * @throws InvalidInterfaceException
1180
     * @throws InvalidDataTypeException
1181
     * @throws EE_Error if fieldSettings is misconfigured or the field doesn't exist.
1182
     */
1183
    public function get_raw($field_name)
1184
    {
1185
        $field_settings = $this->get_model()->field_settings_for($field_name);
1186
        return $field_settings instanceof EE_Datetime_Field && $this->_fields[ $field_name ] instanceof DateTime
1187
            ? $this->_fields[ $field_name ]->format('U')
1188
            : $this->_fields[ $field_name ];
1189
    }
1190
1191
1192
    /**
1193
     * This is used to return the internal DateTime object used for a field that is a
1194
     * EE_Datetime_Field.
1195
     *
1196
     * @param string $field_name               The field name retrieving the DateTime object.
1197
     * @return mixed null | false | DateTime  If the requested field is NOT a EE_Datetime_Field then
1198
     * @throws EE_Error an error is set and false returned.  If the field IS an
1199
     *                                         EE_Datetime_Field and but the field value is null, then
1200
     *                                         just null is returned (because that indicates that likely
1201
     *                                         this field is nullable).
1202
     * @throws InvalidArgumentException
1203
     * @throws InvalidDataTypeException
1204
     * @throws InvalidInterfaceException
1205
     * @throws ReflectionException
1206
     */
1207
    public function get_DateTime_object($field_name)
1208
    {
1209
        $field_settings = $this->get_model()->field_settings_for($field_name);
1210
        if (! $field_settings instanceof EE_Datetime_Field) {
1211
            EE_Error::add_error(
1212
                sprintf(
1213
                    esc_html__(
1214
                        'The field %s is not an EE_Datetime_Field field.  There is no DateTime object stored on this field type.',
1215
                        'event_espresso'
1216
                    ),
1217
                    $field_name
1218
                ),
1219
                __FILE__,
1220
                __FUNCTION__,
1221
                __LINE__
1222
            );
1223
            return false;
1224
        }
1225
        return isset($this->_fields[ $field_name ]) && $this->_fields[ $field_name ] instanceof DateTime
1226
            ? clone $this->_fields[ $field_name ]
1227
            : null;
1228
    }
1229
1230
1231
    /**
1232
     * To be used in template to immediately echo out the value, and format it for output.
1233
     * Eg, should call stripslashes and whatnot before echoing
1234
     *
1235
     * @param string $field_name      the name of the field as it appears in the DB
1236
     * @param string $extra_cache_ref This allows the user to specify an extra cache ref for the given property
1237
     *                                (in cases where the same property may be used for different outputs
1238
     *                                - i.e. datetime, money etc.)
1239
     * @return void
1240
     * @throws ReflectionException
1241
     * @throws InvalidArgumentException
1242
     * @throws InvalidInterfaceException
1243
     * @throws InvalidDataTypeException
1244
     * @throws EE_Error
1245
     */
1246
    public function e($field_name, $extra_cache_ref = null)
1247
    {
1248
        echo $this->get_pretty($field_name, $extra_cache_ref);
1249
    }
1250
1251
1252
    /**
1253
     * Exactly like e(), echoes out the field, but sets its schema to 'form_input', so that it
1254
     * can be easily used as the value of form input.
1255
     *
1256
     * @param string $field_name
1257
     * @return void
1258
     * @throws ReflectionException
1259
     * @throws InvalidArgumentException
1260
     * @throws InvalidInterfaceException
1261
     * @throws InvalidDataTypeException
1262
     * @throws EE_Error
1263
     */
1264
    public function f($field_name)
1265
    {
1266
        $this->e($field_name, 'form_input');
1267
    }
1268
1269
1270
    /**
1271
     * Same as `f()` but just returns the value instead of echoing it
1272
     *
1273
     * @param string $field_name
1274
     * @return string
1275
     * @throws ReflectionException
1276
     * @throws InvalidArgumentException
1277
     * @throws InvalidInterfaceException
1278
     * @throws InvalidDataTypeException
1279
     * @throws EE_Error
1280
     */
1281
    public function get_f($field_name)
1282
    {
1283
        return (string) $this->get_pretty($field_name, 'form_input');
1284
    }
1285
1286
1287
    /**
1288
     * Gets a pretty view of the field's value. $extra_cache_ref can specify different formats for this.
1289
     * The $extra_cache_ref will be passed to the model field's prepare_for_pretty_echoing, so consult the field's class
1290
     * to see what options are available.
1291
     *
1292
     * @param string $field_name
1293
     * @param string $extra_cache_ref This allows the user to specify an extra cache ref for the given property
1294
     *                                (in cases where the same property may be used for different outputs
1295
     *                                - i.e. datetime, money etc.)
1296
     * @return mixed
1297
     * @throws ReflectionException
1298
     * @throws InvalidArgumentException
1299
     * @throws InvalidInterfaceException
1300
     * @throws InvalidDataTypeException
1301
     * @throws EE_Error
1302
     */
1303
    public function get_pretty($field_name, $extra_cache_ref = null)
1304
    {
1305
        return $this->_get_cached_property($field_name, true, $extra_cache_ref);
1306
    }
1307
1308
1309
    /**
1310
     * This simply returns the datetime for the given field name
1311
     * Note: this protected function is called by the wrapper get_date or get_time or get_datetime functions
1312
     * (and the equivalent e_date, e_time, e_datetime).
1313
     *
1314
     * @access   protected
1315
     * @param string   $field_name   Field on the instantiated EE_Base_Class child object
1316
     * @param string   $dt_frmt      valid datetime format used for date
1317
     *                               (if '' then we just use the default on the field,
1318
     *                               if NULL we use the last-used format)
1319
     * @param string   $tm_frmt      Same as above except this is for time format
1320
     * @param string   $date_or_time if NULL then both are returned, otherwise "D" = only date and "T" = only time.
1321
     * @param  boolean $echo         Whether the dtt is echoing using pretty echoing or just returned using vanilla get
1322
     * @return string|bool|EE_Error string on success, FALSE on fail, or EE_Error Exception is thrown
1323
     *                               if field is not a valid dtt field, or void if echoing
1324
     * @throws ReflectionException
1325
     * @throws InvalidArgumentException
1326
     * @throws InvalidInterfaceException
1327
     * @throws InvalidDataTypeException
1328
     * @throws EE_Error
1329
     */
1330
    protected function _get_datetime($field_name, $dt_frmt = '', $tm_frmt = '', $date_or_time = '', $echo = false)
1331
    {
1332
        // clear cached property
1333
        $this->_clear_cached_property($field_name);
1334
        // reset format properties because they are used in get()
1335
        $this->_dt_frmt = $dt_frmt !== '' ? $dt_frmt : $this->_dt_frmt;
1336
        $this->_tm_frmt = $tm_frmt !== '' ? $tm_frmt : $this->_tm_frmt;
1337
        if ($echo) {
1338
            $this->e($field_name, $date_or_time);
1339
            return '';
1340
        }
1341
        return $this->get($field_name, $date_or_time);
1342
    }
1343
1344
1345
    /**
1346
     * below are wrapper functions for the various datetime outputs that can be obtained for JUST returning the date
1347
     * portion of a datetime value. (note the only difference between get_ and e_ is one returns the value and the
1348
     * other echoes the pretty value for dtt)
1349
     *
1350
     * @param  string $field_name name of model object datetime field holding the value
1351
     * @param  string $format     format for the date returned (if NULL we use default in dt_frmt property)
1352
     * @return string            datetime value formatted
1353
     * @throws ReflectionException
1354
     * @throws InvalidArgumentException
1355
     * @throws InvalidInterfaceException
1356
     * @throws InvalidDataTypeException
1357
     * @throws EE_Error
1358
     */
1359
    public function get_date($field_name, $format = '')
1360
    {
1361
        return $this->_get_datetime($field_name, $format, null, 'D');
1362
    }
1363
1364
1365
    /**
1366
     * @param        $field_name
1367
     * @param string $format
1368
     * @throws ReflectionException
1369
     * @throws InvalidArgumentException
1370
     * @throws InvalidInterfaceException
1371
     * @throws InvalidDataTypeException
1372
     * @throws EE_Error
1373
     */
1374
    public function e_date($field_name, $format = '')
1375
    {
1376
        $this->_get_datetime($field_name, $format, null, 'D', true);
1377
    }
1378
1379
1380
    /**
1381
     * below are wrapper functions for the various datetime outputs that can be obtained for JUST returning the time
1382
     * portion of a datetime value. (note the only difference between get_ and e_ is one returns the value and the
1383
     * other echoes the pretty value for dtt)
1384
     *
1385
     * @param  string $field_name name of model object datetime field holding the value
1386
     * @param  string $format     format for the time returned ( if NULL we use default in tm_frmt property)
1387
     * @return string             datetime value formatted
1388
     * @throws ReflectionException
1389
     * @throws InvalidArgumentException
1390
     * @throws InvalidInterfaceException
1391
     * @throws InvalidDataTypeException
1392
     * @throws EE_Error
1393
     */
1394
    public function get_time($field_name, $format = '')
1395
    {
1396
        return $this->_get_datetime($field_name, null, $format, 'T');
1397
    }
1398
1399
1400
    /**
1401
     * @param        $field_name
1402
     * @param string $format
1403
     * @throws ReflectionException
1404
     * @throws InvalidArgumentException
1405
     * @throws InvalidInterfaceException
1406
     * @throws InvalidDataTypeException
1407
     * @throws EE_Error
1408
     */
1409
    public function e_time($field_name, $format = '')
1410
    {
1411
        $this->_get_datetime($field_name, null, $format, 'T', true);
1412
    }
1413
1414
1415
    /**
1416
     * below are wrapper functions for the various datetime outputs that can be obtained for returning the date AND
1417
     * time portion of a datetime value. (note the only difference between get_ and e_ is one returns the value and the
1418
     * other echoes the pretty value for dtt)
1419
     *
1420
     * @param  string $field_name name of model object datetime field holding the value
1421
     * @param  string $dt_frmt    format for the date returned (if NULL we use default in dt_frmt property)
1422
     * @param  string $tm_frmt    format for the time returned (if NULL we use default in tm_frmt property)
1423
     * @return string             datetime value formatted
1424
     * @throws ReflectionException
1425
     * @throws InvalidArgumentException
1426
     * @throws InvalidInterfaceException
1427
     * @throws InvalidDataTypeException
1428
     * @throws EE_Error
1429
     */
1430
    public function get_datetime($field_name, $dt_frmt = '', $tm_frmt = '')
1431
    {
1432
        return $this->_get_datetime($field_name, $dt_frmt, $tm_frmt);
1433
    }
1434
1435
1436
    /**
1437
     * @param string $field_name
1438
     * @param string $dt_frmt
1439
     * @param string $tm_frmt
1440
     * @throws ReflectionException
1441
     * @throws InvalidArgumentException
1442
     * @throws InvalidInterfaceException
1443
     * @throws InvalidDataTypeException
1444
     * @throws EE_Error
1445
     */
1446
    public function e_datetime($field_name, $dt_frmt = '', $tm_frmt = '')
1447
    {
1448
        $this->_get_datetime($field_name, $dt_frmt, $tm_frmt, null, true);
1449
    }
1450
1451
1452
    /**
1453
     * Get the i8ln value for a date using the WordPress @see date_i18n function.
1454
     *
1455
     * @param string $field_name The EE_Datetime_Field reference for the date being retrieved.
1456
     * @param string $format     PHP valid date/time string format.  If none is provided then the internal set format
1457
     *                           on the object will be used.
1458
     * @return string Date and time string in set locale or false if no field exists for the given
1459
     * @throws ReflectionException
1460
     * @throws InvalidArgumentException
1461
     * @throws InvalidInterfaceException
1462
     * @throws InvalidDataTypeException
1463
     * @throws EE_Error
1464
     *                           field name.
1465
     */
1466
    public function get_i18n_datetime($field_name, $format = '')
1467
    {
1468
        $format = empty($format) ? $this->_dt_frmt . ' ' . $this->_tm_frmt : $format;
1469
        return date_i18n(
1470
            $format,
1471
            EEH_DTT_Helper::get_timestamp_with_offset(
1472
                $this->get_raw($field_name),
1473
                $this->_timezone
1474
            )
1475
        );
1476
    }
1477
1478
1479
    /**
1480
     * This method validates whether the given field name is a valid field on the model object as well as it is of a
1481
     * type EE_Datetime_Field.  On success there will be returned the field settings.  On fail an EE_Error exception is
1482
     * thrown.
1483
     *
1484
     * @param  string $field_name The field name being checked
1485
     * @throws ReflectionException
1486
     * @throws InvalidArgumentException
1487
     * @throws InvalidInterfaceException
1488
     * @throws InvalidDataTypeException
1489
     * @throws EE_Error
1490
     * @return EE_Datetime_Field
1491
     */
1492
    protected function _get_dtt_field_settings($field_name)
1493
    {
1494
        $field = $this->get_model()->field_settings_for($field_name);
1495
        // check if field is dtt
1496
        if ($field instanceof EE_Datetime_Field) {
1497
            return $field;
1498
        }
1499
        throw new EE_Error(
1500
            sprintf(
1501
                esc_html__(
1502
                    'The field name "%s" has been requested for the EE_Base_Class datetime functions and it is not a valid EE_Datetime_Field.  Please check the spelling of the field and make sure it has been setup as a EE_Datetime_Field in the %s model constructor',
1503
                    'event_espresso'
1504
                ),
1505
                $field_name,
1506
                self::_get_model_classname(get_class($this))
1507
            )
1508
        );
1509
    }
1510
1511
1512
1513
1514
    /**
1515
     * NOTE ABOUT BELOW:
1516
     * These convenience date and time setters are for setting date and time independently.  In other words you might
1517
     * want to change the time on a datetime_field but leave the date the same (or vice versa). IF on the other hand
1518
     * you want to set both date and time at the same time, you can just use the models default set($fieldname,$value)
1519
     * method and make sure you send the entire datetime value for setting.
1520
     */
1521
    /**
1522
     * sets the time on a datetime property
1523
     *
1524
     * @access protected
1525
     * @param string|Datetime $time      a valid time string for php datetime functions (or DateTime object)
1526
     * @param string          $fieldname the name of the field the time is being set on (must match a EE_Datetime_Field)
1527
     * @throws ReflectionException
1528
     * @throws InvalidArgumentException
1529
     * @throws InvalidInterfaceException
1530
     * @throws InvalidDataTypeException
1531
     * @throws EE_Error
1532
     */
1533
    protected function _set_time_for($time, $fieldname)
1534
    {
1535
        $this->_set_date_time('T', $time, $fieldname);
1536
    }
1537
1538
1539
    /**
1540
     * sets the date on a datetime property
1541
     *
1542
     * @access protected
1543
     * @param string|DateTime $date      a valid date string for php datetime functions ( or DateTime object)
1544
     * @param string          $fieldname the name of the field the date is being set on (must match a EE_Datetime_Field)
1545
     * @throws ReflectionException
1546
     * @throws InvalidArgumentException
1547
     * @throws InvalidInterfaceException
1548
     * @throws InvalidDataTypeException
1549
     * @throws EE_Error
1550
     */
1551
    protected function _set_date_for($date, $fieldname)
1552
    {
1553
        $this->_set_date_time('D', $date, $fieldname);
1554
    }
1555
1556
1557
    /**
1558
     * This takes care of setting a date or time independently on a given model object property. This method also
1559
     * verifies that the given fieldname matches a model object property and is for a EE_Datetime_Field field
1560
     *
1561
     * @access protected
1562
     * @param string          $what           "T" for time, 'B' for both, 'D' for Date.
1563
     * @param string|DateTime $datetime_value A valid Date or Time string (or DateTime object)
1564
     * @param string          $fieldname      the name of the field the date OR time is being set on (must match a
1565
     *                                        EE_Datetime_Field property)
1566
     * @throws ReflectionException
1567
     * @throws InvalidArgumentException
1568
     * @throws InvalidInterfaceException
1569
     * @throws InvalidDataTypeException
1570
     * @throws EE_Error
1571
     */
1572
    protected function _set_date_time($what = 'T', $datetime_value, $fieldname)
1573
    {
1574
        $field = $this->_get_dtt_field_settings($fieldname);
1575
        $field->set_timezone($this->_timezone);
1576
        $field->set_date_format($this->_dt_frmt);
1577
        $field->set_time_format($this->_tm_frmt);
1578
        switch ($what) {
1579
            case 'T':
1580
                $this->_fields[ $fieldname ] = $field->prepare_for_set_with_new_time(
1581
                    $datetime_value,
1582
                    $this->_fields[ $fieldname ]
1583
                );
1584
                break;
1585
            case 'D':
1586
                $this->_fields[ $fieldname ] = $field->prepare_for_set_with_new_date(
1587
                    $datetime_value,
1588
                    $this->_fields[ $fieldname ]
1589
                );
1590
                break;
1591
            case 'B':
1592
                $this->_fields[ $fieldname ] = $field->prepare_for_set($datetime_value);
1593
                break;
1594
        }
1595
        $this->_clear_cached_property($fieldname);
1596
    }
1597
1598
1599
    /**
1600
     * This will return a timestamp for the website timezone but ONLY when the current website timezone is different
1601
     * than the timezone set for the website. NOTE, this currently only works well with methods that return values.  If
1602
     * you use it with methods that echo values the $_timestamp property may not get reset to its original value and
1603
     * that could lead to some unexpected results!
1604
     *
1605
     * @access public
1606
     * @param string $field_name               This is the name of the field on the object that contains the date/time
1607
     *                                         value being returned.
1608
     * @param string $callback                 must match a valid method in this class (defaults to get_datetime)
1609
     * @param mixed (array|string) $args       This is the arguments that will be passed to the callback.
1610
     * @param string $prepend                  You can include something to prepend on the timestamp
1611
     * @param string $append                   You can include something to append on the timestamp
1612
     * @throws ReflectionException
1613
     * @throws InvalidArgumentException
1614
     * @throws InvalidInterfaceException
1615
     * @throws InvalidDataTypeException
1616
     * @throws EE_Error
1617
     * @return string timestamp
1618
     */
1619
    public function display_in_my_timezone(
1620
        $field_name,
1621
        $callback = 'get_datetime',
1622
        $args = null,
1623
        $prepend = '',
1624
        $append = ''
1625
    ) {
1626
        $timezone = EEH_DTT_Helper::get_timezone();
1627
        if ($timezone === $this->_timezone) {
1628
            return '';
1629
        }
1630
        $original_timezone = $this->_timezone;
1631
        $this->set_timezone($timezone);
1632
        $fn = (array) $field_name;
1633
        $args = array_merge($fn, (array) $args);
1634
        if (! method_exists($this, $callback)) {
1635
            throw new EE_Error(
1636
                sprintf(
1637
                    esc_html__(
1638
                        'The method named "%s" given as the callback param in "display_in_my_timezone" does not exist.  Please check your spelling',
1639
                        'event_espresso'
1640
                    ),
1641
                    $callback
1642
                )
1643
            );
1644
        }
1645
        $args = (array) $args;
1646
        $return = $prepend . call_user_func_array(array($this, $callback), $args) . $append;
1647
        $this->set_timezone($original_timezone);
1648
        return $return;
1649
    }
1650
1651
1652
    /**
1653
     * Deletes this model object.
1654
     * This calls the `EE_Base_Class::_delete` method.  Child classes wishing to change default behaviour should
1655
     * override
1656
     * `EE_Base_Class::_delete` NOT this class.
1657
     *
1658
     * @return boolean | int
1659
     * @throws ReflectionException
1660
     * @throws InvalidArgumentException
1661
     * @throws InvalidInterfaceException
1662
     * @throws InvalidDataTypeException
1663
     * @throws EE_Error
1664
     */
1665
    public function delete()
1666
    {
1667
        /**
1668
         * Called just before the `EE_Base_Class::_delete` method call.
1669
         * Note:
1670
         * `EE_Base_Class::_delete` might be overridden by child classes so any client code hooking into these actions
1671
         * should be aware that `_delete` may not always result in a permanent delete.
1672
         * For example, `EE_Soft_Delete_Base_Class::_delete`
1673
         * soft deletes (trash) the object and does not permanently delete it.
1674
         *
1675
         * @param EE_Base_Class $model_object about to be 'deleted'
1676
         */
1677
        do_action('AHEE__EE_Base_Class__delete__before', $this);
1678
        $result = $this->_delete();
1679
        /**
1680
         * Called just after the `EE_Base_Class::_delete` method call.
1681
         * Note:
1682
         * `EE_Base_Class::_delete` might be overridden by child classes so any client code hooking into these actions
1683
         * should be aware that `_delete` may not always result in a permanent delete.
1684
         * For example `EE_Soft_Base_Class::_delete`
1685
         * soft deletes (trash) the object and does not permanently delete it.
1686
         *
1687
         * @param EE_Base_Class $model_object that was just 'deleted'
1688
         * @param boolean       $result
1689
         */
1690
        do_action('AHEE__EE_Base_Class__delete__end', $this, $result);
1691
        return $result;
1692
    }
1693
1694
1695
    /**
1696
     * Calls the specific delete method for the instantiated class.
1697
     * This method is called by the public `EE_Base_Class::delete` method.  Any child classes desiring to override
1698
     * default functionality for "delete" (which is to call `permanently_delete`) should override this method NOT
1699
     * `EE_Base_Class::delete`
1700
     *
1701
     * @return bool|int
1702
     * @throws ReflectionException
1703
     * @throws InvalidArgumentException
1704
     * @throws InvalidInterfaceException
1705
     * @throws InvalidDataTypeException
1706
     * @throws EE_Error
1707
     */
1708
    protected function _delete()
1709
    {
1710
        return $this->delete_permanently();
1711
    }
1712
1713
1714
    /**
1715
     * Deletes this model object permanently from db
1716
     * (but keep in mind related models may block the delete and return an error)
1717
     *
1718
     * @return bool | int
1719
     * @throws ReflectionException
1720
     * @throws InvalidArgumentException
1721
     * @throws InvalidInterfaceException
1722
     * @throws InvalidDataTypeException
1723
     * @throws EE_Error
1724
     */
1725
    public function delete_permanently()
1726
    {
1727
        /**
1728
         * Called just before HARD deleting a model object
1729
         *
1730
         * @param EE_Base_Class $model_object about to be 'deleted'
1731
         */
1732
        do_action('AHEE__EE_Base_Class__delete_permanently__before', $this);
1733
        $model = $this->get_model();
1734
        $result = $model->delete_permanently_by_ID($this->ID());
1735
        $this->refresh_cache_of_related_objects();
1736
        /**
1737
         * Called just after HARD deleting a model object
1738
         *
1739
         * @param EE_Base_Class $model_object that was just 'deleted'
1740
         * @param boolean       $result
1741
         */
1742
        do_action('AHEE__EE_Base_Class__delete_permanently__end', $this, $result);
1743
        return $result;
1744
    }
1745
1746
1747
    /**
1748
     * When this model object is deleted, it may still be cached on related model objects. This clears the cache of
1749
     * related model objects
1750
     *
1751
     * @throws ReflectionException
1752
     * @throws InvalidArgumentException
1753
     * @throws InvalidInterfaceException
1754
     * @throws InvalidDataTypeException
1755
     * @throws EE_Error
1756
     */
1757
    public function refresh_cache_of_related_objects()
1758
    {
1759
        $model = $this->get_model();
1760
        foreach ($model->relation_settings() as $relation_name => $relation_obj) {
1761
            if (! empty($this->_model_relations[ $relation_name ])) {
1762
                $related_objects = $this->_model_relations[ $relation_name ];
1763
                if ($relation_obj instanceof EE_Belongs_To_Relation) {
1764
                    // this relation only stores a single model object, not an array
1765
                    // but let's make it consistent
1766
                    $related_objects = array($related_objects);
1767
                }
1768
                foreach ($related_objects as $related_object) {
1769
                    // only refresh their cache if they're in memory
1770
                    if ($related_object instanceof EE_Base_Class) {
1771
                        $related_object->clear_cache(
1772
                            $model->get_this_model_name(),
1773
                            $this
1774
                        );
1775
                    }
1776
                }
1777
            }
1778
        }
1779
    }
1780
1781
1782
    /**
1783
     *        Saves this object to the database. An array may be supplied to set some values on this
1784
     * object just before saving.
1785
     *
1786
     * @access public
1787
     * @param array $set_cols_n_values keys are field names, values are their new values,
1788
     *                                 if provided during the save() method (often client code will change the fields'
1789
     *                                 values before calling save)
1790
     * @throws InvalidArgumentException
1791
     * @throws InvalidInterfaceException
1792
     * @throws InvalidDataTypeException
1793
     * @throws EE_Error
1794
     * @return int , 1 on a successful update, the ID of the new entry on insert; 0 on failure or if the model object
1795
     *                                 isn't allowed to persist (as determined by EE_Base_Class::allow_persist())
1796
     * @throws ReflectionException
1797
     * @throws ReflectionException
1798
     * @throws ReflectionException
1799
     */
1800
    public function save($set_cols_n_values = array())
1801
    {
1802
        $model = $this->get_model();
1803
        /**
1804
         * Filters the fields we're about to save on the model object
1805
         *
1806
         * @param array         $set_cols_n_values
1807
         * @param EE_Base_Class $model_object
1808
         */
1809
        $set_cols_n_values = (array) apply_filters(
1810
            'FHEE__EE_Base_Class__save__set_cols_n_values',
1811
            $set_cols_n_values,
1812
            $this
1813
        );
1814
        // set attributes as provided in $set_cols_n_values
1815
        foreach ($set_cols_n_values as $column => $value) {
1816
            $this->set($column, $value);
1817
        }
1818
        // no changes ? then don't do anything
1819
        if (! $this->_has_changes && $this->ID() && $model->get_primary_key_field()->is_auto_increment()) {
1820
            return 0;
1821
        }
1822
        /**
1823
         * Saving a model object.
1824
         * Before we perform a save, this action is fired.
1825
         *
1826
         * @param EE_Base_Class $model_object the model object about to be saved.
1827
         */
1828
        do_action('AHEE__EE_Base_Class__save__begin', $this);
1829
        if (! $this->allow_persist()) {
1830
            return 0;
1831
        }
1832
        // now get current attribute values
1833
        $save_cols_n_values = $this->_fields;
1834
        // if the object already has an ID, update it. Otherwise, insert it
1835
        // also: change the assumption about values passed to the model NOT being prepare dby the model object.
1836
        // They have been
1837
        $old_assumption_concerning_value_preparation = $model
1838
            ->get_assumption_concerning_values_already_prepared_by_model_object();
1839
        $model->assume_values_already_prepared_by_model_object(true);
1840
        // does this model have an autoincrement PK?
1841
        if ($model->has_primary_key_field()) {
1842
            if ($model->get_primary_key_field()->is_auto_increment()) {
1843
                // ok check if it's set, if so: update; if not, insert
1844
                if (! empty($save_cols_n_values[ $model->primary_key_name() ])) {
1845
                    $results = $model->update_by_ID($save_cols_n_values, $this->ID());
1846
                } else {
1847
                    unset($save_cols_n_values[ $model->primary_key_name() ]);
1848
                    $results = $model->insert($save_cols_n_values);
1849
                    if ($results) {
1850
                        // if successful, set the primary key
1851
                        // but don't use the normal SET method, because it will check if
1852
                        // an item with the same ID exists in the mapper & db, then
1853
                        // will find it in the db (because we just added it) and THAT object
1854
                        // will get added to the mapper before we can add this one!
1855
                        // but if we just avoid using the SET method, all that headache can be avoided
1856
                        $pk_field_name = $model->primary_key_name();
1857
                        $this->_fields[ $pk_field_name ] = $results;
1858
                        $this->_clear_cached_property($pk_field_name);
1859
                        $model->add_to_entity_map($this);
1860
                        $this->_update_cached_related_model_objs_fks();
1861
                    }
1862
                }
1863
            } else {// PK is NOT auto-increment
1864
                // so check if one like it already exists in the db
1865
                if ($model->exists_by_ID($this->ID())) {
1866
                    if (WP_DEBUG && ! $this->in_entity_map()) {
1867
                        throw new EE_Error(
1868
                            sprintf(
1869
                                esc_html__(
1870
                                    'Using a model object %1$s that is NOT in the entity map, can lead to unexpected errors. You should either: %4$s 1. Put it in the entity mapper by calling %2$s %4$s 2. Discard this model object and use what is in the entity mapper %4$s 3. Fetch from the database using %3$s',
1871
                                    'event_espresso'
1872
                                ),
1873
                                get_class($this),
1874
                                get_class($model) . '::instance()->add_to_entity_map()',
1875
                                get_class($model) . '::instance()->get_one_by_ID()',
1876
                                '<br />'
1877
                            )
1878
                        );
1879
                    }
1880
                    $results = $model->update_by_ID($save_cols_n_values, $this->ID());
1881
                } else {
1882
                    $results = $model->insert($save_cols_n_values);
1883
                    $this->_update_cached_related_model_objs_fks();
1884
                }
1885
            }
1886
        } else {// there is NO primary key
1887
            $already_in_db = false;
1888
            foreach ($model->unique_indexes() as $index) {
1889
                $uniqueness_where_params = array_intersect_key($save_cols_n_values, $index->fields());
1890
                if ($model->exists(array($uniqueness_where_params))) {
1891
                    $already_in_db = true;
1892
                }
1893
            }
1894
            if ($already_in_db) {
1895
                $combined_pk_fields_n_values = array_intersect_key(
1896
                    $save_cols_n_values,
1897
                    $model->get_combined_primary_key_fields()
1898
                );
1899
                $results = $model->update(
1900
                    $save_cols_n_values,
1901
                    $combined_pk_fields_n_values
1902
                );
1903
            } else {
1904
                $results = $model->insert($save_cols_n_values);
1905
            }
1906
        }
1907
        // restore the old assumption about values being prepared by the model object
1908
        $model->assume_values_already_prepared_by_model_object(
1909
            $old_assumption_concerning_value_preparation
1910
        );
1911
        /**
1912
         * After saving the model object this action is called
1913
         *
1914
         * @param EE_Base_Class $model_object which was just saved
1915
         * @param boolean|int   $results      if it were updated, TRUE or FALSE; if it were newly inserted
1916
         *                                    the new ID (or 0 if an error occurred and it wasn't updated)
1917
         */
1918
        do_action('AHEE__EE_Base_Class__save__end', $this, $results);
1919
        $this->_has_changes = false;
1920
        return $results;
1921
    }
1922
1923
1924
    /**
1925
     * Updates the foreign key on related models objects pointing to this to have this model object's ID
1926
     * as their foreign key.  If the cached related model objects already exist in the db, saves them (so that the DB
1927
     * is consistent) Especially useful in case we JUST added this model object ot the database and we want to let its
1928
     * cached relations with foreign keys to it know about that change. Eg: we've created a transaction but haven't
1929
     * saved it to the db. We also create a registration and don't save it to the DB, but we DO cache it on the
1930
     * transaction. Now, when we save the transaction, the registration's TXN_ID will be automatically updated, whether
1931
     * or not they exist in the DB (if they do, their DB records will be automatically updated)
1932
     *
1933
     * @return void
1934
     * @throws ReflectionException
1935
     * @throws InvalidArgumentException
1936
     * @throws InvalidInterfaceException
1937
     * @throws InvalidDataTypeException
1938
     * @throws EE_Error
1939
     */
1940
    protected function _update_cached_related_model_objs_fks()
1941
    {
1942
        $model = $this->get_model();
1943
        foreach ($model->relation_settings() as $relation_name => $relation_obj) {
1944
            if ($relation_obj instanceof EE_Has_Many_Relation) {
1945
                foreach ($this->get_all_from_cache($relation_name) as $related_model_obj_in_cache) {
1946
                    $fk_to_this = $related_model_obj_in_cache->get_model()->get_foreign_key_to(
1947
                        $model->get_this_model_name()
1948
                    );
1949
                    $related_model_obj_in_cache->set($fk_to_this->get_name(), $this->ID());
1950
                    if ($related_model_obj_in_cache->ID()) {
1951
                        $related_model_obj_in_cache->save();
1952
                    }
1953
                }
1954
            }
1955
        }
1956
    }
1957
1958
1959
    /**
1960
     * Saves this model object and its NEW cached relations to the database.
1961
     * (Meaning, for now, IT DOES NOT WORK if the cached items already exist in the DB.
1962
     * In order for that to work, we would need to mark model objects as dirty/clean...
1963
     * because otherwise, there's a potential for infinite looping of saving
1964
     * Saves the cached related model objects, and ensures the relation between them
1965
     * and this object and properly setup
1966
     *
1967
     * @return int ID of new model object on save; 0 on failure+
1968
     * @throws ReflectionException
1969
     * @throws InvalidArgumentException
1970
     * @throws InvalidInterfaceException
1971
     * @throws InvalidDataTypeException
1972
     * @throws EE_Error
1973
     */
1974
    public function save_new_cached_related_model_objs()
1975
    {
1976
        // make sure this has been saved
1977
        if (! $this->ID()) {
1978
            $id = $this->save();
1979
        } else {
1980
            $id = $this->ID();
1981
        }
1982
        // now save all the NEW cached model objects  (ie they don't exist in the DB)
1983
        foreach ($this->get_model()->relation_settings() as $relationName => $relationObj) {
1984
            if ($this->_model_relations[ $relationName ]) {
1985
                // is this a relation where we should expect just ONE related object (ie, EE_Belongs_To_relation)
1986
                // or MANY related objects (ie, EE_HABTM_Relation or EE_Has_Many_Relation)?
1987
                /* @var $related_model_obj EE_Base_Class */
1988
                if ($relationObj instanceof EE_Belongs_To_Relation) {
1989
                    // add a relation to that relation type (which saves the appropriate thing in the process)
1990
                    // but ONLY if it DOES NOT exist in the DB
1991
                    $related_model_obj = $this->_model_relations[ $relationName ];
1992
                    // if( ! $related_model_obj->ID()){
1993
                    $this->_add_relation_to($related_model_obj, $relationName);
1994
                    $related_model_obj->save_new_cached_related_model_objs();
1995
                    // }
1996
                } else {
1997
                    foreach ($this->_model_relations[ $relationName ] as $related_model_obj) {
1998
                        // add a relation to that relation type (which saves the appropriate thing in the process)
1999
                        // but ONLY if it DOES NOT exist in the DB
2000
                        // if( ! $related_model_obj->ID()){
2001
                        $this->_add_relation_to($related_model_obj, $relationName);
2002
                        $related_model_obj->save_new_cached_related_model_objs();
2003
                        // }
2004
                    }
2005
                }
2006
            }
2007
        }
2008
        return $id;
2009
    }
2010
2011
2012
    /**
2013
     * for getting a model while instantiated.
2014
     *
2015
     * @return EEM_Base | EEM_CPT_Base
2016
     * @throws ReflectionException
2017
     * @throws InvalidArgumentException
2018
     * @throws InvalidInterfaceException
2019
     * @throws InvalidDataTypeException
2020
     * @throws EE_Error
2021
     */
2022
    public function get_model()
2023
    {
2024
        if (! $this->_model) {
2025
            $modelName = self::_get_model_classname(get_class($this));
2026
            $this->_model = self::_get_model_instance_with_name($modelName, $this->_timezone);
2027
        } else {
2028
            $this->_model->set_timezone($this->_timezone);
2029
        }
2030
        return $this->_model;
2031
    }
2032
2033
2034
    /**
2035
     * @param $props_n_values
2036
     * @param $classname
2037
     * @return mixed bool|EE_Base_Class|EEM_CPT_Base
2038
     * @throws ReflectionException
2039
     * @throws InvalidArgumentException
2040
     * @throws InvalidInterfaceException
2041
     * @throws InvalidDataTypeException
2042
     * @throws EE_Error
2043
     */
2044
    protected static function _get_object_from_entity_mapper($props_n_values, $classname)
2045
    {
2046
        // TODO: will not work for Term_Relationships because they have no PK!
2047
        $primary_id_ref = self::_get_primary_key_name($classname);
2048
        if (array_key_exists($primary_id_ref, $props_n_values)
2049
            && ! empty($props_n_values[ $primary_id_ref ])
2050
        ) {
2051
            $id = $props_n_values[ $primary_id_ref ];
2052
            return self::_get_model($classname)->get_from_entity_map($id);
2053
        }
2054
        return false;
2055
    }
2056
2057
2058
    /**
2059
     * This is called by child static "new_instance" method and we'll check to see if there is an existing db entry for
2060
     * the primary key (if present in incoming values). If there is a key in the incoming array that matches the
2061
     * primary key for the model AND it is not null, then we check the db. If there's a an object we return it.  If not
2062
     * we return false.
2063
     *
2064
     * @param  array  $props_n_values   incoming array of properties and their values
2065
     * @param  string $classname        the classname of the child class
2066
     * @param null    $timezone
2067
     * @param array   $date_formats     incoming date_formats in an array where the first value is the
2068
     *                                  date_format and the second value is the time format
2069
     * @return mixed (EE_Base_Class|bool)
2070
     * @throws InvalidArgumentException
2071
     * @throws InvalidInterfaceException
2072
     * @throws InvalidDataTypeException
2073
     * @throws EE_Error
2074
     * @throws ReflectionException
2075
     * @throws ReflectionException
2076
     * @throws ReflectionException
2077
     */
2078
    protected static function _check_for_object($props_n_values, $classname, $timezone = null, $date_formats = array())
2079
    {
2080
        $existing = null;
2081
        $model = self::_get_model($classname, $timezone);
2082
        if ($model->has_primary_key_field()) {
2083
            $primary_id_ref = self::_get_primary_key_name($classname);
2084
            if (array_key_exists($primary_id_ref, $props_n_values)
2085
                && ! empty($props_n_values[ $primary_id_ref ])
2086
            ) {
2087
                $existing = $model->get_one_by_ID(
2088
                    $props_n_values[ $primary_id_ref ]
2089
                );
2090
            }
2091
        } elseif ($model->has_all_combined_primary_key_fields($props_n_values)) {
2092
            // no primary key on this model, but there's still a matching item in the DB
2093
            $existing = self::_get_model($classname, $timezone)->get_one_by_ID(
2094
                self::_get_model($classname, $timezone)
2095
                    ->get_index_primary_key_string($props_n_values)
2096
            );
2097
        }
2098
        if ($existing) {
2099
            // set date formats if present before setting values
2100
            if (! empty($date_formats) && is_array($date_formats)) {
2101
                $existing->set_date_format($date_formats[0]);
2102
                $existing->set_time_format($date_formats[1]);
2103
            } else {
2104
                // set default formats for date and time
2105
                $existing->set_date_format(get_option('date_format'));
2106
                $existing->set_time_format(get_option('time_format'));
2107
            }
2108
            foreach ($props_n_values as $property => $field_value) {
2109
                $existing->set($property, $field_value);
2110
            }
2111
            return $existing;
2112
        }
2113
        return false;
2114
    }
2115
2116
2117
    /**
2118
     * Gets the EEM_*_Model for this class
2119
     *
2120
     * @access public now, as this is more convenient
2121
     * @param      $classname
2122
     * @param null $timezone
2123
     * @throws ReflectionException
2124
     * @throws InvalidArgumentException
2125
     * @throws InvalidInterfaceException
2126
     * @throws InvalidDataTypeException
2127
     * @throws EE_Error
2128
     * @return EEM_Base
2129
     */
2130
    protected static function _get_model($classname, $timezone = null)
2131
    {
2132
        // find model for this class
2133
        if (! $classname) {
2134
            throw new EE_Error(
2135
                sprintf(
2136
                    esc_html__(
2137
                        'What were you thinking calling _get_model(%s)?? You need to specify the class name',
2138
                        'event_espresso'
2139
                    ),
2140
                    $classname
2141
                )
2142
            );
2143
        }
2144
        $modelName = self::_get_model_classname($classname);
2145
        return self::_get_model_instance_with_name($modelName, $timezone);
2146
    }
2147
2148
2149
    /**
2150
     * Gets the model instance (eg instance of EEM_Attendee) given its classname (eg EE_Attendee)
2151
     *
2152
     * @param string $model_classname
2153
     * @param null   $timezone
2154
     * @return EEM_Base
2155
     * @throws ReflectionException
2156
     * @throws InvalidArgumentException
2157
     * @throws InvalidInterfaceException
2158
     * @throws InvalidDataTypeException
2159
     * @throws EE_Error
2160
     */
2161
    protected static function _get_model_instance_with_name($model_classname, $timezone = null)
2162
    {
2163
        $model_classname = str_replace('EEM_', '', $model_classname);
2164
        $model = EE_Registry::instance()->load_model($model_classname);
2165
        $model->set_timezone($timezone);
2166
        return $model;
2167
    }
2168
2169
2170
    /**
2171
     * If a model name is provided (eg Registration), gets the model classname for that model.
2172
     * Also works if a model class's classname is provided (eg EE_Registration).
2173
     *
2174
     * @param null $model_name
2175
     * @return string like EEM_Attendee
2176
     */
2177
    private static function _get_model_classname($model_name = null)
2178
    {
2179
        if (strpos($model_name, 'EE_') === 0) {
2180
            $model_classname = str_replace('EE_', 'EEM_', $model_name);
2181
        } else {
2182
            $model_classname = 'EEM_' . $model_name;
2183
        }
2184
        return $model_classname;
2185
    }
2186
2187
2188
    /**
2189
     * returns the name of the primary key attribute
2190
     *
2191
     * @param null $classname
2192
     * @throws ReflectionException
2193
     * @throws InvalidArgumentException
2194
     * @throws InvalidInterfaceException
2195
     * @throws InvalidDataTypeException
2196
     * @throws EE_Error
2197
     * @return string
2198
     */
2199
    protected static function _get_primary_key_name($classname = null)
2200
    {
2201
        if (! $classname) {
2202
            throw new EE_Error(
2203
                sprintf(
2204
                    esc_html__('What were you thinking calling _get_primary_key_name(%s)', 'event_espresso'),
2205
                    $classname
2206
                )
2207
            );
2208
        }
2209
        return self::_get_model($classname)->get_primary_key_field()->get_name();
2210
    }
2211
2212
2213
    /**
2214
     * Gets the value of the primary key.
2215
     * If the object hasn't yet been saved, it should be whatever the model field's default was
2216
     * (eg, if this were the EE_Event class, look at the primary key field on EEM_Event and see what its default value
2217
     * is. Usually defaults for integer primary keys are 0; string primary keys are usually NULL).
2218
     *
2219
     * @return mixed, if the primary key is of type INT it'll be an int. Otherwise it could be a string
2220
     * @throws ReflectionException
2221
     * @throws InvalidArgumentException
2222
     * @throws InvalidInterfaceException
2223
     * @throws InvalidDataTypeException
2224
     * @throws EE_Error
2225
     */
2226
    public function ID()
2227
    {
2228
        $model = $this->get_model();
2229
        // now that we know the name of the variable, use a variable variable to get its value and return its
2230
        if ($model->has_primary_key_field()) {
2231
            return $this->_fields[ $model->primary_key_name() ];
2232
        }
2233
        return $model->get_index_primary_key_string($this->_fields);
2234
    }
2235
2236
2237
    /**
2238
     * Adds a relationship to the specified EE_Base_Class object, given the relationship's name. Eg, if the current
2239
     * model is related to a group of events, the $relationName should be 'Event', and should be a key in the EE
2240
     * Model's $_model_relations array. If this model object doesn't exist in the DB, just caches the related thing
2241
     *
2242
     * @param mixed  $otherObjectModelObjectOrID       EE_Base_Class or the ID of the other object
2243
     * @param string $relationName                     eg 'Events','Question',etc.
2244
     *                                                 an attendee to a group, you also want to specify which role they
2245
     *                                                 will have in that group. So you would use this parameter to
2246
     *                                                 specify array('role-column-name'=>'role-id')
2247
     * @param array  $extra_join_model_fields_n_values You can optionally include an array of key=>value pairs that
2248
     *                                                 allow you to further constrict the relation to being added.
2249
     *                                                 However, keep in mind that the columns (keys) given must match a
2250
     *                                                 column on the JOIN table and currently only the HABTM models
2251
     *                                                 accept these additional conditions.  Also remember that if an
2252
     *                                                 exact match isn't found for these extra cols/val pairs, then a
2253
     *                                                 NEW row is created in the join table.
2254
     * @param null   $cache_id
2255
     * @throws ReflectionException
2256
     * @throws InvalidArgumentException
2257
     * @throws InvalidInterfaceException
2258
     * @throws InvalidDataTypeException
2259
     * @throws EE_Error
2260
     * @return EE_Base_Class the object the relation was added to
2261
     */
2262
    public function _add_relation_to(
2263
        $otherObjectModelObjectOrID,
2264
        $relationName,
2265
        $extra_join_model_fields_n_values = array(),
2266
        $cache_id = null
2267
    ) {
2268
        $model = $this->get_model();
2269
        // if this thing exists in the DB, save the relation to the DB
2270
        if ($this->ID()) {
2271
            $otherObject = $model->add_relationship_to(
2272
                $this,
2273
                $otherObjectModelObjectOrID,
2274
                $relationName,
2275
                $extra_join_model_fields_n_values
2276
            );
2277
            // clear cache so future get_many_related and get_first_related() return new results.
2278
            $this->clear_cache($relationName, $otherObject, true);
2279
            if ($otherObject instanceof EE_Base_Class) {
2280
                $otherObject->clear_cache($model->get_this_model_name(), $this);
2281
            }
2282
        } else {
2283
            // this thing doesn't exist in the DB,  so just cache it
2284 View Code Duplication
            if (! $otherObjectModelObjectOrID instanceof EE_Base_Class) {
2285
                throw new EE_Error(
2286
                    sprintf(
2287
                        esc_html__(
2288
                            'Before a model object is saved to the database, calls to _add_relation_to must be passed an actual object, not just an ID. You provided %s as the model object to a %s',
2289
                            'event_espresso'
2290
                        ),
2291
                        $otherObjectModelObjectOrID,
2292
                        get_class($this)
2293
                    )
2294
                );
2295
            }
2296
            $otherObject = $otherObjectModelObjectOrID;
2297
            $this->cache($relationName, $otherObjectModelObjectOrID, $cache_id);
2298
        }
2299
        if ($otherObject instanceof EE_Base_Class) {
2300
            // fix the reciprocal relation too
2301
            if ($otherObject->ID()) {
2302
                // its saved so assumed relations exist in the DB, so we can just
2303
                // clear the cache so future queries use the updated info in the DB
2304
                $otherObject->clear_cache(
2305
                    $model->get_this_model_name(),
2306
                    null,
2307
                    true
2308
                );
2309
            } else {
2310
                // it's not saved, so it caches relations like this
2311
                $otherObject->cache($model->get_this_model_name(), $this);
2312
            }
2313
        }
2314
        return $otherObject;
2315
    }
2316
2317
2318
    /**
2319
     * Removes a relationship to the specified EE_Base_Class object, given the relationships' name. Eg, if the current
2320
     * model is related to a group of events, the $relationName should be 'Events', and should be a key in the EE
2321
     * Model's $_model_relations array. If this model object doesn't exist in the DB, just removes the related thing
2322
     * from the cache
2323
     *
2324
     * @param mixed  $otherObjectModelObjectOrID
2325
     *                EE_Base_Class or the ID of the other object, OR an array key into the cache if this isn't saved
2326
     *                to the DB yet
2327
     * @param string $relationName
2328
     * @param array  $where_query
2329
     *                You can optionally include an array of key=>value pairs that allow you to further constrict the
2330
     *                relation to being added. However, keep in mind that the columns (keys) given must match a column
2331
     *                on the JOIN table and currently only the HABTM models accept these additional conditions. Also
2332
     *                remember that if an exact match isn't found for these extra cols/val pairs, then no row is
2333
     *                deleted.
2334
     * @return EE_Base_Class the relation was removed from
2335
     * @throws ReflectionException
2336
     * @throws InvalidArgumentException
2337
     * @throws InvalidInterfaceException
2338
     * @throws InvalidDataTypeException
2339
     * @throws EE_Error
2340
     */
2341
    public function _remove_relation_to($otherObjectModelObjectOrID, $relationName, $where_query = array())
2342
    {
2343
        if ($this->ID()) {
2344
            // if this exists in the DB, save the relation change to the DB too
2345
            $otherObject = $this->get_model()->remove_relationship_to(
2346
                $this,
2347
                $otherObjectModelObjectOrID,
2348
                $relationName,
2349
                $where_query
2350
            );
2351
            $this->clear_cache(
2352
                $relationName,
2353
                $otherObject
2354
            );
2355
        } else {
2356
            // this doesn't exist in the DB, just remove it from the cache
2357
            $otherObject = $this->clear_cache(
2358
                $relationName,
2359
                $otherObjectModelObjectOrID
2360
            );
2361
        }
2362
        if ($otherObject instanceof EE_Base_Class) {
2363
            $otherObject->clear_cache(
2364
                $this->get_model()->get_this_model_name(),
2365
                $this
2366
            );
2367
        }
2368
        return $otherObject;
2369
    }
2370
2371
2372
    /**
2373
     * Removes ALL the related things for the $relationName.
2374
     *
2375
     * @param string $relationName
2376
     * @param array  $where_query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
2377
     * @return EE_Base_Class
2378
     * @throws ReflectionException
2379
     * @throws InvalidArgumentException
2380
     * @throws InvalidInterfaceException
2381
     * @throws InvalidDataTypeException
2382
     * @throws EE_Error
2383
     */
2384
    public function _remove_relations($relationName, $where_query_params = array())
2385
    {
2386
        if ($this->ID()) {
2387
            // if this exists in the DB, save the relation change to the DB too
2388
            $otherObjects = $this->get_model()->remove_relations(
2389
                $this,
2390
                $relationName,
2391
                $where_query_params
2392
            );
2393
            $this->clear_cache(
2394
                $relationName,
2395
                null,
2396
                true
2397
            );
2398
        } else {
2399
            // this doesn't exist in the DB, just remove it from the cache
2400
            $otherObjects = $this->clear_cache(
2401
                $relationName,
2402
                null,
2403
                true
2404
            );
2405
        }
2406
        if (is_array($otherObjects)) {
2407
            foreach ($otherObjects as $otherObject) {
2408
                $otherObject->clear_cache(
2409
                    $this->get_model()->get_this_model_name(),
2410
                    $this
2411
                );
2412
            }
2413
        }
2414
        return $otherObjects;
2415
    }
2416
2417
2418
    /**
2419
     * Gets all the related model objects of the specified type. Eg, if the current class if
2420
     * EE_Event, you could call $this->get_many_related('Registration') to get an array of all the
2421
     * EE_Registration objects which related to this event. Note: by default, we remove the "default query params"
2422
     * because we want to get even deleted items etc.
2423
     *
2424
     * @param string $relationName key in the model's _model_relations array
2425
     * @param array  $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
2426
     * @return EE_Base_Class[]     Results not necessarily indexed by IDs, because some results might not have primary
2427
     *                             keys or might not be saved yet. Consider using EEM_Base::get_IDs() on these
2428
     *                             results if you want IDs
2429
     * @throws ReflectionException
2430
     * @throws InvalidArgumentException
2431
     * @throws InvalidInterfaceException
2432
     * @throws InvalidDataTypeException
2433
     * @throws EE_Error
2434
     */
2435
    public function get_many_related($relationName, $query_params = array())
2436
    {
2437
        if ($this->ID()) {
2438
            // this exists in the DB, so get the related things from either the cache or the DB
2439
            // if there are query parameters, forget about caching the related model objects.
2440
            if ($query_params) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $query_params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2441
                $related_model_objects = $this->get_model()->get_all_related(
2442
                    $this,
2443
                    $relationName,
2444
                    $query_params
2445
                );
2446
            } else {
2447
                // did we already cache the result of this query?
2448
                $cached_results = $this->get_all_from_cache($relationName);
2449
                if (! $cached_results) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cached_results of type EE_Base_Class[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2450
                    $related_model_objects = $this->get_model()->get_all_related(
2451
                        $this,
2452
                        $relationName,
2453
                        $query_params
2454
                    );
2455
                    // if no query parameters were passed, then we got all the related model objects
2456
                    // for that relation. We can cache them then.
2457
                    foreach ($related_model_objects as $related_model_object) {
2458
                        $this->cache($relationName, $related_model_object);
2459
                    }
2460
                } else {
2461
                    $related_model_objects = $cached_results;
2462
                }
2463
            }
2464
        } else {
2465
            // this doesn't exist in the DB, so just get the related things from the cache
2466
            $related_model_objects = $this->get_all_from_cache($relationName);
2467
        }
2468
        return $related_model_objects;
2469
    }
2470
2471
2472
    /**
2473
     * Instead of getting the related model objects, simply counts them. Ignores default_where_conditions by default,
2474
     * unless otherwise specified in the $query_params
2475
     *
2476
     * @param string $relation_name  model_name like 'Event', or 'Registration'
2477
     * @param array  $query_params   @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
2478
     * @param string $field_to_count name of field to count by. By default, uses primary key
2479
     * @param bool   $distinct       if we want to only count the distinct values for the column then you can trigger
2480
     *                               that by the setting $distinct to TRUE;
2481
     * @return int
2482
     * @throws ReflectionException
2483
     * @throws InvalidArgumentException
2484
     * @throws InvalidInterfaceException
2485
     * @throws InvalidDataTypeException
2486
     * @throws EE_Error
2487
     */
2488
    public function count_related($relation_name, $query_params = array(), $field_to_count = null, $distinct = false)
2489
    {
2490
        return $this->get_model()->count_related(
2491
            $this,
2492
            $relation_name,
2493
            $query_params,
2494
            $field_to_count,
2495
            $distinct
2496
        );
2497
    }
2498
2499
2500
    /**
2501
     * Instead of getting the related model objects, simply sums up the values of the specified field.
2502
     * Note: ignores default_where_conditions by default, unless otherwise specified in the $query_params
2503
     *
2504
     * @param string $relation_name model_name like 'Event', or 'Registration'
2505
     * @param array  $query_params  @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
2506
     * @param string $field_to_sum  name of field to count by.
2507
     *                              By default, uses primary key
2508
     *                              (which doesn't make much sense, so you should probably change it)
2509
     * @return int
2510
     * @throws ReflectionException
2511
     * @throws InvalidArgumentException
2512
     * @throws InvalidInterfaceException
2513
     * @throws InvalidDataTypeException
2514
     * @throws EE_Error
2515
     */
2516
    public function sum_related($relation_name, $query_params = array(), $field_to_sum = null)
2517
    {
2518
        return $this->get_model()->sum_related(
2519
            $this,
2520
            $relation_name,
2521
            $query_params,
2522
            $field_to_sum
2523
        );
2524
    }
2525
2526
2527
    /**
2528
     * Gets the first (ie, one) related model object of the specified type.
2529
     *
2530
     * @param string $relationName key in the model's _model_relations array
2531
     * @param array  $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
2532
     * @return EE_Base_Class (not an array, a single object)
2533
     * @throws ReflectionException
2534
     * @throws InvalidArgumentException
2535
     * @throws InvalidInterfaceException
2536
     * @throws InvalidDataTypeException
2537
     * @throws EE_Error
2538
     */
2539
    public function get_first_related($relationName, $query_params = array())
2540
    {
2541
        $model = $this->get_model();
2542
        if ($this->ID()) {// this exists in the DB, get from the cache OR the DB
2543
            // if they've provided some query parameters, don't bother trying to cache the result
2544
            // also make sure we're not caching the result of get_first_related
2545
            // on a relation which should have an array of objects (because the cache might have an array of objects)
2546
            if ($query_params
0 ignored issues
show
Bug Best Practice introduced by
The expression $query_params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2547
                || ! $model->related_settings_for($relationName)
2548
                     instanceof
2549
                     EE_Belongs_To_Relation
2550
            ) {
2551
                $related_model_object = $model->get_first_related(
2552
                    $this,
2553
                    $relationName,
2554
                    $query_params
2555
                );
2556
            } else {
2557
                // first, check if we've already cached the result of this query
2558
                $cached_result = $this->get_one_from_cache($relationName);
2559
                if (! $cached_result) {
2560
                    $related_model_object = $model->get_first_related(
2561
                        $this,
2562
                        $relationName,
2563
                        $query_params
2564
                    );
2565
                    $this->cache($relationName, $related_model_object);
2566
                } else {
2567
                    $related_model_object = $cached_result;
2568
                }
2569
            }
2570
        } else {
2571
            $related_model_object = null;
2572
            // this doesn't exist in the Db,
2573
            // but maybe the relation is of type belongs to, and so the related thing might
2574
            if ($model->related_settings_for($relationName) instanceof EE_Belongs_To_Relation) {
2575
                $related_model_object = $model->get_first_related(
2576
                    $this,
2577
                    $relationName,
2578
                    $query_params
2579
                );
2580
            }
2581
            // this doesn't exist in the DB and apparently the thing it belongs to doesn't either,
2582
            // just get what's cached on this object
2583
            if (! $related_model_object) {
2584
                $related_model_object = $this->get_one_from_cache($relationName);
2585
            }
2586
        }
2587
        return $related_model_object;
2588
    }
2589
2590
2591
    /**
2592
     * Does a delete on all related objects of type $relationName and removes
2593
     * the current model object's relation to them. If they can't be deleted (because
2594
     * of blocking related model objects) does nothing. If the related model objects are
2595
     * soft-deletable, they will be soft-deleted regardless of related blocking model objects.
2596
     * If this model object doesn't exist yet in the DB, just removes its related things
2597
     *
2598
     * @param string $relationName
2599
     * @param array  $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
2600
     * @return int how many deleted
2601
     * @throws ReflectionException
2602
     * @throws InvalidArgumentException
2603
     * @throws InvalidInterfaceException
2604
     * @throws InvalidDataTypeException
2605
     * @throws EE_Error
2606
     */
2607 View Code Duplication
    public function delete_related($relationName, $query_params = array())
2608
    {
2609
        if ($this->ID()) {
2610
            $count = $this->get_model()->delete_related(
2611
                $this,
2612
                $relationName,
2613
                $query_params
2614
            );
2615
        } else {
2616
            $count = count($this->get_all_from_cache($relationName));
2617
            $this->clear_cache($relationName, null, true);
2618
        }
2619
        return $count;
2620
    }
2621
2622
2623
    /**
2624
     * Does a hard delete (ie, removes the DB row) on all related objects of type $relationName and removes
2625
     * the current model object's relation to them. If they can't be deleted (because
2626
     * of blocking related model objects) just does a soft delete on it instead, if possible.
2627
     * If the related thing isn't a soft-deletable model object, this function is identical
2628
     * to delete_related(). If this model object doesn't exist in the DB, just remove its related things
2629
     *
2630
     * @param string $relationName
2631
     * @param array  $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
2632
     * @return int how many deleted (including those soft deleted)
2633
     * @throws ReflectionException
2634
     * @throws InvalidArgumentException
2635
     * @throws InvalidInterfaceException
2636
     * @throws InvalidDataTypeException
2637
     * @throws EE_Error
2638
     */
2639 View Code Duplication
    public function delete_related_permanently($relationName, $query_params = array())
2640
    {
2641
        if ($this->ID()) {
2642
            $count = $this->get_model()->delete_related_permanently(
2643
                $this,
2644
                $relationName,
2645
                $query_params
2646
            );
2647
        } else {
2648
            $count = count($this->get_all_from_cache($relationName));
2649
        }
2650
        $this->clear_cache($relationName, null, true);
2651
        return $count;
2652
    }
2653
2654
2655
    /**
2656
     * is_set
2657
     * Just a simple utility function children can use for checking if property exists
2658
     *
2659
     * @access  public
2660
     * @param  string $field_name property to check
2661
     * @return bool                              TRUE if existing,FALSE if not.
2662
     */
2663
    public function is_set($field_name)
2664
    {
2665
        return isset($this->_fields[ $field_name ]);
2666
    }
2667
2668
2669
    /**
2670
     * Just a simple utility function children can use for checking if property (or properties) exists and throwing an
2671
     * EE_Error exception if they don't
2672
     *
2673
     * @param  mixed (string|array) $properties properties to check
2674
     * @throws EE_Error
2675
     * @return bool                              TRUE if existing, throw EE_Error if not.
2676
     */
2677
    protected function _property_exists($properties)
2678
    {
2679
        foreach ((array) $properties as $property_name) {
2680
            // first make sure this property exists
2681
            if (! $this->_fields[ $property_name ]) {
2682
                throw new EE_Error(
2683
                    sprintf(
2684
                        esc_html__(
2685
                            'Trying to retrieve a non-existent property (%s).  Double check the spelling please',
2686
                            'event_espresso'
2687
                        ),
2688
                        $property_name
2689
                    )
2690
                );
2691
            }
2692
        }
2693
        return true;
2694
    }
2695
2696
2697
    /**
2698
     * This simply returns an array of model fields for this object
2699
     *
2700
     * @return array
2701
     * @throws ReflectionException
2702
     * @throws InvalidArgumentException
2703
     * @throws InvalidInterfaceException
2704
     * @throws InvalidDataTypeException
2705
     * @throws EE_Error
2706
     */
2707
    public function model_field_array()
2708
    {
2709
        $fields = $this->get_model()->field_settings(false);
2710
        $properties = array();
2711
        // remove prepended underscore
2712
        foreach ($fields as $field_name => $settings) {
2713
            $properties[ $field_name ] = $this->get($field_name);
2714
        }
2715
        return $properties;
2716
    }
2717
2718
2719
    /**
2720
     * Very handy general function to allow for plugins to extend any child of EE_Base_Class.
2721
     * If a method is called on a child of EE_Base_Class that doesn't exist, this function is called
2722
     * (http://www.garfieldtech.com/blog/php-magic-call) and passed the method's name and arguments.
2723
     * Instead of requiring a plugin to extend the EE_Base_Class
2724
     * (which works fine is there's only 1 plugin, but when will that happen?)
2725
     * they can add a hook onto 'filters_hook_espresso__{className}__{methodName}'
2726
     * (eg, filters_hook_espresso__EE_Answer__my_great_function)
2727
     * and accepts 2 arguments: the object on which the function was called,
2728
     * and an array of the original arguments passed to the function.
2729
     * Whatever their callback function returns will be returned by this function.
2730
     * Example: in functions.php (or in a plugin):
2731
     *      add_filter('FHEE__EE_Answer__my_callback','my_callback',10,3);
2732
     *      function my_callback($previousReturnValue,EE_Base_Class $object,$argsArray){
2733
     *          $returnString= "you called my_callback! and passed args:".implode(",",$argsArray);
2734
     *          return $previousReturnValue.$returnString;
2735
     *      }
2736
     * require('EE_Answer.class.php');
2737
     * $answer= EE_Answer::new_instance(array('REG_ID' => 2,'QST_ID' => 3,'ANS_value' => The answer is 42'));
2738
     * echo $answer->my_callback('monkeys',100);
2739
     * //will output "you called my_callback! and passed args:monkeys,100"
2740
     *
2741
     * @param string $methodName name of method which was called on a child of EE_Base_Class, but which
2742
     * @param array  $args       array of original arguments passed to the function
2743
     * @throws EE_Error
2744
     * @return mixed whatever the plugin which calls add_filter decides
2745
     */
2746
    public function __call($methodName, $args)
2747
    {
2748
        $className = get_class($this);
2749
        $tagName = "FHEE__{$className}__{$methodName}";
2750
        if (! has_filter($tagName)) {
2751
            throw new EE_Error(
2752
                sprintf(
2753
                    esc_html__(
2754
                        "Method %s on class %s does not exist! You can create one with the following code in functions.php or in a plugin: add_filter('%s','my_callback',10,3);function my_callback(\$previousReturnValue,EE_Base_Class \$object, \$argsArray){/*function body*/return \$whatever;}",
2755
                        'event_espresso'
2756
                    ),
2757
                    $methodName,
2758
                    $className,
2759
                    $tagName
2760
                )
2761
            );
2762
        }
2763
        return apply_filters($tagName, null, $this, $args);
2764
    }
2765
2766
2767
    /**
2768
     * Similar to insert_post_meta, adds a record in the Extra_Meta model's table with the given key and value.
2769
     * A $previous_value can be specified in case there are many meta rows with the same key
2770
     *
2771
     * @param string $meta_key
2772
     * @param mixed  $meta_value
2773
     * @param mixed  $previous_value
2774
     * @return bool|int # of records updated (or BOOLEAN if we actually ended up inserting the extra meta row)
2775
     *                  NOTE: if the values haven't changed, returns 0
2776
     * @throws InvalidArgumentException
2777
     * @throws InvalidInterfaceException
2778
     * @throws InvalidDataTypeException
2779
     * @throws EE_Error
2780
     * @throws ReflectionException
2781
     */
2782
    public function update_extra_meta($meta_key, $meta_value, $previous_value = null)
2783
    {
2784
        $query_params = array(
2785
            array(
2786
                'EXM_key'  => $meta_key,
2787
                'OBJ_ID'   => $this->ID(),
2788
                'EXM_type' => $this->get_model()->get_this_model_name(),
2789
            ),
2790
        );
2791
        if ($previous_value !== null) {
2792
            $query_params[0]['EXM_value'] = $meta_value;
2793
        }
2794
        $existing_rows_like_that = EEM_Extra_Meta::instance()->get_all($query_params);
2795
        if (! $existing_rows_like_that) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $existing_rows_like_that of type EE_Base_Class[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2796
            return $this->add_extra_meta($meta_key, $meta_value);
2797
        }
2798
        foreach ($existing_rows_like_that as $existing_row) {
2799
            $existing_row->save(array('EXM_value' => $meta_value));
2800
        }
2801
        return count($existing_rows_like_that);
2802
    }
2803
2804
2805
    /**
2806
     * Adds a new extra meta record. If $unique is set to TRUE, we'll first double-check
2807
     * no other extra meta for this model object have the same key. Returns TRUE if the
2808
     * extra meta row was entered, false if not
2809
     *
2810
     * @param string  $meta_key
2811
     * @param mixed   $meta_value
2812
     * @param boolean $unique
2813
     * @return boolean
2814
     * @throws InvalidArgumentException
2815
     * @throws InvalidInterfaceException
2816
     * @throws InvalidDataTypeException
2817
     * @throws EE_Error
2818
     * @throws ReflectionException
2819
     * @throws ReflectionException
2820
     */
2821
    public function add_extra_meta($meta_key, $meta_value, $unique = false)
2822
    {
2823
        if ($unique) {
2824
            $existing_extra_meta = EEM_Extra_Meta::instance()->get_one(
2825
                array(
2826
                    array(
2827
                        'EXM_key'  => $meta_key,
2828
                        'OBJ_ID'   => $this->ID(),
2829
                        'EXM_type' => $this->get_model()->get_this_model_name(),
2830
                    ),
2831
                )
2832
            );
2833
            if ($existing_extra_meta) {
2834
                return false;
2835
            }
2836
        }
2837
        $new_extra_meta = EE_Extra_Meta::new_instance(
2838
            array(
2839
                'EXM_key'   => $meta_key,
2840
                'EXM_value' => $meta_value,
2841
                'OBJ_ID'    => $this->ID(),
2842
                'EXM_type'  => $this->get_model()->get_this_model_name(),
2843
            )
2844
        );
2845
        $new_extra_meta->save();
2846
        return true;
2847
    }
2848
2849
2850
    /**
2851
     * Deletes all the extra meta rows for this record as specified by key. If $meta_value
2852
     * is specified, only deletes extra meta records with that value.
2853
     *
2854
     * @param string $meta_key
2855
     * @param mixed  $meta_value
2856
     * @return int number of extra meta rows deleted
2857
     * @throws InvalidArgumentException
2858
     * @throws InvalidInterfaceException
2859
     * @throws InvalidDataTypeException
2860
     * @throws EE_Error
2861
     * @throws ReflectionException
2862
     */
2863
    public function delete_extra_meta($meta_key, $meta_value = null)
2864
    {
2865
        $query_params = array(
2866
            array(
2867
                'EXM_key'  => $meta_key,
2868
                'OBJ_ID'   => $this->ID(),
2869
                'EXM_type' => $this->get_model()->get_this_model_name(),
2870
            ),
2871
        );
2872
        if ($meta_value !== null) {
2873
            $query_params[0]['EXM_value'] = $meta_value;
2874
        }
2875
        return EEM_Extra_Meta::instance()->delete($query_params);
2876
    }
2877
2878
2879
    /**
2880
     * Gets the extra meta with the given meta key. If you specify "single" we just return 1, otherwise
2881
     * an array of everything found. Requires that this model actually have a relation of type EE_Has_Many_Any_Relation.
2882
     * You can specify $default is case you haven't found the extra meta
2883
     *
2884
     * @param string  $meta_key
2885
     * @param boolean $single
2886
     * @param mixed   $default if we don't find anything, what should we return?
2887
     * @return mixed single value if $single; array if ! $single
2888
     * @throws ReflectionException
2889
     * @throws InvalidArgumentException
2890
     * @throws InvalidInterfaceException
2891
     * @throws InvalidDataTypeException
2892
     * @throws EE_Error
2893
     */
2894
    public function get_extra_meta($meta_key, $single = false, $default = null)
2895
    {
2896
        if ($single) {
2897
            $result = $this->get_first_related(
2898
                'Extra_Meta',
2899
                array(array('EXM_key' => $meta_key))
2900
            );
2901
            if ($result instanceof EE_Extra_Meta) {
2902
                return $result->value();
2903
            }
2904
        } else {
2905
            $results = $this->get_many_related(
2906
                'Extra_Meta',
2907
                array(array('EXM_key' => $meta_key))
2908
            );
2909
            if ($results) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type EE_Base_Class[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2910
                $values = array();
2911
                foreach ($results as $result) {
2912
                    if ($result instanceof EE_Extra_Meta) {
2913
                        $values[ $result->ID() ] = $result->value();
2914
                    }
2915
                }
2916
                return $values;
2917
            }
2918
        }
2919
        // if nothing discovered yet return default.
2920
        return apply_filters(
2921
            'FHEE__EE_Base_Class__get_extra_meta__default_value',
2922
            $default,
2923
            $meta_key,
2924
            $single,
2925
            $this
2926
        );
2927
    }
2928
2929
2930
    /**
2931
     * Returns a simple array of all the extra meta associated with this model object.
2932
     * If $one_of_each_key is true (Default), it will be an array of simple key-value pairs, keys being the
2933
     * extra meta's key, and teh value being its value. However, if there are duplicate extra meta rows with
2934
     * the same key, only one will be used. (eg array('foo'=>'bar','monkey'=>123))
2935
     * If $one_of_each_key is false, it will return an array with the top-level keys being
2936
     * the extra meta keys, but their values are also arrays, which have the extra-meta's ID as their sub-key, and
2937
     * finally the extra meta's value as each sub-value. (eg
2938
     * array('foo'=>array(1=>'bar',2=>'bill'),'monkey'=>array(3=>123)))
2939
     *
2940
     * @param boolean $one_of_each_key
2941
     * @return array
2942
     * @throws ReflectionException
2943
     * @throws InvalidArgumentException
2944
     * @throws InvalidInterfaceException
2945
     * @throws InvalidDataTypeException
2946
     * @throws EE_Error
2947
     */
2948
    public function all_extra_meta_array($one_of_each_key = true)
2949
    {
2950
        $return_array = array();
2951
        if ($one_of_each_key) {
2952
            $extra_meta_objs = $this->get_many_related(
2953
                'Extra_Meta',
2954
                array('group_by' => 'EXM_key')
2955
            );
2956
            foreach ($extra_meta_objs as $extra_meta_obj) {
2957
                if ($extra_meta_obj instanceof EE_Extra_Meta) {
2958
                    $return_array[ $extra_meta_obj->key() ] = $extra_meta_obj->value();
2959
                }
2960
            }
2961
        } else {
2962
            $extra_meta_objs = $this->get_many_related('Extra_Meta');
2963
            foreach ($extra_meta_objs as $extra_meta_obj) {
2964
                if ($extra_meta_obj instanceof EE_Extra_Meta) {
2965
                    if (! isset($return_array[ $extra_meta_obj->key() ])) {
2966
                        $return_array[ $extra_meta_obj->key() ] = array();
2967
                    }
2968
                    $return_array[ $extra_meta_obj->key() ][ $extra_meta_obj->ID() ] = $extra_meta_obj->value();
2969
                }
2970
            }
2971
        }
2972
        return $return_array;
2973
    }
2974
2975
2976
    /**
2977
     * Gets a pretty nice displayable nice for this model object. Often overridden
2978
     *
2979
     * @return string
2980
     * @throws ReflectionException
2981
     * @throws InvalidArgumentException
2982
     * @throws InvalidInterfaceException
2983
     * @throws InvalidDataTypeException
2984
     * @throws EE_Error
2985
     */
2986
    public function name()
2987
    {
2988
        // find a field that's not a text field
2989
        $field_we_can_use = $this->get_model()->get_a_field_of_type('EE_Text_Field_Base');
2990
        if ($field_we_can_use) {
2991
            return $this->get($field_we_can_use->get_name());
2992
        }
2993
        $first_few_properties = $this->model_field_array();
2994
        $first_few_properties = array_slice($first_few_properties, 0, 3);
2995
        $name_parts = array();
2996
        foreach ($first_few_properties as $name => $value) {
2997
            $name_parts[] = "$name:$value";
2998
        }
2999
        return implode(',', $name_parts);
3000
    }
3001
3002
3003
    /**
3004
     * in_entity_map
3005
     * Checks if this model object has been proven to already be in the entity map
3006
     *
3007
     * @return boolean
3008
     * @throws ReflectionException
3009
     * @throws InvalidArgumentException
3010
     * @throws InvalidInterfaceException
3011
     * @throws InvalidDataTypeException
3012
     * @throws EE_Error
3013
     */
3014
    public function in_entity_map()
3015
    {
3016
        // well, if we looked, did we find it in the entity map?
3017
        return $this->ID() && $this->get_model()->get_from_entity_map($this->ID()) === $this;
3018
    }
3019
3020
3021
    /**
3022
     * refresh_from_db
3023
     * Makes sure the fields and values on this model object are in-sync with what's in the database.
3024
     *
3025
     * @throws ReflectionException
3026
     * @throws InvalidArgumentException
3027
     * @throws InvalidInterfaceException
3028
     * @throws InvalidDataTypeException
3029
     * @throws EE_Error if this model object isn't in the entity mapper (because then you should
3030
     * just use what's in the entity mapper and refresh it) and WP_DEBUG is TRUE
3031
     */
3032
    public function refresh_from_db()
3033
    {
3034
        if ($this->ID() && $this->in_entity_map()) {
3035
            $this->get_model()->refresh_entity_map_from_db($this->ID());
3036
        } else {
3037
            // if it doesn't have ID, you shouldn't be asking to refresh it from teh database (because its not in the database)
3038
            // if it has an ID but it's not in the map, and you're asking me to refresh it
3039
            // that's kinda dangerous. You should just use what's in the entity map, or add this to the entity map if there's
3040
            // absolutely nothing in it for this ID
3041
            if (WP_DEBUG) {
3042
                throw new EE_Error(
3043
                    sprintf(
3044
                        esc_html__(
3045
                            'Trying to refresh a model object with ID "%1$s" that\'s not in the entity map? First off: you should put it in the entity map by calling %2$s. Second off, if you want what\'s in the database right now, you should just call %3$s yourself and discard this model object.',
3046
                            'event_espresso'
3047
                        ),
3048
                        $this->ID(),
3049
                        get_class($this->get_model()) . '::instance()->add_to_entity_map()',
3050
                        get_class($this->get_model()) . '::instance()->refresh_entity_map()'
3051
                    )
3052
                );
3053
            }
3054
        }
3055
    }
3056
3057
3058
    /**
3059
     * Change $fields' values to $new_value_sql (which is a string of raw SQL)
3060
     *
3061
     * @since $VID:$
3062
     * @param EE_Model_Field_Base[] $fields
3063
     * @param string $new_value_sql
3064
     *      example: 'column_name=123',
3065
     *      or 'column_name=column_name+1',
3066
     *      or 'column_name= CASE
3067
     *          WHEN (`column_name` + `other_column` + 5) <= `yet_another_column`
3068
     *          THEN `column_name` + 5
3069
     *          ELSE `column_name`
3070
     *      END'
3071
     *      Also updates $field on this model object with the latest value from the database.
3072
     * @return bool
3073
     * @throws EE_Error
3074
     * @throws InvalidArgumentException
3075
     * @throws InvalidDataTypeException
3076
     * @throws InvalidInterfaceException
3077
     * @throws ReflectionException
3078
     */
3079
    protected function updateFieldsInDB($fields, $new_value_sql)
3080
    {
3081
        // First make sure this model object actually exists in the DB. It would be silly to try to update it in the DB
3082
        // if it wasn't even there to start off.
3083
        if (! $this->ID()) {
3084
            $this->save();
3085
        }
3086
        global $wpdb;
3087
        if (empty($fields)) {
3088
            throw new InvalidArgumentException(
3089
                esc_html__(
3090
                    'EE_Base_Class::updateFieldsInDB was passed an empty array of fields.',
3091
                    'event_espresso'
3092
                )
3093
            );
3094
        }
3095
        $first_field = reset($fields);
3096
        $table_alias = $first_field->get_table_alias();
3097
        foreach ($fields as $field) {
3098
            if ($table_alias !== $field->get_table_alias()) {
3099
                throw new InvalidArgumentException(
3100
                    sprintf(
3101
                        esc_html__(
3102
                            // @codingStandardsIgnoreStart
3103
                            'EE_Base_Class::updateFieldsInDB was passed fields for different tables ("%1$s" and "%2$s"), which is not supported. Instead, please call the method multiple times.',
3104
                            // @codingStandardsIgnoreEnd
3105
                            'event_espresso'
3106
                        ),
3107
                        $table_alias,
3108
                        $field->get_table_alias()
3109
                    )
3110
                );
3111
            }
3112
        }
3113
        // Ok the fields are now known to all be for the same table. Proceed with creating the SQL to update it.
3114
        $table_obj = $this->get_model()->get_table_obj_by_alias($table_alias);
3115
        $table_pk_value = $this->ID();
3116
        $table_name = $table_obj->get_table_name();
3117
        if ($table_obj instanceof EE_Secondary_Table) {
3118
            $table_pk_field_name = $table_obj->get_fk_on_table();
3119
        } else {
3120
            $table_pk_field_name = $table_obj->get_pk_column();
3121
        }
3122
3123
        $query =
3124
            "UPDATE `{$table_name}`
3125
            SET "
3126
            . $new_value_sql
3127
            . $wpdb->prepare(
3128
                "
3129
            WHERE `{$table_pk_field_name}` = %d;",
3130
                $table_pk_value
3131
            );
3132
        $result = $wpdb->query($query);
3133
        foreach ($fields as $field) {
3134
            // If it was successful, we'd like to know the new value.
3135
            // If it failed, we'd also like to know the new value.
3136
            $new_value = $this->get_model()->get_var(
3137
                $this->get_model()->alter_query_params_to_restrict_by_ID(
3138
                    $this->get_model()->get_index_primary_key_string(
3139
                        $this->model_field_array()
3140
                    ),
3141
                    array(
3142
                        'default_where_conditions' => 'minimum',
3143
                    )
3144
                ),
3145
                $field->get_name()
3146
            );
3147
            $this->set_from_db(
3148
                $field->get_name(),
3149
                $new_value
3150
            );
3151
        }
3152
        return (bool) $result;
3153
    }
3154
3155
3156
    /**
3157
     * Nudges $field_name's value by $quantity, without any conditionals (in comparison to bumpConditionally()).
3158
     * Does not allow negative values, however.
3159
     *
3160
     * @since $VID:$
3161
     * @param array $fields_n_quantities keys are the field names, and values are the amount by which to bump them
3162
     *                                   (positive or negative). One important gotcha: all these values must be
3163
     *                                   on the same table (eg don't pass in one field for the posts table and
3164
     *                                   another for the event meta table.)
3165
     * @return bool
3166
     * @throws EE_Error
3167
     * @throws InvalidArgumentException
3168
     * @throws InvalidDataTypeException
3169
     * @throws InvalidInterfaceException
3170
     * @throws ReflectionException
3171
     */
3172
    public function adjustNumericFieldsInDb(array $fields_n_quantities)
3173
    {
3174
        global $wpdb;
3175
        if (empty($fields_n_quantities)) {
3176
            // No fields to update? Well sure, we updated them to that value just fine.
3177
            return true;
3178
        }
3179
        $fields = [];
3180
        $set_sql_statements = [];
3181
        foreach ($fields_n_quantities as $field_name => $quantity) {
3182
            $field = $this->get_model()->field_settings_for($field_name, true);
3183
            $fields[] = $field;
3184
            $column_name = $field->get_table_column();
3185
3186
            $abs_qty = absint($quantity);
3187
            if ($quantity > 0) {
3188
                // don't let the value be negative as often these fields are unsigned
3189
                $set_sql_statements[] = $wpdb->prepare(
3190
                    "`{$column_name}` = `{$column_name}` + %d",
3191
                    $abs_qty
3192
                );
3193
            } else {
3194
                $set_sql_statements[] = $wpdb->prepare(
3195
                    "`{$column_name}` = CASE
3196
                       WHEN (`{$column_name}` >= %d)
3197
                       THEN `{$column_name}` - %d
3198
                       ELSE 0
3199
                    END",
3200
                    $abs_qty,
3201
                    $abs_qty
3202
                );
3203
            }
3204
        }
3205
        return $this->updateFieldsInDB(
3206
            $fields,
3207
            implode(', ', $set_sql_statements)
3208
        );
3209
    }
3210
3211
3212
    /**
3213
     * Increases the value of the field $field_name_to_bump by $quantity, but only if the values of
3214
     * $field_name_to_bump plus $field_name_affecting_total and $quantity won't exceed $limit_field_name's value.
3215
     * For example, this is useful when bumping the value of TKT_reserved, TKT_sold, DTT_reserved or DTT_sold.
3216
     * Returns true if the value was successfully bumped, and updates the value on this model object.
3217
     * Otherwise returns false.
3218
     *
3219
     * @since $VID:$
3220
     * @param string $field_name_to_bump
3221
     * @param string $field_name_affecting_total
3222
     * @param string $limit_field_name
3223
     * @param int    $quantity
3224
     * @return bool
3225
     * @throws EE_Error
3226
     * @throws InvalidArgumentException
3227
     * @throws InvalidDataTypeException
3228
     * @throws InvalidInterfaceException
3229
     * @throws ReflectionException
3230
     */
3231
    public function incrementFieldConditionallyInDb($field_name_to_bump, $field_name_affecting_total, $limit_field_name, $quantity)
3232
    {
3233
        global $wpdb;
3234
        $field = $this->get_model()->field_settings_for($field_name_to_bump, true);
3235
        $column_name = $field->get_table_column();
3236
3237
        $field_affecting_total = $this->get_model()->field_settings_for($field_name_affecting_total, true);
3238
        $column_affecting_total = $field_affecting_total->get_table_column();
3239
3240
        $limiting_field = $this->get_model()->field_settings_for($limit_field_name, true);
3241
        $limiting_column = $limiting_field->get_table_column();
3242
        return $this->updateFieldsInDB(
3243
            [$field],
3244
            $wpdb->prepare(
3245
                "`{$column_name}` =
3246
            CASE
3247
               WHEN ((`{$column_name}` + `{$column_affecting_total}` + %d) <= `{$limiting_column}`) OR `{$limiting_column}` = %d
3248
               THEN `{$column_name}` + %d
3249
               ELSE `{$column_name}`
3250
            END",
3251
                $quantity,
3252
                EE_INF_IN_DB,
3253
                $quantity
3254
            )
3255
        );
3256
    }
3257
3258
3259
    /**
3260
     * Because some other plugins, like Advanced Cron Manager, expect all objects to have this method
3261
     * (probably a bad assumption they have made, oh well)
3262
     *
3263
     * @return string
3264
     */
3265
    public function __toString()
3266
    {
3267
        try {
3268
            return sprintf('%s (%s)', $this->name(), $this->ID());
3269
        } catch (Exception $e) {
3270
            EE_Error::add_error($e->getMessage(), __FILE__, __FUNCTION__, __LINE__);
3271
            return '';
3272
        }
3273
    }
3274
3275
3276
    /**
3277
     * Clear related model objects if they're already in the DB, because otherwise when we
3278
     * UN-serialize this model object we'll need to be careful to add them to the entity map.
3279
     * This means if we have made changes to those related model objects, and want to unserialize
3280
     * the this model object on a subsequent request, changes to those related model objects will be lost.
3281
     * Instead, those related model objects should be directly serialized and stored.
3282
     * Eg, the following won't work:
3283
     * $reg = EEM_Registration::instance()->get_one_by_ID( 123 );
3284
     * $att = $reg->attendee();
3285
     * $att->set( 'ATT_fname', 'Dirk' );
3286
     * update_option( 'my_option', serialize( $reg ) );
3287
     * //END REQUEST
3288
     * //START NEXT REQUEST
3289
     * $reg = get_option( 'my_option' );
3290
     * $reg->attendee()->save();
3291
     * And would need to be replace with:
3292
     * $reg = EEM_Registration::instance()->get_one_by_ID( 123 );
3293
     * $att = $reg->attendee();
3294
     * $att->set( 'ATT_fname', 'Dirk' );
3295
     * update_option( 'my_option', serialize( $reg ) );
3296
     * //END REQUEST
3297
     * //START NEXT REQUEST
3298
     * $att = get_option( 'my_option' );
3299
     * $att->save();
3300
     *
3301
     * @return array
3302
     * @throws ReflectionException
3303
     * @throws InvalidArgumentException
3304
     * @throws InvalidInterfaceException
3305
     * @throws InvalidDataTypeException
3306
     * @throws EE_Error
3307
     */
3308
    public function __sleep()
3309
    {
3310
        $model = $this->get_model();
3311
        foreach ($model->relation_settings() as $relation_name => $relation_obj) {
3312
            if ($relation_obj instanceof EE_Belongs_To_Relation) {
3313
                $classname = 'EE_' . $model->get_this_model_name();
3314
                if ($this->get_one_from_cache($relation_name) instanceof $classname
3315
                    && $this->get_one_from_cache($relation_name)->ID()
3316
                ) {
3317
                    $this->clear_cache(
3318
                        $relation_name,
3319
                        $this->get_one_from_cache($relation_name)->ID()
3320
                    );
3321
                }
3322
            }
3323
        }
3324
        $this->_props_n_values_provided_in_constructor = array();
3325
        $properties_to_serialize = get_object_vars($this);
3326
        // don't serialize the model. It's big and that risks recursion
3327
        unset($properties_to_serialize['_model']);
3328
        return array_keys($properties_to_serialize);
3329
    }
3330
3331
3332
    /**
3333
     * restore _props_n_values_provided_in_constructor
3334
     * PLZ NOTE: this will reset the array to whatever fields values were present prior to serialization,
3335
     * and therefore should NOT be used to determine if state change has occurred since initial construction.
3336
     * At best, you would only be able to detect if state change has occurred during THIS request.
3337
     */
3338
    public function __wakeup()
3339
    {
3340
        $this->_props_n_values_provided_in_constructor = $this->_fields;
3341
    }
3342
3343
3344
    /**
3345
     * Usage of this magic method is to ensure any internally cached references to object instances that must remain
3346
     * distinct with the clone host instance are also cloned.
3347
     */
3348
    public function __clone()
3349
    {
3350
        // handle DateTimes (this is handled in here because there's no one specific child class that uses datetimes).
3351
        foreach ($this->_fields as $field => $value) {
3352
            if ($value instanceof DateTime) {
3353
                $this->_fields[ $field ] = clone $value;
3354
            }
3355
        }
3356
    }
3357
}
3358