Completed
Branch BUG/11288/fix-datepicker (d15367)
by
unknown
108:07 queued 94:31
created

EE_Base_Class::save()   D

Complexity

Conditions 16
Paths 24

Size

Total Lines 110
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 60
nc 24
nop 1
dl 0
loc 110
rs 4.8736
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
4
use EventEspresso\core\domain\values\currency\Money;
5
use EventEspresso\core\domain\values\currency\UsesMoneyInterface;
6
use EventEspresso\core\exceptions\InvalidDataTypeException;
7
use EventEspresso\core\exceptions\InvalidEntityException;
8
use EventEspresso\core\exceptions\InvalidIdentifierException;
9
use EventEspresso\core\exceptions\InvalidInterfaceException;
10
use EventEspresso\core\services\currency\MoneyFactory;
11
use EventEspresso\core\services\loaders\LoaderFactory;
12
13
if ( ! defined('EVENT_ESPRESSO_VERSION')) {
14
    exit('No direct script access allowed');
15
}
16
do_action('AHEE_log', __FILE__, ' FILE LOADED', '');
17
18
19
20
/**
21
 * EE_Base_Class class
22
 *
23
 * @package     Event Espresso
24
 * @subpackage  includes/classes/EE_Base_Class.class.php
25
 * @author      Michael Nelson
26
 */
27
abstract class EE_Base_Class
28
{
29
30
    /**
31
     * This is an array of the original properties and values provided during construction
32
     * of this model object. (keys are model field names, values are their values).
33
     * This list is important to remember so that when we are merging data from the db, we know
34
     * which values to override and which to not override.
35
     *
36
     * @var array
37
     */
38
    protected $_props_n_values_provided_in_constructor;
39
40
    /**
41
     * Timezone
42
     * This gets set by the "set_timezone()" method so that we know what timezone incoming strings|timestamps are in.
43
     * This can also be used before a get to set what timezone you want strings coming out of the object to be in.  NOT
44
     * all EE_Base_Class child classes use this property but any that use a EE_Datetime_Field data type will have
45
     * access to it.
46
     *
47
     * @var string
48
     */
49
    protected $_timezone;
50
51
52
53
    /**
54
     * date format
55
     * pattern or format for displaying dates
56
     *
57
     * @var string $_dt_frmt
58
     */
59
    protected $_dt_frmt;
60
61
62
63
    /**
64
     * time format
65
     * pattern or format for displaying time
66
     *
67
     * @var string $_tm_frmt
68
     */
69
    protected $_tm_frmt;
70
71
72
73
    /**
74
     * This property is for holding a cached array of object properties indexed by property name as the key.
75
     * The purpose of this is for setting a cache on properties that may have calculated values after a
76
     * prepare_for_get.  That way the cache can be checked first and the calculated property returned instead of having
77
     * to recalculate. Used by _set_cached_property() and _get_cached_property() methods.
78
     *
79
     * @var array
80
     */
81
    protected $_cached_properties = array();
82
83
    /**
84
     * An array containing keys of the related model, and values are either an array of related mode objects or a
85
     * single
86
     * related model object. see the model's _model_relations. The keys should match those specified. And if the
87
     * relation is of type EE_Belongs_To (or one of its children), then there should only be ONE related model object,
88
     * all others have an array)
89
     *
90
     * @var array
91
     */
92
    protected $_model_relations = array();
93
94
    /**
95
     * Array where keys are field names (see the model's _fields property) and values are their values. To see what
96
     * their types should be, look at what that field object returns on its prepare_for_get and prepare_for_set methods)
97
     *
98
     * @var array
99
     */
100
    protected $_fields = array();
101
102
    /**
103
     * @var boolean indicating whether or not this model object is intended to ever be saved
104
     * For example, we might create model objects intended to only be used for the duration
105
     * of this request and to be thrown away, and if they were accidentally saved
106
     * it would be a bug.
107
     */
108
    protected $_allow_persist = true;
109
110
    /**
111
     * @var boolean indicating whether or not this model object's properties have changed since construction
112
     */
113
    protected $_has_changes = false;
114
115
    /**
116
     * @var EEM_Base
117
     */
118
    protected $_model;
119
120
    /**
121
     * @var MoneyFactory
122
     */
123
    protected $money_factory;
124
125
126
    /**
127
     * basic constructor for Event Espresso classes, performs any necessary initialization, and verifies it's children
128
     * play nice
129
     *
130
     * @param array $fieldValues where each key is a field (ie, array key in the 2nd
131
     *                                                         layer of the model's _fields array, (eg, EVT_ID,
132
     *                                                         TXN_amount, QST_name, etc) and values are their values
133
     * @param boolean $bydb a flag for setting if the class is instantiated by the
134
     *                                                         corresponding db model or not.
135
     * @param string $timezone indicate what timezone you want any datetime fields to
136
     *                                                         be in when instantiating a EE_Base_Class object.
137
     * @param array $date_formats An array of date formats to set on construct where first
138
     *                                                         value is the date_format and second value is the time
139
     *                                                         format.
140
     * @throws InvalidArgumentException
141
     * @throws InvalidInterfaceException
142
     * @throws InvalidDataTypeException
143
     * @throws EE_Error
144
     */
145
    protected function __construct(
146
        $fieldValues = array(),
147
        $bydb = false,
148
        $timezone = '',
149
        $date_formats = array()
150
    ) {
151
        $className = get_class($this);
152
        do_action("AHEE__{$className}__construct", $this, $fieldValues);
153
        $model = $this->get_model();
154
        $model_fields = $model->field_settings(false);
155
        // ensure $fieldValues is an array
156
        $fieldValues = is_array($fieldValues) ? $fieldValues : array($fieldValues);
157
        // EEH_Debug_Tools::printr( $fieldValues, '$fieldValues  <br /><span style="font-size:10px;font-weight:normal;">' . __FILE__ . '<br />line no: ' . __LINE__ . '</span>', 'auto' );
158
        // verify client code has not passed any invalid field names
159
        foreach ($fieldValues as $field_name => $field_value) {
160 View Code Duplication
            if ( ! isset($model_fields[$field_name])) {
161
                throw new EE_Error(sprintf(__("Invalid field (%s) passed to constructor of %s. Allowed fields are :%s",
162
                    "event_espresso"), $field_name, get_class($this), implode(", ", array_keys($model_fields))));
163
            }
164
        }
165
        // EEH_Debug_Tools::printr( $model_fields, '$model_fields  <br /><span style="font-size:10px;font-weight:normal;">' . __FILE__ . '<br />line no: ' . __LINE__ . '</span>', 'auto' );
166
        $this->_timezone = EEH_DTT_Helper::get_valid_timezone_string($timezone);
167
        if ( ! empty($date_formats) && is_array($date_formats)) {
168
            list($this->_dt_frmt, $this->_tm_frmt) = $date_formats;
169
        } else {
170
            //set default formats for date and time
171
            $this->_dt_frmt = (string)get_option('date_format', 'Y-m-d');
172
            $this->_tm_frmt = (string)get_option('time_format', 'g:i a');
173
        }
174
        //if db model is instantiating
175
        if ($bydb) {
176
            //client code has indicated these field values are from the database
177 View Code Duplication
            foreach ($model_fields as $fieldName => $field) {
178
                $this->set_from_db($fieldName, isset($fieldValues[$fieldName]) ? $fieldValues[$fieldName] : null);
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($fieldName, isset($fieldValues[$fieldName]) ? $fieldValues[$fieldName] : null, true);
185
            }
186
        }
187
        //remember what values were passed to this constructor
188
        $this->_props_n_values_provided_in_constructor = $fieldValues;
189
        //remember in entity mapper
190
        if ( ! $bydb && $model->has_primary_key_field() && $this->ID()) {
191
            $model->add_to_entity_map($this);
192
        }
193
        //setup all the relations
194
        foreach ($model->relation_settings() as $relation_name => $relation_obj) {
195
            if ($relation_obj instanceof EE_Belongs_To_Relation) {
196
                $this->_model_relations[$relation_name] = null;
197
            } else {
198
                $this->_model_relations[$relation_name] = array();
199
            }
200
        }
201
        /**
202
         * Action done at the end of each model object construction
203
         *
204
         * @param EE_Base_Class $this the model object just created
205
         */
206
        do_action('AHEE__EE_Base_Class__construct__finished', $this);
207
    }
208
209
210
211
    /**
212
     * Gets whether or not this model object is allowed to persist/be saved to the database.
213
     *
214
     * @return boolean
215
     */
216
    public function allow_persist()
217
    {
218
        return $this->_allow_persist;
219
    }
220
221
222
223
    /**
224
     * Sets whether or not this model object should be allowed to be saved to the DB.
225
     * Normally once this is set to FALSE you wouldn't set it back to TRUE, unless
226
     * you got new information that somehow made you change your mind.
227
     *
228
     * @param boolean $allow_persist
229
     * @return boolean
230
     */
231
    public function set_allow_persist($allow_persist)
232
    {
233
        return $this->_allow_persist = $allow_persist;
234
    }
235
236
237
238
    /**
239
     * Gets the field's original value when this object was constructed during this request.
240
     * This can be helpful when determining if a model object has changed or not
241
     *
242
     * @param string $field_name
243
     * @return mixed|null
244
     * @throws EE_Error
245
     */
246
    public function get_original($field_name)
247
    {
248
        if (isset($this->_props_n_values_provided_in_constructor[$field_name])
249
            && $field_settings = $this->get_model()->field_settings_for($field_name)
250
        ) {
251
            return $field_settings->prepare_for_get($this->_props_n_values_provided_in_constructor[$field_name]);
252
        } else {
253
            return null;
254
        }
255
    }
256
257
258
259
    /**
260
     * @param EE_Base_Class $obj
261
     * @return string
262
     */
263
    public function get_class($obj)
264
    {
265
        return get_class($obj);
266
    }
267
268
269
    /**
270
     * Overrides parent because parent expects old models.
271
     * This also doesn't do any validation, and won't work for serialized arrays
272
     *
273
     * @param    string $field_name
274
     * @param    mixed $field_value
275
     * @param bool $use_default
276
     * @throws InvalidArgumentException
277
     * @throws InvalidInterfaceException
278
     * @throws InvalidDataTypeException
279
     * @throws EE_Error
280
     */
281
    public function set($field_name, $field_value, $use_default = false)
282
    {
283
        // if not using default and nothing has changed, and object has already been setup (has ID),
284
        // then don't do anything
285
        if (
286
            ! $use_default
287
            && $this->_fields[$field_name] === $field_value
288
            && $this->ID()
289
        ) {
290
            return;
291
        }
292
        $model = $this->get_model();
293
        $this->_has_changes = true;
294
        $field_obj = $model->field_settings_for($field_name);
295
        if ($field_obj instanceof EE_Model_Field_Base) {
296
            //			if ( method_exists( $field_obj, 'set_timezone' )) {
297
            if ($field_obj instanceof EE_Datetime_Field) {
298
                $field_obj->set_timezone($this->_timezone);
299
                $field_obj->set_date_format($this->_dt_frmt);
300
                $field_obj->set_time_format($this->_tm_frmt);
301
            }
302
            $holder_of_value = $field_obj->prepare_for_set($field_value);
303
            //should the value be null?
304
            if (($field_value === null || $holder_of_value === null || $holder_of_value === '') && $use_default) {
305
                $this->_fields[$field_name] = $field_obj->get_default_value();
306
                /**
307
                 * To save having to refactor all the models, if a default value is used for a
308
                 * EE_Datetime_Field, and that value is not null nor is it a DateTime
309
                 * object.  Then let's do a set again to ensure that it becomes a DateTime
310
                 * object.
311
                 *
312
                 * @since 4.6.10+
313
                 */
314
                if (
315
                    $field_obj instanceof EE_Datetime_Field
316
                    && $this->_fields[$field_name] !== null
317
                    && ! $this->_fields[$field_name] instanceof DateTime
318
                ) {
319
                    empty($this->_fields[$field_name])
320
                        ? $this->set($field_name, time())
321
                        : $this->set($field_name, $this->_fields[$field_name]);
322
                }
323
            } else {
324
                $this->_fields[$field_name] = $holder_of_value;
325
            }
326
            //if we're not in the constructor...
327
            //now check if what we set was a primary key
328
            if (
329
                //note: props_n_values_provided_in_constructor is only set at the END of the constructor
330
                $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...
331
                && $field_value
332
                && $field_name === $model->primary_key_name()
333
            ) {
334
                //if so, we want all this object's fields to be filled either with
335
                //what we've explicitly set on this model
336
                //or what we have in the db
337
                // echo "setting primary key!";
338
                $fields_on_model = self::_get_model(get_class($this))->field_settings();
339
                $obj_in_db = self::_get_model(get_class($this))->get_one_by_ID($field_value);
340
                foreach ($fields_on_model as $field_obj) {
341
                    if ( ! array_key_exists($field_obj->get_name(), $this->_props_n_values_provided_in_constructor)
342
                         && $field_obj->get_name() !== $field_name
343
                    ) {
344
                        $this->set($field_obj->get_name(), $obj_in_db->get($field_obj->get_name()));
345
                    }
346
                }
347
                //oh this model object has an ID? well make sure its in the entity mapper
348
                $model->add_to_entity_map($this);
349
            }
350
            //let's unset any cache for this field_name from the $_cached_properties property.
351
            $this->_clear_cached_property($field_name);
352
        } else {
353
            throw new EE_Error(sprintf(__("A valid EE_Model_Field_Base could not be found for the given field name: %s",
354
                "event_espresso"), $field_name));
355
        }
356
    }
357
358
359
360
    /**
361
     * This sets the field value on the db column if it exists for the given $column_name or
362
     * saves it to EE_Extra_Meta if the given $column_name does not match a db column.
363
     *
364
     * @see EE_message::get_column_value for related documentation on the necessity of this method.
365
     * @param string $field_name  Must be the exact column name.
366
     * @param mixed  $field_value The value to set.
367
     * @return int|bool @see EE_Base_Class::update_extra_meta() for return docs.
368
     * @throws EE_Error
369
     */
370
    public function set_field_or_extra_meta($field_name, $field_value)
371
    {
372
        if ($this->get_model()->has_field($field_name)) {
373
            $this->set($field_name, $field_value);
374
            return true;
375
        } else {
376
            //ensure this object is saved first so that extra meta can be properly related.
377
            $this->save();
378
            return $this->update_extra_meta($field_name, $field_value);
379
        }
380
    }
381
382
383
384
    /**
385
     * This retrieves the value of the db column set on this class or if that's not present
386
     * it will attempt to retrieve from extra_meta if found.
387
     * Example Usage:
388
     * Via EE_Message child class:
389
     * Due to the dynamic nature of the EE_messages system, EE_messengers will always have a "to",
390
     * "from", "subject", and "content" field (as represented in the EE_Message schema), however they may
391
     * also have additional main fields specific to the messenger.  The system accommodates those extra
392
     * fields through the EE_Extra_Meta table.  This method allows for EE_messengers to retrieve the
393
     * value for those extra fields dynamically via the EE_message object.
394
     *
395
     * @param  string $field_name expecting the fully qualified field name.
396
     * @return mixed|null  value for the field if found.  null if not found.
397
     * @throws EE_Error
398
     */
399
    public function get_field_or_extra_meta($field_name)
400
    {
401
        if ($this->get_model()->has_field($field_name)) {
402
            $column_value = $this->get($field_name);
403
        } else {
404
            //This isn't a column in the main table, let's see if it is in the extra meta.
405
            $column_value = $this->get_extra_meta($field_name, true, null);
406
        }
407
        return $column_value;
408
    }
409
410
411
    /**
412
     * See $_timezone property for description of what the timezone property is for.  This SETS the timezone internally
413
     * for being able to reference what timezone we are running conversions on when converting TO the internal timezone
414
     * (UTC Unix Timestamp) for the object OR when converting FROM the internal timezone (UTC Unix Timestamp). This is
415
     * available to all child classes that may be using the EE_Datetime_Field for a field data type.
416
     *
417
     * @access public
418
     * @param string $timezone A valid timezone string as described by @link http://www.php.net/manual/en/timezones.php
419
     * @return void
420
     * @throws InvalidArgumentException
421
     * @throws InvalidInterfaceException
422
     * @throws InvalidDataTypeException
423
     * @throws EE_Error
424
     */
425
    public function set_timezone($timezone = '')
426
    {
427
        $this->_timezone = EEH_DTT_Helper::get_valid_timezone_string($timezone);
428
        //make sure we clear all cached properties because they won't be relevant now
429
        $this->_clear_cached_properties();
430
        //make sure we update field settings and the date for all EE_Datetime_Fields
431
        $model_fields = $this->get_model()->field_settings(false);
432
        foreach ($model_fields as $field_name => $field_obj) {
433
            if ($field_obj instanceof EE_Datetime_Field) {
434
                $field_obj->set_timezone($this->_timezone);
435
                if (isset($this->_fields[$field_name]) && $this->_fields[$field_name] instanceof DateTime) {
436
                    $this->_fields[$field_name]->setTimezone(new DateTimeZone($this->_timezone));
437
                }
438
            }
439
        }
440
    }
441
442
443
444
    /**
445
     * This just returns whatever is set for the current timezone.
446
     *
447
     * @access public
448
     * @return string timezone string
449
     */
450
    public function get_timezone()
451
    {
452
        return $this->_timezone;
453
    }
454
455
456
457
    /**
458
     * This sets the internal date format to what is sent in to be used as the new default for the class
459
     * internally instead of wp set date format options
460
     *
461
     * @since 4.6
462
     * @param string $format should be a format recognizable by PHP date() functions.
463
     */
464
    public function set_date_format($format)
465
    {
466
        $this->_dt_frmt = $format;
467
        //clear cached_properties because they won't be relevant now.
468
        $this->_clear_cached_properties();
469
    }
470
471
472
473
    /**
474
     * This sets the internal time format string to what is sent in to be used as the new default for the
475
     * class internally instead of wp set time format options.
476
     *
477
     * @since 4.6
478
     * @param string $format should be a format recognizable by PHP date() functions.
479
     */
480
    public function set_time_format($format)
481
    {
482
        $this->_tm_frmt = $format;
483
        //clear cached_properties because they won't be relevant now.
484
        $this->_clear_cached_properties();
485
    }
486
487
488
489
    /**
490
     * This returns the current internal set format for the date and time formats.
491
     *
492
     * @param bool $full           if true (default), then return the full format.  Otherwise will return an array
493
     *                             where the first value is the date format and the second value is the time format.
494
     * @return mixed string|array
495
     */
496
    public function get_format($full = true)
497
    {
498
        return $full ? $this->_dt_frmt . ' ' . $this->_tm_frmt : array($this->_dt_frmt, $this->_tm_frmt);
499
    }
500
501
502
503
    /**
504
     * cache
505
     * stores the passed model object on the current model object.
506
     * In certain circumstances, we can use this cached model object instead of querying for another one entirely.
507
     *
508
     * @param string        $relationName    one of the keys in the _model_relations array on the model. Eg
509
     *                                       'Registration' associated with this model object
510
     * @param EE_Base_Class $object_to_cache that has a relation to this model object. (Eg, if this is a Transaction,
511
     *                                       that could be a payment or a registration)
512
     * @param null          $cache_id        a string or number that will be used as the key for any Belongs_To_Many
513
     *                                       items which will be stored in an array on this object
514
     * @throws EE_Error
515
     * @return mixed    index into cache, or just TRUE if the relation is of type Belongs_To (because there's only one
516
     *                  related thing, no array)
517
     */
518
    public function cache($relationName = '', $object_to_cache = null, $cache_id = null)
519
    {
520
        // its entirely possible that there IS no related object yet in which case there is nothing to cache.
521
        if ( ! $object_to_cache instanceof EE_Base_Class) {
522
            return false;
523
        }
524
        // also get "how" the object is related, or throw an error
525
        if ( ! $relationship_to_model = $this->get_model()->related_settings_for($relationName)) {
526
            throw new EE_Error(sprintf(__('There is no relationship to %s on a %s. Cannot cache it', 'event_espresso'),
527
                $relationName, get_class($this)));
528
        }
529
        // how many things are related ?
530
        if ($relationship_to_model instanceof EE_Belongs_To_Relation) {
531
            // if it's a "belongs to" relationship, then there's only one related model object  eg, if this is a registration, there's only 1 attendee for it
532
            // so for these model objects just set it to be cached
533
            $this->_model_relations[$relationName] = $object_to_cache;
534
            $return = true;
535
        } else {
536
            // otherwise, this is the "many" side of a one to many relationship, so we'll add the object to the array of related objects for that type.
537
            // eg: if this is an event, there are many registrations for that event, so we cache the registrations in an array
538
            if ( ! is_array($this->_model_relations[$relationName])) {
539
                // if for some reason, the cached item is a model object, then stick that in the array, otherwise start with an empty array
540
                $this->_model_relations[$relationName] = $this->_model_relations[$relationName] instanceof EE_Base_Class
541
                    ? array($this->_model_relations[$relationName]) : array();
542
            }
543
            // first check for a cache_id which is normally empty
544
            if ( ! empty($cache_id)) {
545
                // if the cache_id exists, then it means we are purposely trying to cache this with a known key that can then be used to retrieve the object later on
546
                $this->_model_relations[$relationName][$cache_id] = $object_to_cache;
547
                $return = $cache_id;
548
            } elseif ($object_to_cache->ID()) {
549
                // OR the cached object originally came from the db, so let's just use it's PK for an ID
550
                $this->_model_relations[$relationName][$object_to_cache->ID()] = $object_to_cache;
551
                $return = $object_to_cache->ID();
552
            } else {
553
                // OR it's a new object with no ID, so just throw it in the array with an auto-incremented ID
554
                $this->_model_relations[$relationName][] = $object_to_cache;
555
                // move the internal pointer to the end of the array
556
                end($this->_model_relations[$relationName]);
557
                // and grab the key so that we can return it
558
                $return = key($this->_model_relations[$relationName]);
559
            }
560
        }
561
        return $return;
562
    }
563
564
565
566
    /**
567
     * For adding an item to the cached_properties property.
568
     *
569
     * @access protected
570
     * @param string      $fieldname the property item the corresponding value is for.
571
     * @param mixed       $value     The value we are caching.
572
     * @param string|null $cache_type
573
     * @return void
574
     * @throws EE_Error
575
     */
576
    protected function _set_cached_property($fieldname, $value, $cache_type = null)
577
    {
578
        //first make sure this property exists
579
        $this->get_model()->field_settings_for($fieldname);
580
        $cache_type = empty($cache_type) ? 'standard' : $cache_type;
581
        $this->_cached_properties[$fieldname][$cache_type] = $value;
582
    }
583
584
585
586
    /**
587
     * This returns the value cached property if it exists OR the actual property value if the cache doesn't exist.
588
     * This also SETS the cache if we return the actual property!
589
     *
590
     * @param string $fieldname        the name of the property we're trying to retrieve
591
     * @param bool   $pretty
592
     * @param string $extra_cache_ref  This allows the user to specify an extra cache ref for the given property
593
     *                                 (in cases where the same property may be used for different outputs
594
     *                                 - i.e. datetime, money etc.)
595
     *                                 It can also accept certain pre-defined "schema" strings
596
     *                                 to define how to output the property.
597
     *                                 see the field's prepare_for_pretty_echoing for what strings can be used
598
     * @return mixed                   whatever the value for the property is we're retrieving
599
     * @throws EE_Error
600
     */
601
    protected function _get_cached_property($fieldname, $pretty = false, $extra_cache_ref = null)
602
    {
603
        //verify the field exists
604
        $model = $this->get_model();
605
        $model->field_settings_for($fieldname);
606
        $cache_type = $pretty ? 'pretty' : 'standard';
607
        $cache_type .= ! empty($extra_cache_ref) ? '_' . $extra_cache_ref : '';
608
        if (isset($this->_cached_properties[$fieldname][$cache_type])) {
609
            return $this->_cached_properties[$fieldname][$cache_type];
610
        }
611
        $value = $this->_get_fresh_property($fieldname, $pretty, $extra_cache_ref);
612
        $this->_set_cached_property($fieldname, $value, $cache_type);
613
        return $value;
614
    }
615
616
617
    /**
618
     * If the cache didn't fetch the needed item, this fetches it.
619
     * @param string $fieldname
620
     * @param bool $pretty
621
     * @param string $extra_cache_ref
622
     * @return mixed
623
     * @throws EE_Error
624
     */
625
    protected function _get_fresh_property($fieldname, $pretty = false, $extra_cache_ref = null)
626
    {
627
        $field_obj = $this->get_model()->field_settings_for($fieldname);
628
        // If this is an EE_Datetime_Field we need to make sure timezone, formats, and output are correct
629
        if ($field_obj instanceof EE_Datetime_Field) {
630
            $this->_prepare_datetime_field($field_obj, $pretty, $extra_cache_ref);
631
        }
632
        if ( ! isset($this->_fields[$fieldname])) {
633
            $this->_fields[$fieldname] = null;
634
        }
635
        $value = $pretty
636
            ? $field_obj->prepare_for_pretty_echoing($this->_fields[$fieldname], $extra_cache_ref)
637
            : $field_obj->prepare_for_get($this->_fields[$fieldname]);
638
        return $value;
639
    }
640
641
642
    /**
643
     * set timezone, formats, and output for EE_Datetime_Field objects
644
     *
645
     * @param \EE_Datetime_Field $datetime_field
646
     * @param bool $pretty
647
     * @param null $date_or_time
648
     * @return void
649
     * @throws InvalidArgumentException
650
     * @throws InvalidInterfaceException
651
     * @throws InvalidDataTypeException
652
     * @throws EE_Error
653
     */
654
    protected function _prepare_datetime_field(
655
        EE_Datetime_Field $datetime_field,
656
        $pretty = false,
657
        $date_or_time = null
658
    ) {
659
        $datetime_field->set_timezone($this->_timezone);
660
        $datetime_field->set_date_format($this->_dt_frmt, $pretty);
661
        $datetime_field->set_time_format($this->_tm_frmt, $pretty);
662
        //set the output returned
663
        switch ($date_or_time) {
664
            case 'D' :
665
                $datetime_field->set_date_time_output('date');
666
                break;
667
            case 'T' :
668
                $datetime_field->set_date_time_output('time');
669
                break;
670
            default :
671
                $datetime_field->set_date_time_output();
672
        }
673
    }
674
675
676
677
    /**
678
     * This just takes care of clearing out the cached_properties
679
     *
680
     * @return void
681
     */
682
    protected function _clear_cached_properties()
683
    {
684
        $this->_cached_properties = array();
685
    }
686
687
688
689
    /**
690
     * This just clears out ONE property if it exists in the cache
691
     *
692
     * @param  string $property_name the property to remove if it exists (from the _cached_properties array)
693
     * @return void
694
     */
695
    protected function _clear_cached_property($property_name)
696
    {
697
        if (isset($this->_cached_properties[$property_name])) {
698
            unset($this->_cached_properties[$property_name]);
699
        }
700
    }
701
702
703
704
    /**
705
     * Ensures that this related thing is a model object.
706
     *
707
     * @param mixed  $object_or_id EE_base_Class/int/string either a related model object, or its ID
708
     * @param string $model_name   name of the related thing, eg 'Attendee',
709
     * @return EE_Base_Class
710
     * @throws EE_Error
711
     */
712
    protected function ensure_related_thing_is_model_obj($object_or_id, $model_name)
713
    {
714
        $other_model_instance = self::_get_model_instance_with_name(
715
            self::_get_model_classname($model_name),
716
            $this->_timezone
717
        );
718
        return $other_model_instance->ensure_is_obj($object_or_id);
719
    }
720
721
722
723
    /**
724
     * Forgets the cached model of the given relation Name. So the next time we request it,
725
     * we will fetch it again from the database. (Handy if you know it's changed somehow).
726
     * If a specific object is supplied, and the relationship to it is either a HasMany or HABTM,
727
     * then only remove that one object from our cached array. Otherwise, clear the entire list
728
     *
729
     * @param string $relationName                         one of the keys in the _model_relations array on the model.
730
     *                                                     Eg 'Registration'
731
     * @param mixed  $object_to_remove_or_index_into_array or an index into the array of cached things, or NULL
732
     *                                                     if you intend to use $clear_all = TRUE, or the relation only
733
     *                                                     has 1 object anyways (ie, it's a BelongsToRelation)
734
     * @param bool   $clear_all                            This flags clearing the entire cache relation property if
735
     *                                                     this is HasMany or HABTM.
736
     * @throws EE_Error
737
     * @return EE_Base_Class | boolean from which was cleared from the cache, or true if we requested to remove a
738
     *                       relation from all
739
     */
740
    public function clear_cache($relationName, $object_to_remove_or_index_into_array = null, $clear_all = false)
741
    {
742
        $relationship_to_model = $this->get_model()->related_settings_for($relationName);
743
        $index_in_cache = '';
744
        if ( ! $relationship_to_model) {
745
            throw new EE_Error(
746
                sprintf(
747
                    __("There is no relationship to %s on a %s. Cannot clear that cache", 'event_espresso'),
748
                    $relationName,
749
                    get_class($this)
750
                )
751
            );
752
        }
753
        if ($clear_all) {
754
            $obj_removed = true;
755
            $this->_model_relations[$relationName] = null;
756
        } elseif ($relationship_to_model instanceof EE_Belongs_To_Relation) {
757
            $obj_removed = $this->_model_relations[$relationName];
758
            $this->_model_relations[$relationName] = null;
759
        } else {
760
            if ($object_to_remove_or_index_into_array instanceof EE_Base_Class
761
                && $object_to_remove_or_index_into_array->ID()
762
            ) {
763
                $index_in_cache = $object_to_remove_or_index_into_array->ID();
764
                if (is_array($this->_model_relations[$relationName])
765
                    && ! isset($this->_model_relations[$relationName][$index_in_cache])
766
                ) {
767
                    $index_found_at = null;
768
                    //find this object in the array even though it has a different key
769
                    foreach ($this->_model_relations[$relationName] as $index => $obj) {
770
                        if (
771
                            $obj instanceof EE_Base_Class
772
                            && (
773
                                $obj == $object_to_remove_or_index_into_array
774
                                || $obj->ID() === $object_to_remove_or_index_into_array->ID()
775
                            )
776
                        ) {
777
                            $index_found_at = $index;
778
                            break;
779
                        }
780
                    }
781
                    if ($index_found_at) {
782
                        $index_in_cache = $index_found_at;
783
                    } else {
784
                        //it wasn't found. huh. well obviously it doesn't need to be removed from teh cache
785
                        //if it wasn't in it to begin with. So we're done
786
                        return $object_to_remove_or_index_into_array;
787
                    }
788
                }
789
            } elseif ($object_to_remove_or_index_into_array instanceof EE_Base_Class) {
790
                //so they provided a model object, but it's not yet saved to the DB... so let's go hunting for it!
791
                foreach ($this->get_all_from_cache($relationName) as $index => $potentially_obj_we_want) {
792
                    if ($potentially_obj_we_want == $object_to_remove_or_index_into_array) {
793
                        $index_in_cache = $index;
794
                    }
795
                }
796
            } else {
797
                $index_in_cache = $object_to_remove_or_index_into_array;
798
            }
799
            //supposedly we've found it. But it could just be that the client code
800
            //provided a bad index/object
801
            if (
802
            isset(
803
                $this->_model_relations[$relationName],
804
                $this->_model_relations[$relationName][$index_in_cache]
805
            )
806
            ) {
807
                $obj_removed = $this->_model_relations[$relationName][$index_in_cache];
808
                unset($this->_model_relations[$relationName][$index_in_cache]);
809
            } else {
810
                //that thing was never cached anyways.
811
                $obj_removed = null;
812
            }
813
        }
814
        return $obj_removed;
815
    }
816
817
818
819
    /**
820
     * update_cache_after_object_save
821
     * Allows a cached item to have it's cache ID (within the array of cached items) reset using the new ID it has
822
     * obtained after being saved to the db
823
     *
824
     * @param string         $relationName       - the type of object that is cached
825
     * @param EE_Base_Class $newly_saved_object - the newly saved object to be re-cached
826
     * @param string         $current_cache_id   - the ID that was used when originally caching the object
827
     * @return boolean TRUE on success, FALSE on fail
828
     * @throws EE_Error
829
     */
830
    public function update_cache_after_object_save(
831
        $relationName,
832
        EE_Base_Class $newly_saved_object,
833
        $current_cache_id = ''
834
    ) {
835
        // verify that incoming object is of the correct type
836
        $obj_class = 'EE_' . $relationName;
837
        if ($newly_saved_object instanceof $obj_class) {
838
            /* @type EE_Base_Class $newly_saved_object */
839
            // now get the type of relation
840
            $relationship_to_model = $this->get_model()->related_settings_for($relationName);
841
            // if this is a 1:1 relationship
842
            if ($relationship_to_model instanceof EE_Belongs_To_Relation) {
843
                // then just replace the cached object with the newly saved object
844
                $this->_model_relations[$relationName] = $newly_saved_object;
845
                return true;
846
                // or if it's some kind of sordid feral polyamorous relationship...
847
            } elseif (is_array($this->_model_relations[$relationName])
848
                      && isset($this->_model_relations[$relationName][$current_cache_id])
849
            ) {
850
                // then remove the current cached item
851
                unset($this->_model_relations[$relationName][$current_cache_id]);
852
                // and cache the newly saved object using it's new ID
853
                $this->_model_relations[$relationName][$newly_saved_object->ID()] = $newly_saved_object;
854
                return true;
855
            }
856
        }
857
        return false;
858
    }
859
860
861
862
    /**
863
     * Fetches a single EE_Base_Class on that relation. (If the relation is of type
864
     * BelongsTo, it will only ever have 1 object. However, other relations could have an array of objects)
865
     *
866
     * @param string $relationName
867
     * @return EE_Base_Class
868
     */
869
    public function get_one_from_cache($relationName)
870
    {
871
        $cached_array_or_object = isset($this->_model_relations[$relationName]) ? $this->_model_relations[$relationName]
872
            : null;
873
        if (is_array($cached_array_or_object)) {
874
            return array_shift($cached_array_or_object);
875
        } else {
876
            return $cached_array_or_object;
877
        }
878
    }
879
880
881
    /**
882
     * Fetches a single EE_Base_Class on that relation. (If the relation is of type
883
     * BelongsTo, it will only ever have 1 object. However, other relations could have an array of objects)
884
     *
885
     * @param string $relationName
886
     * @throws \ReflectionException
887
     * @throws InvalidArgumentException
888
     * @throws InvalidInterfaceException
889
     * @throws InvalidDataTypeException
890
     * @throws EE_Error
891
     * @return EE_Base_Class[] NOT necessarily indexed by primary keys
892
     */
893
    public function get_all_from_cache($relationName)
894
    {
895
        $objects = isset($this->_model_relations[$relationName]) ? $this->_model_relations[$relationName] : array();
896
        // if the result is not an array, but exists, make it an array
897
        $objects = is_array($objects) ? $objects : array($objects);
898
        //bugfix for https://events.codebasehq.com/projects/event-espresso/tickets/7143
899
        //basically, if this model object was stored in the session, and these cached model objects
900
        //already have IDs, let's make sure they're in their model's entity mapper
901
        //otherwise we will have duplicates next time we call
902
        // EE_Registry::instance()->load_model( $relationName )->get_one_by_ID( $result->ID() );
903
        $model = EE_Registry::instance()->load_model($relationName);
904
        foreach ($objects as $model_object) {
905
            if ($model instanceof EEM_Base && $model_object instanceof EE_Base_Class) {
906
                //ensure its in the map if it has an ID; otherwise it will be added to the map when its saved
907
                if ($model_object->ID()) {
908
                    $model->add_to_entity_map($model_object);
909
                }
910
            } else {
911
                throw new EE_Error(
912
                    sprintf(
913
                        __(
914
                            'Error retrieving related model objects. Either $1%s is not a model or $2%s is not a model object',
915
                            'event_espresso'
916
                        ),
917
                        $relationName,
918
                        gettype($model_object)
919
                    )
920
                );
921
            }
922
        }
923
        return $objects;
924
    }
925
926
927
928
    /**
929
     * Returns the next x number of EE_Base_Class objects in sequence from this object as found in the database
930
     * matching the given query conditions.
931
     *
932
     * @param null  $field_to_order_by  What field is being used as the reference point.
933
     * @param int   $limit              How many objects to return.
934
     * @param array $query_params       Any additional conditions on the query.
935
     * @param null  $columns_to_select  If left null, then an array of EE_Base_Class objects is returned, otherwise
936
     *                                  you can indicate just the columns you want returned
937
     * @return array|EE_Base_Class[]
938
     * @throws EE_Error
939
     */
940 View Code Duplication
    public function next_x($field_to_order_by = null, $limit = 1, $query_params = array(), $columns_to_select = null)
941
    {
942
        $model = $this->get_model();
943
        $field = empty($field_to_order_by) && $model->has_primary_key_field()
944
            ? $model->get_primary_key_field()->get_name()
945
            : $field_to_order_by;
946
        $current_value = ! empty($field) ? $this->get($field) : null;
947
        if (empty($field) || empty($current_value)) {
948
            return array();
949
        }
950
        return $model->next_x($current_value, $field, $limit, $query_params, $columns_to_select);
951
    }
952
953
954
955
    /**
956
     * Returns the previous x number of EE_Base_Class objects in sequence from this object as found in the database
957
     * matching the given query conditions.
958
     *
959
     * @param null  $field_to_order_by  What field is being used as the reference point.
960
     * @param int   $limit              How many objects to return.
961
     * @param array $query_params       Any additional conditions on the query.
962
     * @param null  $columns_to_select  If left null, then an array of EE_Base_Class objects is returned, otherwise
963
     *                                  you can indicate just the columns you want returned
964
     * @return array|EE_Base_Class[]
965
     * @throws EE_Error
966
     */
967 View Code Duplication
    public function previous_x(
968
        $field_to_order_by = null,
969
        $limit = 1,
970
        $query_params = array(),
971
        $columns_to_select = null
972
    ) {
973
        $model = $this->get_model();
974
        $field = empty($field_to_order_by) && $model->has_primary_key_field()
975
            ? $model->get_primary_key_field()->get_name()
976
            : $field_to_order_by;
977
        $current_value = ! empty($field) ? $this->get($field) : null;
978
        if (empty($field) || empty($current_value)) {
979
            return array();
980
        }
981
        return $model->previous_x($current_value, $field, $limit, $query_params, $columns_to_select);
982
    }
983
984
985
986
    /**
987
     * Returns the next EE_Base_Class object in sequence from this object as found in the database
988
     * matching the given query conditions.
989
     *
990
     * @param null  $field_to_order_by  What field is being used as the reference point.
991
     * @param array $query_params       Any additional conditions on the query.
992
     * @param null  $columns_to_select  If left null, then an array of EE_Base_Class objects is returned, otherwise
993
     *                                  you can indicate just the columns you want returned
994
     * @return array|EE_Base_Class
995
     * @throws EE_Error
996
     */
997 View Code Duplication
    public function next($field_to_order_by = null, $query_params = array(), $columns_to_select = null)
998
    {
999
        $model = $this->get_model();
1000
        $field = empty($field_to_order_by) && $model->has_primary_key_field()
1001
            ? $model->get_primary_key_field()->get_name()
1002
            : $field_to_order_by;
1003
        $current_value = ! empty($field) ? $this->get($field) : null;
1004
        if (empty($field) || empty($current_value)) {
1005
            return array();
1006
        }
1007
        return $model->next($current_value, $field, $query_params, $columns_to_select);
1008
    }
1009
1010
1011
1012
    /**
1013
     * Returns the previous EE_Base_Class object in sequence from this object as found in the database
1014
     * matching the given query conditions.
1015
     *
1016
     * @param null  $field_to_order_by  What field is being used as the reference point.
1017
     * @param array $query_params       Any additional conditions on the query.
1018
     * @param null  $columns_to_select  If left null, then an EE_Base_Class object is returned, otherwise
1019
     *                                  you can indicate just the column you want returned
1020
     * @return array|EE_Base_Class
1021
     * @throws EE_Error
1022
     */
1023 View Code Duplication
    public function previous($field_to_order_by = null, $query_params = array(), $columns_to_select = null)
1024
    {
1025
        $model = $this->get_model();
1026
        $field = empty($field_to_order_by) && $model->has_primary_key_field()
1027
            ? $model->get_primary_key_field()->get_name()
1028
            : $field_to_order_by;
1029
        $current_value = ! empty($field) ? $this->get($field) : null;
1030
        if (empty($field) || empty($current_value)) {
1031
            return array();
1032
        }
1033
        return $model->previous($current_value, $field, $query_params, $columns_to_select);
1034
    }
1035
1036
1037
1038
    /**
1039
     * Overrides parent because parent expects old models.
1040
     * This also doesn't do any validation, and won't work for serialized arrays
1041
     *
1042
     * @param string $field_name
1043
     * @param mixed  $field_value_from_db
1044
     * @throws EE_Error
1045
     */
1046
    public function set_from_db($field_name, $field_value_from_db)
1047
    {
1048
        $field_obj = $this->get_model()->field_settings_for($field_name);
1049
        if ($field_obj instanceof EE_Model_Field_Base) {
1050
            //you would think the DB has no NULLs for non-null label fields right? wrong!
1051
            //eg, a CPT model object could have an entry in the posts table, but no
1052
            //entry in the meta table. Meaning that all its columns in the meta table
1053
            //are null! yikes! so when we find one like that, use defaults for its meta columns
1054
            if ($field_value_from_db === null) {
1055
                if ($field_obj->is_nullable()) {
1056
                    //if the field allows nulls, then let it be null
1057
                    $field_value = null;
1058
                } else {
1059
                    $field_value = $field_obj->get_default_value();
1060
                }
1061
            } else {
1062
                $field_value = $field_obj->prepare_for_set_from_db($field_value_from_db);
1063
            }
1064
            $this->_fields[$field_name] = $field_value;
1065
            $this->_clear_cached_property($field_name);
1066
        }
1067
    }
1068
1069
1070
1071
    /**
1072
     * verifies that the specified field is of the correct type
1073
     *
1074
     * @param string $field_name
1075
     * @param string $extra_cache_ref This allows the user to specify an extra cache ref for the given property
1076
     *                                (in cases where the same property may be used for different outputs
1077
     *                                - i.e. datetime, money etc.)
1078
     * @return mixed
1079
     * @throws EE_Error
1080
     */
1081
    public function get($field_name, $extra_cache_ref = null)
1082
    {
1083
        return $this->_get_cached_property($field_name, false, $extra_cache_ref);
1084
    }
1085
1086
1087
1088
    /**
1089
     * This method simply returns the RAW unprocessed value for the given property in this class
1090
     *
1091
     * @param  string $field_name A valid fieldname
1092
     * @return mixed              Whatever the raw value stored on the property is.
1093
     * @throws EE_Error if fieldSettings is misconfigured or the field doesn't exist.
1094
     */
1095
    public function get_raw($field_name)
1096
    {
1097
        $field_settings = $this->get_model()->field_settings_for($field_name);
1098
        switch(true){
1099
            case $field_settings instanceof EE_Datetime_Field && $this->_fields[$field_name] instanceof DateTime:
1100
                $value = $this->_fields[$field_name]->format('U');
1101
                break;
1102
            case $field_settings instanceof EE_Money_Field && $this->_fields[$field_name] instanceof Money:
1103
                $value = $this->_fields[$field_name]->floatAmount();
1104
                break;
1105
            default:
1106
                $value = $this->_fields[$field_name];
1107
        }
1108
        return $value;
1109
    }
1110
1111
1112
1113
    /**
1114
     * This is used to return the internal DateTime object used for a field that is a
1115
     * EE_Datetime_Field.
1116
     *
1117
     * @param string $field_name               The field name retrieving the DateTime object.
1118
     * @return mixed null | false | DateTime  If the requested field is NOT a EE_Datetime_Field then
1119
     * @throws EE_Error
1120
     *                                         an error is set and false returned.  If the field IS an
1121
     *                                         EE_Datetime_Field and but the field value is null, then
1122
     *                                         just null is returned (because that indicates that likely
1123
     *                                         this field is nullable).
1124
     */
1125
    public function get_DateTime_object($field_name)
1126
    {
1127
        $field_settings = $this->get_model()->field_settings_for($field_name);
1128 View Code Duplication
        if ( ! $field_settings instanceof EE_Datetime_Field) {
1129
            EE_Error::add_error(
1130
                sprintf(
1131
                    __(
1132
                        'The field %s is not an EE_Datetime_Field field.  There is no DateTime object stored on this field type.',
1133
                        'event_espresso'
1134
                    ),
1135
                    $field_name
1136
                ),
1137
                __FILE__,
1138
                __FUNCTION__,
1139
                __LINE__
1140
            );
1141
            return false;
1142
        }
1143
        return $this->_fields[$field_name];
1144
    }
1145
1146
1147
    /**
1148
     * Gets a Money object for the specified field. Please note that this should only be
1149
     * used for fields corresponding to EE_Money_Fields, and it will always return a money object,
1150
     * or else it will throw an exception.
1151
     *
1152
     * @param $field_name
1153
     * @return Money
1154
     * @throws InvalidEntityException
1155
     * @throws EE_Error
1156
     * @throws DomainException
1157
     * @since $VID:$
1158
     */
1159
    public function getMoneyObject($field_name)
1160
    {
1161
        $this->verifyUsesMoney(__FUNCTION__);
1162
        $field = $this->get_model()->field_settings_for($field_name);
1163
        $value = isset($this->_fields[$field_name]) ? $this->_fields[$field_name] : null;
1164
        if (! $field instanceof EE_Money_Field
1165
            || ! $value instanceof Money) {
1166
            throw new InvalidEntityException(
1167
                get_class($value),
1168
                'Money',
1169
                sprintf(
1170
                    esc_html__(
1171
                        // @codingStandardsIgnoreStart
1172
                        'Tried to retrieve money value from %1$s with ID %2$s from field %3$s but no money object present.',
1173
                        // @codingStandardsIgnoreEnd
1174
                        'event_espresso'
1175
                    ),
1176
                    get_class($this),
1177
                    $this->ID(),
1178
                    $field_name
1179
                )
1180
            );
1181
        }
1182
        return $value;
1183
    }
1184
1185
1186
1187
    /**
1188
     * To be used in template to immediately echo out the value, and format it for output.
1189
     * Eg, should call stripslashes and whatnot before echoing
1190
     *
1191
     * @param string $field_name      the name of the field as it appears in the DB
1192
     * @param string $extra_cache_ref This allows the user to specify an extra cache ref for the given property
1193
     *                                (in cases where the same property may be used for different outputs
1194
     *                                - i.e. datetime, money etc.)
1195
     * @return void
1196
     * @throws EE_Error
1197
     */
1198
    public function e($field_name, $extra_cache_ref = null)
1199
    {
1200
        echo $this->get_pretty($field_name, $extra_cache_ref);
1201
    }
1202
1203
1204
1205
    /**
1206
     * Exactly like e(), echoes out the field, but sets its schema to 'form_input', so that it
1207
     * can be easily used as the value of form input.
1208
     *
1209
     * @param string $field_name
1210
     * @return void
1211
     * @throws EE_Error
1212
     */
1213
    public function f($field_name)
1214
    {
1215
        $this->e($field_name, 'form_input');
1216
    }
1217
1218
    /**
1219
     * Same as `f()` but just returns the value instead of echoing it
1220
     * @param string $field_name
1221
     * @return string
1222
     * @throws EE_Error
1223
     */
1224
    public function get_f($field_name)
1225
    {
1226
        return (string)$this->get_pretty($field_name,'form_input');
1227
    }
1228
1229
1230
1231
    /**
1232
     * Gets a pretty view of the field's value. $extra_cache_ref can specify different formats for this.
1233
     * The $extra_cache_ref will be passed to the model field's prepare_for_pretty_echoing, so consult the field's class
1234
     * to see what options are available.
1235
     * @param string $field_name
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 mixed
1240
     * @throws EE_Error
1241
     */
1242
    public function get_pretty($field_name, $extra_cache_ref = null)
1243
    {
1244
        return $this->_get_cached_property($field_name, true, $extra_cache_ref);
1245
    }
1246
1247
1248
1249
    /**
1250
     * This simply returns the datetime for the given field name
1251
     * Note: this protected function is called by the wrapper get_date or get_time or get_datetime functions
1252
     * (and the equivalent e_date, e_time, e_datetime).
1253
     *
1254
     * @access   protected
1255
     * @param string   $field_name   Field on the instantiated EE_Base_Class child object
1256
     * @param string   $dt_frmt      valid datetime format used for date
1257
     *                               (if '' then we just use the default on the field,
1258
     *                               if NULL we use the last-used format)
1259
     * @param string   $tm_frmt      Same as above except this is for time format
1260
     * @param string   $date_or_time if NULL then both are returned, otherwise "D" = only date and "T" = only time.
1261
     * @param  boolean $echo         Whether the dtt is echoing using pretty echoing or just returned using vanilla get
1262
     * @return string|bool|EE_Error string on success, FALSE on fail, or EE_Error Exception is thrown
1263
     *                               if field is not a valid dtt field, or void if echoing
1264
     * @throws EE_Error
1265
     */
1266
    protected function _get_datetime($field_name, $dt_frmt = '', $tm_frmt = '', $date_or_time = '', $echo = false)
1267
    {
1268
        // clear cached property
1269
        $this->_clear_cached_property($field_name);
1270
        //reset format properties because they are used in get()
1271
        $this->_dt_frmt = $dt_frmt !== '' ? $dt_frmt : $this->_dt_frmt;
1272
        $this->_tm_frmt = $tm_frmt !== '' ? $tm_frmt : $this->_tm_frmt;
1273
        if ($echo) {
1274
            $this->e($field_name, $date_or_time);
1275
            return '';
1276
        }
1277
        return $this->get($field_name, $date_or_time);
1278
    }
1279
1280
1281
1282
    /**
1283
     * below are wrapper functions for the various datetime outputs that can be obtained for JUST returning the date
1284
     * portion of a datetime value. (note the only difference between get_ and e_ is one returns the value and the
1285
     * other echoes the pretty value for dtt)
1286
     *
1287
     * @param  string $field_name name of model object datetime field holding the value
1288
     * @param  string $format     format for the date returned (if NULL we use default in dt_frmt property)
1289
     * @return string            datetime value formatted
1290
     * @throws EE_Error
1291
     */
1292
    public function get_date($field_name, $format = '')
1293
    {
1294
        return $this->_get_datetime($field_name, $format, null, 'D');
1295
    }
1296
1297
1298
1299
    /**
1300
     * @param      $field_name
1301
     * @param string $format
1302
     * @throws EE_Error
1303
     */
1304
    public function e_date($field_name, $format = '')
1305
    {
1306
        $this->_get_datetime($field_name, $format, null, 'D', true);
1307
    }
1308
1309
1310
1311
    /**
1312
     * below are wrapper functions for the various datetime outputs that can be obtained for JUST returning the time
1313
     * portion of a datetime value. (note the only difference between get_ and e_ is one returns the value and the
1314
     * other echoes the pretty value for dtt)
1315
     *
1316
     * @param  string $field_name name of model object datetime field holding the value
1317
     * @param  string $format     format for the time returned ( if NULL we use default in tm_frmt property)
1318
     * @return string             datetime value formatted
1319
     * @throws EE_Error
1320
     */
1321
    public function get_time($field_name, $format = '')
1322
    {
1323
        return $this->_get_datetime($field_name, null, $format, 'T');
1324
    }
1325
1326
1327
1328
    /**
1329
     * @param      $field_name
1330
     * @param string $format
1331
     * @throws EE_Error
1332
     */
1333
    public function e_time($field_name, $format = '')
1334
    {
1335
        $this->_get_datetime($field_name, null, $format, 'T', true);
1336
    }
1337
1338
1339
1340
    /**
1341
     * below are wrapper functions for the various datetime outputs that can be obtained for returning the date AND
1342
     * time portion of a datetime value. (note the only difference between get_ and e_ is one returns the value and the
1343
     * other echoes the pretty value for dtt)
1344
     *
1345
     * @param  string $field_name name of model object datetime field holding the value
1346
     * @param  string $dt_frmt    format for the date returned (if NULL we use default in dt_frmt property)
1347
     * @param  string $tm_frmt    format for the time returned (if NULL we use default in tm_frmt property)
1348
     * @return string             datetime value formatted
1349
     * @throws EE_Error
1350
     */
1351
    public function get_datetime($field_name, $dt_frmt = '', $tm_frmt = '')
1352
    {
1353
        return $this->_get_datetime($field_name, $dt_frmt, $tm_frmt);
1354
    }
1355
1356
1357
1358
    /**
1359
     * @param string $field_name
1360
     * @param string $dt_frmt
1361
     * @param string $tm_frmt
1362
     * @throws EE_Error
1363
     */
1364
    public function e_datetime($field_name, $dt_frmt = '', $tm_frmt = '')
1365
    {
1366
        $this->_get_datetime($field_name, $dt_frmt, $tm_frmt, null, true);
1367
    }
1368
1369
1370
    /**
1371
     * Get the i8ln value for a date using the WordPress @see date_i18n function.
1372
     *
1373
     * @param string $field_name The EE_Datetime_Field reference for the date being retrieved.
1374
     * @param string $format PHP valid date/time string format.  If none is provided then the internal set format
1375
     *                           on the object will be used.
1376
     * @return string Date and time string in set locale or false if no field exists for the given
1377
     * @throws InvalidArgumentException
1378
     * @throws InvalidInterfaceException
1379
     * @throws InvalidDataTypeException
1380
     * @throws EE_Error
1381
     *                           field name.
1382
     */
1383
    public function get_i18n_datetime($field_name, $format = '')
1384
    {
1385
        $format = empty($format) ? $this->_dt_frmt . ' ' . $this->_tm_frmt : $format;
1386
        return date_i18n(
1387
            $format,
1388
            EEH_DTT_Helper::get_timestamp_with_offset($this->get_raw($field_name), $this->_timezone)
1389
        );
1390
    }
1391
1392
1393
1394
    /**
1395
     * This method validates whether the given field name is a valid field on the model object as well as it is of a
1396
     * type EE_Datetime_Field.  On success there will be returned the field settings.  On fail an EE_Error exception is
1397
     * thrown.
1398
     *
1399
     * @param  string $field_name The field name being checked
1400
     * @throws EE_Error
1401
     * @return EE_Datetime_Field
1402
     */
1403
    protected function _get_dtt_field_settings($field_name)
1404
    {
1405
        $field = $this->get_model()->field_settings_for($field_name);
1406
        //check if field is dtt
1407
        if ($field instanceof EE_Datetime_Field) {
1408
            return $field;
1409
        } else {
1410
            throw new EE_Error(sprintf(__('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',
1411
                'event_espresso'), $field_name, self::_get_model_classname(get_class($this))));
1412
        }
1413
    }
1414
1415
1416
1417
1418
    /**
1419
     * NOTE ABOUT BELOW:
1420
     * These convenience date and time setters are for setting date and time independently.  In other words you might
1421
     * want to change the time on a datetime_field but leave the date the same (or vice versa). IF on the other hand
1422
     * you want to set both date and time at the same time, you can just use the models default set($fieldname,$value)
1423
     * method and make sure you send the entire datetime value for setting.
1424
     */
1425
    /**
1426
     * sets the time on a datetime property
1427
     *
1428
     * @access protected
1429
     * @param string|Datetime $time      a valid time string for php datetime functions (or DateTime object)
1430
     * @param string          $fieldname the name of the field the time is being set on (must match a EE_Datetime_Field)
1431
     * @throws EE_Error
1432
     */
1433
    protected function _set_time_for($time, $fieldname)
1434
    {
1435
        $this->_set_date_time('T', $time, $fieldname);
1436
    }
1437
1438
1439
1440
    /**
1441
     * sets the date on a datetime property
1442
     *
1443
     * @access protected
1444
     * @param string|DateTime $date      a valid date string for php datetime functions ( or DateTime object)
1445
     * @param string          $fieldname the name of the field the date is being set on (must match a EE_Datetime_Field)
1446
     * @throws EE_Error
1447
     */
1448
    protected function _set_date_for($date, $fieldname)
1449
    {
1450
        $this->_set_date_time('D', $date, $fieldname);
1451
    }
1452
1453
1454
    /**
1455
     * This takes care of setting a date or time independently on a given model object property. This method also
1456
     * verifies that the given fieldname matches a model object property and is for a EE_Datetime_Field field
1457
     *
1458
     * @access protected
1459
     * @param string $what "T" for time, 'B' for both, 'D' for Date.
1460
     * @param string|DateTime $datetime_value A valid Date or Time string (or DateTime object)
1461
     * @param string $fieldname the name of the field the date OR time is being set on (must match a
1462
     *                                        EE_Datetime_Field property)
1463
     * @throws InvalidArgumentException
1464
     * @throws InvalidInterfaceException
1465
     * @throws InvalidDataTypeException
1466
     * @throws EE_Error
1467
     */
1468
    protected function _set_date_time($what = 'T', $datetime_value, $fieldname)
1469
    {
1470
        $field = $this->_get_dtt_field_settings($fieldname);
1471
        $field->set_timezone($this->_timezone);
1472
        $field->set_date_format($this->_dt_frmt);
1473
        $field->set_time_format($this->_tm_frmt);
1474
        switch ($what) {
1475
            case 'T' :
1476
                $this->_fields[$fieldname] = $field->prepare_for_set_with_new_time(
1477
                    $datetime_value,
1478
                    $this->_fields[$fieldname]
1479
                );
1480
                break;
1481
            case 'D' :
1482
                $this->_fields[$fieldname] = $field->prepare_for_set_with_new_date(
1483
                    $datetime_value,
1484
                    $this->_fields[$fieldname]
1485
                );
1486
                break;
1487
            case 'B' :
1488
                $this->_fields[$fieldname] = $field->prepare_for_set($datetime_value);
1489
                break;
1490
        }
1491
        $this->_clear_cached_property($fieldname);
1492
    }
1493
1494
1495
    /**
1496
     * This will return a timestamp for the website timezone but ONLY when the current website timezone is different
1497
     * than the timezone set for the website. NOTE, this currently only works well with methods that return values.  If
1498
     * you use it with methods that echo values the $_timestamp property may not get reset to its original value and
1499
     * that could lead to some unexpected results!
1500
     *
1501
     * @access public
1502
     * @param string $field_name This is the name of the field on the object that contains the date/time
1503
     *                                         value being returned.
1504
     * @param string $callback must match a valid method in this class (defaults to get_datetime)
1505
     * @param mixed (array|string) $args       This is the arguments that will be passed to the callback.
1506
     * @param string $prepend You can include something to prepend on the timestamp
1507
     * @param string $append You can include something to append on the timestamp
1508
     * @throws InvalidArgumentException
1509
     * @throws InvalidInterfaceException
1510
     * @throws InvalidDataTypeException
1511
     * @throws EE_Error
1512
     * @return string timestamp
1513
     */
1514
    public function display_in_my_timezone(
1515
        $field_name,
1516
        $callback = 'get_datetime',
1517
        $args = null,
1518
        $prepend = '',
1519
        $append = ''
1520
    ) {
1521
        $timezone = EEH_DTT_Helper::get_timezone();
1522
        if ($timezone === $this->_timezone) {
1523
            return '';
1524
        }
1525
        $original_timezone = $this->_timezone;
1526
        $this->set_timezone($timezone);
1527
        $fn = (array)$field_name;
1528
        $args = array_merge($fn, (array)$args);
1529 View Code Duplication
        if ( ! method_exists($this, $callback)) {
1530
            throw new EE_Error(
1531
                sprintf(
1532
                    __(
1533
                        'The method named "%s" given as the callback param in "display_in_my_timezone" does not exist.  Please check your spelling',
1534
                        'event_espresso'
1535
                    ),
1536
                    $callback
1537
                )
1538
            );
1539
        }
1540
        $args = (array)$args;
1541
        $return = $prepend . call_user_func_array(array($this, $callback), $args) . $append;
1542
        $this->set_timezone($original_timezone);
1543
        return $return;
1544
    }
1545
1546
1547
1548
    /**
1549
     * Deletes this model object.
1550
     * This calls the `EE_Base_Class::_delete` method.  Child classes wishing to change default behaviour should
1551
     * override
1552
     * `EE_Base_Class::_delete` NOT this class.
1553
     *
1554
     * @return boolean | int
1555
     * @throws EE_Error
1556
     */
1557
    public function delete()
1558
    {
1559
        /**
1560
         * Called just before the `EE_Base_Class::_delete` method call.
1561
         * Note: `EE_Base_Class::_delete` might be overridden by child classes so any client code hooking into these actions
1562
         * should be aware that `_delete` may not always result in a permanent delete.  For example, `EE_Soft_Delete_Base_Class::_delete`
1563
         * soft deletes (trash) the object and does not permanently delete it.
1564
         *
1565
         * @param EE_Base_Class $model_object about to be 'deleted'
1566
         */
1567
        do_action('AHEE__EE_Base_Class__delete__before', $this);
1568
        $result = $this->_delete();
1569
        /**
1570
         * Called just after the `EE_Base_Class::_delete` method call.
1571
         * Note: `EE_Base_Class::_delete` might be overridden by child classes so any client code hooking into these actions
1572
         * should be aware that `_delete` may not always result in a permanent delete.  For example `EE_Soft_Base_Class::_delete`
1573
         * soft deletes (trash) the object and does not permanently delete it.
1574
         *
1575
         * @param EE_Base_Class $model_object that was just 'deleted'
1576
         * @param boolean       $result
1577
         */
1578
        do_action('AHEE__EE_Base_Class__delete__end', $this, $result);
1579
        return $result;
1580
    }
1581
1582
1583
1584
    /**
1585
     * Calls the specific delete method for the instantiated class.
1586
     * This method is called by the public `EE_Base_Class::delete` method.  Any child classes desiring to override
1587
     * default functionality for "delete" (which is to call `permanently_delete`) should override this method NOT
1588
     * `EE_Base_Class::delete`
1589
     *
1590
     * @return bool|int
1591
     * @throws EE_Error
1592
     */
1593
    protected function _delete()
1594
    {
1595
        return $this->delete_permanently();
1596
    }
1597
1598
1599
1600
    /**
1601
     * Deletes this model object permanently from db (but keep in mind related models my block the delete and return an
1602
     * error)
1603
     *
1604
     * @return bool | int
1605
     * @throws EE_Error
1606
     */
1607
    public function delete_permanently()
1608
    {
1609
        /**
1610
         * Called just before HARD deleting a model object
1611
         *
1612
         * @param EE_Base_Class $model_object about to be 'deleted'
1613
         */
1614
        do_action('AHEE__EE_Base_Class__delete_permanently__before', $this);
1615
        $model = $this->get_model();
1616
        $result = $model->delete_permanently_by_ID($this->ID());
1617
        $this->refresh_cache_of_related_objects();
1618
        /**
1619
         * Called just after HARD deleting a model object
1620
         *
1621
         * @param EE_Base_Class $model_object that was just 'deleted'
1622
         * @param boolean       $result
1623
         */
1624
        do_action('AHEE__EE_Base_Class__delete_permanently__end', $this, $result);
1625
        return $result;
1626
    }
1627
1628
1629
1630
    /**
1631
     * When this model object is deleted, it may still be cached on related model objects. This clears the cache of
1632
     * related model objects
1633
     *
1634
     * @throws EE_Error
1635
     */
1636
    public function refresh_cache_of_related_objects()
1637
    {
1638
        $model = $this->get_model();
1639
        foreach ($model->relation_settings() as $relation_name => $relation_obj) {
1640
            if ( ! empty($this->_model_relations[$relation_name])) {
1641
                $related_objects = $this->_model_relations[$relation_name];
1642
                if ($relation_obj instanceof EE_Belongs_To_Relation) {
1643
                    //this relation only stores a single model object, not an array
1644
                    //but let's make it consistent
1645
                    $related_objects = array($related_objects);
1646
                }
1647
                foreach ($related_objects as $related_object) {
1648
                    //only refresh their cache if they're in memory
1649
                    if ($related_object instanceof EE_Base_Class) {
1650
                        $related_object->clear_cache($model->get_this_model_name(), $this);
1651
                    }
1652
                }
1653
            }
1654
        }
1655
    }
1656
1657
1658
1659
    /**
1660
     *        Saves this object to the database. An array may be supplied to set some values on this
1661
     * object just before saving.
1662
     *
1663
     * @access public
1664
     * @param array $set_cols_n_values keys are field names, values are their new values,
1665
     *                                 if provided during the save() method (often client code will change the fields'
1666
     *                                 values before calling save)
1667
     * @throws EE_Error
1668
     * @return int , 1 on a successful update, the ID of the new entry on insert; 0 on failure or if the model object
1669
     *                                 isn't allowed to persist (as determined by EE_Base_Class::allow_persist())
1670
     */
1671
    public function save($set_cols_n_values = array())
1672
    {
1673
        $model = $this->get_model();
1674
        /**
1675
         * Filters the fields we're about to save on the model object
1676
         *
1677
         * @param array         $set_cols_n_values
1678
         * @param EE_Base_Class $model_object
1679
         */
1680
        $set_cols_n_values = (array)apply_filters('FHEE__EE_Base_Class__save__set_cols_n_values', $set_cols_n_values,
1681
            $this);
1682
        //set attributes as provided in $set_cols_n_values
1683
        foreach ($set_cols_n_values as $column => $value) {
1684
            $this->set($column, $value);
1685
        }
1686
        // no changes ? then don't do anything
1687
        if (! $this->_has_changes && $this->ID() && $model->get_primary_key_field()->is_auto_increment()) {
1688
            return 0;
1689
        }
1690
        /**
1691
         * Saving a model object.
1692
         * Before we perform a save, this action is fired.
1693
         *
1694
         * @param EE_Base_Class $model_object the model object about to be saved.
1695
         */
1696
        do_action('AHEE__EE_Base_Class__save__begin', $this);
1697
        if ( ! $this->allow_persist()) {
1698
            return 0;
1699
        }
1700
        //now get current attribute values
1701
        $save_cols_n_values = $this->_fields;
1702
        //if the object already has an ID, update it. Otherwise, insert it
1703
        //also: change the assumption about values passed to the model NOT being prepare dby the model object. They have been
1704
        $old_assumption_concerning_value_preparation = $model
1705
                                                            ->get_assumption_concerning_values_already_prepared_by_model_object();
1706
        $model->assume_values_already_prepared_by_model_object(true);
1707
        //does this model have an autoincrement PK?
1708
        if ($model->has_primary_key_field()) {
1709
            if ($model->get_primary_key_field()->is_auto_increment()) {
1710
                //ok check if it's set, if so: update; if not, insert
1711
                if ( ! empty($save_cols_n_values[$model->primary_key_name()])) {
1712
                    $results = $model->update_by_ID($save_cols_n_values, $this->ID());
1713
                } else {
1714
                    unset($save_cols_n_values[$model->primary_key_name()]);
1715
                    $results = $model->insert($save_cols_n_values);
1716
                    if ($results) {
1717
                        //if successful, set the primary key
1718
                        //but don't use the normal SET method, because it will check if
1719
                        //an item with the same ID exists in the mapper & db, then
1720
                        //will find it in the db (because we just added it) and THAT object
1721
                        //will get added to the mapper before we can add this one!
1722
                        //but if we just avoid using the SET method, all that headache can be avoided
1723
                        $pk_field_name = $model->primary_key_name();
1724
                        $this->_fields[$pk_field_name] = $results;
1725
                        $this->_clear_cached_property($pk_field_name);
1726
                        $model->add_to_entity_map($this);
1727
                        $this->_update_cached_related_model_objs_fks();
1728
                    }
1729
                }
1730
            } else {//PK is NOT auto-increment
1731
                //so check if one like it already exists in the db
1732
                if ($model->exists_by_ID($this->ID())) {
1733
                    if (WP_DEBUG && ! $this->in_entity_map()) {
1734
                        throw new EE_Error(
1735
                            sprintf(
1736
                                __('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',
1737
                                    'event_espresso'),
1738
                                get_class($this),
1739
                                get_class($model) . '::instance()->add_to_entity_map()',
1740
                                get_class($model) . '::instance()->get_one_by_ID()',
1741
                                '<br />'
1742
                            )
1743
                        );
1744
                    }
1745
                    $results = $model->update_by_ID($save_cols_n_values, $this->ID());
1746
                } else {
1747
                    $results = $model->insert($save_cols_n_values);
1748
                    $this->_update_cached_related_model_objs_fks();
1749
                }
1750
            }
1751
        } else {//there is NO primary key
1752
            $already_in_db = false;
1753
            foreach ($model->unique_indexes() as $index) {
1754
                $uniqueness_where_params = array_intersect_key($save_cols_n_values, $index->fields());
1755
                if ($model->exists(array($uniqueness_where_params))) {
1756
                    $already_in_db = true;
1757
                }
1758
            }
1759
            if ($already_in_db) {
1760
                $combined_pk_fields_n_values = array_intersect_key($save_cols_n_values,
1761
                    $model->get_combined_primary_key_fields());
1762
                $results = $model->update($save_cols_n_values, $combined_pk_fields_n_values);
1763
            } else {
1764
                $results = $model->insert($save_cols_n_values);
1765
            }
1766
        }
1767
        //restore the old assumption about values being prepared by the model object
1768
        $model
1769
             ->assume_values_already_prepared_by_model_object($old_assumption_concerning_value_preparation);
1770
        /**
1771
         * After saving the model object this action is called
1772
         *
1773
         * @param EE_Base_Class $model_object which was just saved
1774
         * @param boolean|int   $results      if it were updated, TRUE or FALSE; if it were newly inserted
1775
         *                                    the new ID (or 0 if an error occurred and it wasn't updated)
1776
         */
1777
        do_action('AHEE__EE_Base_Class__save__end', $this, $results);
1778
        $this->_has_changes = false;
1779
        return $results;
1780
    }
1781
1782
1783
1784
    /**
1785
     * Updates the foreign key on related models objects pointing to this to have this model object's ID
1786
     * as their foreign key.  If the cached related model objects already exist in the db, saves them (so that the DB
1787
     * is consistent) Especially useful in case we JUST added this model object ot the database and we want to let its
1788
     * cached relations with foreign keys to it know about that change. Eg: we've created a transaction but haven't
1789
     * 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
1790
     * transaction. Now, when we save the transaction, the registration's TXN_ID will be automatically updated, whether
1791
     * or not they exist in the DB (if they do, their DB records will be automatically updated)
1792
     *
1793
     * @return void
1794
     * @throws EE_Error
1795
     */
1796
    protected function _update_cached_related_model_objs_fks()
1797
    {
1798
        $model = $this->get_model();
1799
        foreach ($model->relation_settings() as $relation_name => $relation_obj) {
1800
            if ($relation_obj instanceof EE_Has_Many_Relation) {
1801
                foreach ($this->get_all_from_cache($relation_name) as $related_model_obj_in_cache) {
1802
                    $fk_to_this = $related_model_obj_in_cache->get_model()->get_foreign_key_to(
1803
                        $model->get_this_model_name()
1804
                    );
1805
                    $related_model_obj_in_cache->set($fk_to_this->get_name(), $this->ID());
1806
                    if ($related_model_obj_in_cache->ID()) {
1807
                        $related_model_obj_in_cache->save();
1808
                    }
1809
                }
1810
            }
1811
        }
1812
    }
1813
1814
1815
1816
    /**
1817
     * Saves this model object and its NEW cached relations to the database.
1818
     * (Meaning, for now, IT DOES NOT WORK if the cached items already exist in the DB.
1819
     * In order for that to work, we would need to mark model objects as dirty/clean...
1820
     * because otherwise, there's a potential for infinite looping of saving
1821
     * Saves the cached related model objects, and ensures the relation between them
1822
     * and this object and properly setup
1823
     *
1824
     * @return int ID of new model object on save; 0 on failure+
1825
     * @throws EE_Error
1826
     */
1827
    public function save_new_cached_related_model_objs()
1828
    {
1829
        //make sure this has been saved
1830
        if ( ! $this->ID()) {
1831
            $id = $this->save();
1832
        } else {
1833
            $id = $this->ID();
1834
        }
1835
        //now save all the NEW cached model objects  (ie they don't exist in the DB)
1836
        foreach ($this->get_model()->relation_settings() as $relationName => $relationObj) {
1837
            if ($this->_model_relations[$relationName]) {
1838
                //is this a relation where we should expect just ONE related object (ie, EE_Belongs_To_relation)
1839
                //or MANY related objects (ie, EE_HABTM_Relation or EE_Has_Many_Relation)?
1840
                if ($relationObj instanceof EE_Belongs_To_Relation) {
1841
                    //add a relation to that relation type (which saves the appropriate thing in the process)
1842
                    //but ONLY if it DOES NOT exist in the DB
1843
                    /* @var $related_model_obj EE_Base_Class */
1844
                    $related_model_obj = $this->_model_relations[$relationName];
1845
                    //					if( ! $related_model_obj->ID()){
1846
                    $this->_add_relation_to($related_model_obj, $relationName);
1847
                    $related_model_obj->save_new_cached_related_model_objs();
1848
                    //					}
1849
                } else {
1850
                    foreach ($this->_model_relations[$relationName] as $related_model_obj) {
1851
                        //add a relation to that relation type (which saves the appropriate thing in the process)
1852
                        //but ONLY if it DOES NOT exist in the DB
1853
                        //						if( ! $related_model_obj->ID()){
1854
                        $this->_add_relation_to($related_model_obj, $relationName);
1855
                        $related_model_obj->save_new_cached_related_model_objs();
1856
                        //						}
1857
                    }
1858
                }
1859
            }
1860
        }
1861
        return $id;
1862
    }
1863
1864
1865
1866
    /**
1867
     * for getting a model while instantiated.
1868
     *
1869
     * @return EEM_Base | EEM_CPT_Base
1870
     */
1871
    public function get_model()
1872
    {
1873
        if( ! $this->_model){
1874
            $modelName = self::_get_model_classname(get_class($this));
1875
            $this->_model = self::_get_model_instance_with_name($modelName, $this->_timezone);
1876
        } else {
1877
            $this->_model->set_timezone($this->_timezone);
1878
        }
1879
1880
        return $this->_model;
1881
    }
1882
1883
1884
1885
    /**
1886
     * @param $props_n_values
1887
     * @param $classname
1888
     * @return mixed bool|EE_Base_Class|EEM_CPT_Base
1889
     * @throws EE_Error
1890
     */
1891
    protected static function _get_object_from_entity_mapper($props_n_values, $classname)
1892
    {
1893
        //TODO: will not work for Term_Relationships because they have no PK!
1894
        $primary_id_ref = self::_get_primary_key_name($classname);
1895
        if (array_key_exists($primary_id_ref, $props_n_values) && ! empty($props_n_values[$primary_id_ref])) {
1896
            $id = $props_n_values[$primary_id_ref];
1897
            return self::_get_model($classname)->get_from_entity_map($id);
1898
        }
1899
        return false;
1900
    }
1901
1902
1903
1904
    /**
1905
     * This is called by child static "new_instance" method and we'll check to see if there is an existing db entry for
1906
     * the primary key (if present in incoming values). If there is a key in the incoming array that matches the
1907
     * 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
1908
     * we return false.
1909
     *
1910
     * @param  array  $props_n_values   incoming array of properties and their values
1911
     * @param  string $classname        the classname of the child class
1912
     * @param null    $timezone
1913
     * @param array   $date_formats     incoming date_formats in an array where the first value is the
1914
     *                                  date_format and the second value is the time format
1915
     * @return mixed (EE_Base_Class|bool)
1916
     * @throws EE_Error
1917
     */
1918
    protected static function _check_for_object($props_n_values, $classname, $timezone = null, $date_formats = array())
1919
    {
1920
        $existing = null;
1921
        $model = self::_get_model($classname, $timezone);
1922
        if ($model->has_primary_key_field()) {
1923
            $primary_id_ref = self::_get_primary_key_name($classname);
1924
            if (array_key_exists($primary_id_ref, $props_n_values)
1925
                && ! empty($props_n_values[$primary_id_ref])
1926
            ) {
1927
                $existing = $model->get_one_by_ID(
1928
                    $props_n_values[$primary_id_ref]
1929
                );
1930
            }
1931
        } elseif ($model->has_all_combined_primary_key_fields($props_n_values)) {
1932
            //no primary key on this model, but there's still a matching item in the DB
1933
            $existing = self::_get_model($classname, $timezone)->get_one_by_ID(
1934
                self::_get_model($classname, $timezone)->get_index_primary_key_string($props_n_values)
1935
            );
1936
        }
1937
        if ($existing) {
1938
            //set date formats if present before setting values
1939
            if ( ! empty($date_formats) && is_array($date_formats)) {
1940
                $existing->set_date_format($date_formats[0]);
1941
                $existing->set_time_format($date_formats[1]);
1942
            } else {
1943
                //set default formats for date and time
1944
                $existing->set_date_format(get_option('date_format'));
1945
                $existing->set_time_format(get_option('time_format'));
1946
            }
1947
            foreach ($props_n_values as $property => $field_value) {
1948
                $existing->set($property, $field_value);
1949
            }
1950
            return $existing;
1951
        } else {
1952
            return false;
1953
        }
1954
    }
1955
1956
1957
1958
    /**
1959
     * Gets the EEM_*_Model for this class
1960
     *
1961
     * @access public now, as this is more convenient
1962
     * @param      $classname
1963
     * @param null $timezone
1964
     * @throws EE_Error
1965
     * @return EEM_Base
1966
     */
1967
    protected static function _get_model($classname, $timezone = null)
1968
    {
1969
        //find model for this class
1970
        if ( ! $classname) {
1971
            throw new EE_Error(
1972
                sprintf(
1973
                    __(
1974
                        "What were you thinking calling _get_model(%s)?? You need to specify the class name",
1975
                        "event_espresso"
1976
                    ),
1977
                    $classname
1978
                )
1979
            );
1980
        }
1981
        $modelName = self::_get_model_classname($classname);
1982
        return self::_get_model_instance_with_name($modelName, $timezone);
1983
    }
1984
1985
1986
    /**
1987
     * Gets the model instance (eg instance of EEM_Attendee) given its classname (eg EE_Attendee)
1988
     *
1989
     * @param string $model_classname
1990
     * @param null $timezone
1991
     * @return EEM_Base
1992
     * @throws \ReflectionException
1993
     * @throws InvalidArgumentException
1994
     * @throws InvalidInterfaceException
1995
     * @throws InvalidDataTypeException
1996
     * @throws EE_Error
1997
     */
1998
    protected static function _get_model_instance_with_name($model_classname, $timezone = null)
1999
    {
2000
        $model_classname = str_replace('EEM_', '', $model_classname);
2001
        $model = EE_Registry::instance()->load_model($model_classname);
2002
        $model->set_timezone($timezone);
2003
        return $model;
2004
    }
2005
2006
2007
2008
    /**
2009
     * If a model name is provided (eg Registration), gets the model classname for that model.
2010
     * Also works if a model class's classname is provided (eg EE_Registration).
2011
     *
2012
     * @param null $model_name
2013
     * @return string like EEM_Attendee
2014
     */
2015
    private static function _get_model_classname($model_name = null)
2016
    {
2017
        if (strpos($model_name, "EE_") === 0) {
2018
            $model_classname = str_replace("EE_", "EEM_", $model_name);
2019
        } else {
2020
            $model_classname = "EEM_" . $model_name;
2021
        }
2022
        return $model_classname;
2023
    }
2024
2025
2026
2027
    /**
2028
     * returns the name of the primary key attribute
2029
     *
2030
     * @param null $classname
2031
     * @throws EE_Error
2032
     * @return string
2033
     */
2034
    protected static function _get_primary_key_name($classname = null)
2035
    {
2036
        if ( ! $classname) {
2037
            throw new EE_Error(
2038
                sprintf(
2039
                    __("What were you thinking calling _get_primary_key_name(%s)", "event_espresso"),
2040
                    $classname
2041
                )
2042
            );
2043
        }
2044
        return self::_get_model($classname)->get_primary_key_field()->get_name();
2045
    }
2046
2047
2048
2049
    /**
2050
     * Gets the value of the primary key.
2051
     * If the object hasn't yet been saved, it should be whatever the model field's default was
2052
     * (eg, if this were the EE_Event class, look at the primary key field on EEM_Event and see what its default value
2053
     * is. Usually defaults for integer primary keys are 0; string primary keys are usually NULL).
2054
     *
2055
     * @return mixed, if the primary key is of type INT it'll be an int. Otherwise it could be a string
2056
     * @throws EE_Error
2057
     */
2058
    public function ID()
2059
    {
2060
        $model = $this->get_model();
2061
        //now that we know the name of the variable, use a variable variable to get its value and return its
2062
        if ($model->has_primary_key_field()) {
2063
            return $this->_fields[$model->primary_key_name()];
2064
        } else {
2065
            return $model->get_index_primary_key_string($this->_fields);
2066
        }
2067
    }
2068
2069
2070
2071
    /**
2072
     * Adds a relationship to the specified EE_Base_Class object, given the relationship's name. Eg, if the current
2073
     * model is related to a group of events, the $relationName should be 'Event', and should be a key in the EE
2074
     * Model's $_model_relations array. If this model object doesn't exist in the DB, just caches the related thing
2075
     *
2076
     * @param mixed  $otherObjectModelObjectOrID       EE_Base_Class or the ID of the other object
2077
     * @param string $relationName                     eg 'Events','Question',etc.
2078
     *                                                 an attendee to a group, you also want to specify which role they
2079
     *                                                 will have in that group. So you would use this parameter to
2080
     *                                                 specify array('role-column-name'=>'role-id')
2081
     * @param array  $extra_join_model_fields_n_values You can optionally include an array of key=>value pairs that
2082
     *                                                 allow you to further constrict the relation to being added.
2083
     *                                                 However, keep in mind that the columns (keys) given must match a
2084
     *                                                 column on the JOIN table and currently only the HABTM models
2085
     *                                                 accept these additional conditions.  Also remember that if an
2086
     *                                                 exact match isn't found for these extra cols/val pairs, then a
2087
     *                                                 NEW row is created in the join table.
2088
     * @param null   $cache_id
2089
     * @throws EE_Error
2090
     * @return EE_Base_Class the object the relation was added to
2091
     */
2092
    public function _add_relation_to(
2093
        $otherObjectModelObjectOrID,
2094
        $relationName,
2095
        $extra_join_model_fields_n_values = array(),
2096
        $cache_id = null
2097
    ) {
2098
        $model = $this->get_model();
2099
        //if this thing exists in the DB, save the relation to the DB
2100
        if ($this->ID()) {
2101
            $otherObject = $model
2102
                                ->add_relationship_to($this, $otherObjectModelObjectOrID, $relationName,
2103
                                    $extra_join_model_fields_n_values);
2104
            //clear cache so future get_many_related and get_first_related() return new results.
2105
            $this->clear_cache($relationName, $otherObject, true);
2106
            if ($otherObject instanceof EE_Base_Class) {
2107
                $otherObject->clear_cache($model->get_this_model_name(), $this);
2108
            }
2109 View Code Duplication
        } else {
2110
            //this thing doesn't exist in the DB,  so just cache it
2111
            if ( ! $otherObjectModelObjectOrID instanceof EE_Base_Class) {
2112
                throw new EE_Error(sprintf(
2113
                    __('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',
2114
                        'event_espresso'),
2115
                    $otherObjectModelObjectOrID,
2116
                    get_class($this)
2117
                ));
2118
            } else {
2119
                $otherObject = $otherObjectModelObjectOrID;
2120
            }
2121
            $this->cache($relationName, $otherObjectModelObjectOrID, $cache_id);
2122
        }
2123
        if ($otherObject instanceof EE_Base_Class) {
2124
            //fix the reciprocal relation too
2125
            if ($otherObject->ID()) {
2126
                //its saved so assumed relations exist in the DB, so we can just
2127
                //clear the cache so future queries use the updated info in the DB
2128
                $otherObject->clear_cache($model->get_this_model_name(), null, true);
2129
            } else {
2130
                //it's not saved, so it caches relations like this
2131
                $otherObject->cache($model->get_this_model_name(), $this);
2132
            }
2133
        }
2134
        return $otherObject;
2135
    }
2136
2137
2138
2139
    /**
2140
     * Removes a relationship to the specified EE_Base_Class object, given the relationships' name. Eg, if the current
2141
     * model is related to a group of events, the $relationName should be 'Events', and should be a key in the EE
2142
     * Model's $_model_relations array. If this model object doesn't exist in the DB, just removes the related thing
2143
     * from the cache
2144
     *
2145
     * @param mixed  $otherObjectModelObjectOrID
2146
     *                EE_Base_Class or the ID of the other object, OR an array key into the cache if this isn't saved
2147
     *                to the DB yet
2148
     * @param string $relationName
2149
     * @param array  $where_query
2150
     *                You can optionally include an array of key=>value pairs that allow you to further constrict the
2151
     *                relation to being added. However, keep in mind that the columns (keys) given must match a column
2152
     *                on the JOIN table and currently only the HABTM models accept these additional conditions. Also
2153
     *                remember that if an exact match isn't found for these extra cols/val pairs, then a NEW row is
2154
     *                created in the join table.
2155
     * @return EE_Base_Class the relation was removed from
2156
     * @throws EE_Error
2157
     */
2158
    public function _remove_relation_to($otherObjectModelObjectOrID, $relationName, $where_query = array())
2159
    {
2160
        if ($this->ID()) {
2161
            //if this exists in the DB, save the relation change to the DB too
2162
            $otherObject = $this->get_model()
2163
                                ->remove_relationship_to($this, $otherObjectModelObjectOrID, $relationName,
2164
                                    $where_query);
2165
            $this->clear_cache($relationName, $otherObject);
2166
        } else {
2167
            //this doesn't exist in the DB, just remove it from the cache
2168
            $otherObject = $this->clear_cache($relationName, $otherObjectModelObjectOrID);
2169
        }
2170
        if ($otherObject instanceof EE_Base_Class) {
2171
            $otherObject->clear_cache($this->get_model()->get_this_model_name(), $this);
2172
        }
2173
        return $otherObject;
2174
    }
2175
2176
2177
2178
    /**
2179
     * Removes ALL the related things for the $relationName.
2180
     *
2181
     * @param string $relationName
2182
     * @param array  $where_query_params like EEM_Base::get_all's $query_params[0] (where conditions)
2183
     * @return EE_Base_Class
2184
     * @throws EE_Error
2185
     */
2186
    public function _remove_relations($relationName, $where_query_params = array())
2187
    {
2188
        if ($this->ID()) {
2189
            //if this exists in the DB, save the relation change to the DB too
2190
            $otherObjects = $this->get_model()->remove_relations($this, $relationName, $where_query_params);
2191
            $this->clear_cache($relationName, null, true);
2192
        } else {
2193
            //this doesn't exist in the DB, just remove it from the cache
2194
            $otherObjects = $this->clear_cache($relationName, null, true);
2195
        }
2196
        if (is_array($otherObjects)) {
2197
            foreach ($otherObjects as $otherObject) {
2198
                $otherObject->clear_cache($this->get_model()->get_this_model_name(), $this);
2199
            }
2200
        }
2201
        return $otherObjects;
2202
    }
2203
2204
2205
2206
    /**
2207
     * Gets all the related model objects of the specified type. Eg, if the current class if
2208
     * EE_Event, you could call $this->get_many_related('Registration') to get an array of all the
2209
     * EE_Registration objects which related to this event. Note: by default, we remove the "default query params"
2210
     * because we want to get even deleted items etc.
2211
     *
2212
     * @param string $relationName key in the model's _model_relations array
2213
     * @param array  $query_params like EEM_Base::get_all
2214
     * @return EE_Base_Class[] Results not necessarily indexed by IDs, because some results might not have primary keys
2215
     * @throws EE_Error
2216
     *                             or might not be saved yet. Consider using EEM_Base::get_IDs() on these results if
2217
     *                             you want IDs
2218
     */
2219
    public function get_many_related($relationName, $query_params = array())
2220
    {
2221
        if ($this->ID()) {
2222
            //this exists in the DB, so get the related things from either the cache or the DB
2223
            //if there are query parameters, forget about caching the related model objects.
2224
            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...
2225
                $related_model_objects = $this->get_model()->get_all_related($this, $relationName, $query_params);
2226
            } else {
2227
                //did we already cache the result of this query?
2228
                $cached_results = $this->get_all_from_cache($relationName);
2229
                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...
2230
                    $related_model_objects = $this->get_model()->get_all_related($this, $relationName, $query_params);
2231
                    //if no query parameters were passed, then we got all the related model objects
2232
                    //for that relation. We can cache them then.
2233
                    foreach ($related_model_objects as $related_model_object) {
2234
                        $this->cache($relationName, $related_model_object);
2235
                    }
2236
                } else {
2237
                    $related_model_objects = $cached_results;
2238
                }
2239
            }
2240
        } else {
2241
            //this doesn't exist in the DB, so just get the related things from the cache
2242
            $related_model_objects = $this->get_all_from_cache($relationName);
2243
        }
2244
        return $related_model_objects;
2245
    }
2246
2247
2248
    /**
2249
     * Instead of getting the related model objects, simply counts them. Ignores default_where_conditions by default,
2250
     * unless otherwise specified in the $query_params
2251
     *
2252
     * @param string $relation_name model_name like 'Event', or 'Registration'
2253
     * @param array $query_params like EEM_Base::get_all's
2254
     * @param string $field_to_count name of field to count by. By default, uses primary key
2255
     * @param bool $distinct if we want to only count the distinct values for the column then you can trigger
2256
     *                               that by the setting $distinct to TRUE;
2257
     * @return int
2258
     * @throws EE_Error
2259
     */
2260
    public function count_related($relation_name, $query_params = array(), $field_to_count = null, $distinct = false)
2261
    {
2262
        return $this->get_model()->count_related($this, $relation_name, $query_params, $field_to_count, $distinct);
2263
    }
2264
2265
2266
    /**
2267
     * Instead of getting the related model objects, simply sums up the values of the specified field.
2268
     * Note: ignores default_where_conditions by default, unless otherwise specified in the $query_params
2269
     *
2270
     * @param string $relation_name model_name like 'Event', or 'Registration'
2271
     * @param array $query_params like EEM_Base::get_all's
2272
     * @param string $field_to_sum name of field to count by.
2273
     *                              By default, uses primary key (which doesn't make much sense, so you should probably
2274
     *                              change it)
2275
     * @return int
2276
     * @throws EE_Error
2277
     */
2278
    public function sum_related($relation_name, $query_params = array(), $field_to_sum = null)
2279
    {
2280
        return $this->get_model()->sum_related($this, $relation_name, $query_params, $field_to_sum);
2281
    }
2282
2283
2284
2285
    /**
2286
     * Gets the first (ie, one) related model object of the specified type.
2287
     *
2288
     * @param string $relationName key in the model's _model_relations array
2289
     * @param array  $query_params like EEM_Base::get_all
2290
     * @return EE_Base_Class (not an array, a single object)
2291
     * @throws EE_Error
2292
     */
2293
    public function get_first_related($relationName, $query_params = array())
2294
    {
2295
        $model = $this->get_model();
2296
        if ($this->ID()) {//this exists in the DB, get from the cache OR the DB
2297
            //if they've provided some query parameters, don't bother trying to cache the result
2298
            //also make sure we're not caching the result of get_first_related
2299
            //on a relation which should have an array of objects (because the cache might have an array of objects)
2300
            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...
2301
                || ! $model->related_settings_for($relationName)
2302
                     instanceof
2303
                     EE_Belongs_To_Relation
2304
            ) {
2305
                $related_model_object = $model->get_first_related($this, $relationName, $query_params);
2306
            } else {
2307
                //first, check if we've already cached the result of this query
2308
                $cached_result = $this->get_one_from_cache($relationName);
2309
                if ( ! $cached_result) {
2310
                    $related_model_object = $model->get_first_related($this, $relationName, $query_params);
2311
                    $this->cache($relationName, $related_model_object);
2312
                } else {
2313
                    $related_model_object = $cached_result;
2314
                }
2315
            }
2316
        } else {
2317
            $related_model_object = null;
2318
            //this doesn't exist in the Db, but maybe the relation is of type belongs to, and so the related thing might
2319
            if ($model->related_settings_for($relationName) instanceof EE_Belongs_To_Relation) {
2320
                $related_model_object = $model->get_first_related($this, $relationName, $query_params);
2321
            }
2322
            //this doesn't exist in the DB and apparently the thing it belongs to doesn't either, just get what's cached on this object
2323
            if ( ! $related_model_object) {
2324
                $related_model_object = $this->get_one_from_cache($relationName);
2325
            }
2326
        }
2327
        return $related_model_object;
2328
    }
2329
2330
2331
2332
    /**
2333
     * Does a delete on all related objects of type $relationName and removes
2334
     * the current model object's relation to them. If they can't be deleted (because
2335
     * of blocking related model objects) does nothing. If the related model objects are
2336
     * soft-deletable, they will be soft-deleted regardless of related blocking model objects.
2337
     * If this model object doesn't exist yet in the DB, just removes its related things
2338
     *
2339
     * @param string $relationName
2340
     * @param array  $query_params like EEM_Base::get_all's
2341
     * @return int how many deleted
2342
     * @throws EE_Error
2343
     */
2344 View Code Duplication
    public function delete_related($relationName, $query_params = array())
2345
    {
2346
        if ($this->ID()) {
2347
            $count = $this->get_model()->delete_related($this, $relationName, $query_params);
2348
        } else {
2349
            $count = count($this->get_all_from_cache($relationName));
2350
            $this->clear_cache($relationName, null, true);
2351
        }
2352
        return $count;
2353
    }
2354
2355
2356
2357
    /**
2358
     * Does a hard delete (ie, removes the DB row) on all related objects of type $relationName and removes
2359
     * the current model object's relation to them. If they can't be deleted (because
2360
     * of blocking related model objects) just does a soft delete on it instead, if possible.
2361
     * If the related thing isn't a soft-deletable model object, this function is identical
2362
     * to delete_related(). If this model object doesn't exist in the DB, just remove its related things
2363
     *
2364
     * @param string $relationName
2365
     * @param array  $query_params like EEM_Base::get_all's
2366
     * @return int how many deleted (including those soft deleted)
2367
     * @throws EE_Error
2368
     */
2369 View Code Duplication
    public function delete_related_permanently($relationName, $query_params = array())
2370
    {
2371
        if ($this->ID()) {
2372
            $count = $this->get_model()->delete_related_permanently($this, $relationName, $query_params);
2373
        } else {
2374
            $count = count($this->get_all_from_cache($relationName));
2375
        }
2376
        $this->clear_cache($relationName, null, true);
2377
        return $count;
2378
    }
2379
2380
2381
2382
    /**
2383
     * is_set
2384
     * Just a simple utility function children can use for checking if property exists
2385
     *
2386
     * @access  public
2387
     * @param  string $field_name property to check
2388
     * @return bool                              TRUE if existing,FALSE if not.
2389
     */
2390
    public function is_set($field_name)
2391
    {
2392
        return isset($this->_fields[$field_name]);
2393
    }
2394
2395
2396
2397
    /**
2398
     * Just a simple utility function children can use for checking if property (or properties) exists and throwing an
2399
     * EE_Error exception if they don't
2400
     *
2401
     * @param  mixed (string|array) $properties properties to check
2402
     * @throws EE_Error
2403
     * @return bool                              TRUE if existing, throw EE_Error if not.
2404
     */
2405
    protected function _property_exists($properties)
2406
    {
2407
        foreach ((array)$properties as $property_name) {
2408
            //first make sure this property exists
2409
            if ( ! $this->_fields[$property_name]) {
2410
                throw new EE_Error(
2411
                    sprintf(
2412
                        __(
2413
                            'Trying to retrieve a non-existent property (%s).  Double check the spelling please',
2414
                            'event_espresso'
2415
                        ),
2416
                        $property_name
2417
                    )
2418
                );
2419
            }
2420
        }
2421
        return true;
2422
    }
2423
2424
2425
2426
    /**
2427
     * This simply returns an array of model fields for this object
2428
     *
2429
     * @return array
2430
     * @throws EE_Error
2431
     */
2432
    public function model_field_array()
2433
    {
2434
        $fields = $this->get_model()->field_settings(false);
2435
        $properties = array();
2436
        //remove prepended underscore
2437
        foreach ($fields as $field_name => $settings) {
2438
            $properties[$field_name] = $this->get($field_name);
2439
        }
2440
        return $properties;
2441
    }
2442
2443
2444
2445
    /**
2446
     * Very handy general function to allow for plugins to extend any child of EE_Base_Class.
2447
     * If a method is called on a child of EE_Base_Class that doesn't exist, this function is called
2448
     * (http://www.garfieldtech.com/blog/php-magic-call) and passed the method's name and arguments. Instead of
2449
     * requiring a plugin to extend the EE_Base_Class (which works fine is there's only 1 plugin, but when will that
2450
     * happen?) they can add a hook onto 'filters_hook_espresso__{className}__{methodName}' (eg,
2451
     * filters_hook_espresso__EE_Answer__my_great_function) and accepts 2 arguments: the object on which the function
2452
     * was called, and an array of the original arguments passed to the function. Whatever their callback function
2453
     * returns will be returned by this function. Example: in functions.php (or in a plugin):
2454
     * add_filter('FHEE__EE_Answer__my_callback','my_callback',10,3); function
2455
     * my_callback($previousReturnValue,EE_Base_Class $object,$argsArray){
2456
     * $returnString= "you called my_callback! and passed args:".implode(",",$argsArray);
2457
     *        return $previousReturnValue.$returnString;
2458
     * }
2459
     * require('EE_Answer.class.php');
2460
     * $answer= EE_Answer::new_instance(array('REG_ID' => 2,'QST_ID' => 3,'ANS_value' => The answer is 42'));
2461
     * echo $answer->my_callback('monkeys',100);
2462
     * //will output "you called my_callback! and passed args:monkeys,100"
2463
     *
2464
     * @param string $methodName name of method which was called on a child of EE_Base_Class, but which
2465
     * @param array  $args       array of original arguments passed to the function
2466
     * @throws EE_Error
2467
     * @return mixed whatever the plugin which calls add_filter decides
2468
     */
2469
    public function __call($methodName, $args)
2470
    {
2471
        $className = get_class($this);
2472
        $tagName = "FHEE__{$className}__{$methodName}";
2473
        if ( ! has_filter($tagName)) {
2474
            throw new EE_Error(
2475
                sprintf(
2476
                    __(
2477
                        "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;}",
2478
                        "event_espresso"
2479
                    ),
2480
                    $methodName,
2481
                    $className,
2482
                    $tagName
2483
                )
2484
            );
2485
        }
2486
        return apply_filters($tagName, null, $this, $args);
2487
    }
2488
2489
2490
    /**
2491
     * Similar to insert_post_meta, adds a record in the Extra_Meta model's table with the given key and value.
2492
     * A $previous_value can be specified in case there are many meta rows with the same key
2493
     *
2494
     * @param string $meta_key
2495
     * @param mixed $meta_value
2496
     * @param mixed $previous_value
2497
     * @return bool|int # of records updated (or BOOLEAN if we actually ended up inserting the extra meta row)
2498
     * @throws InvalidArgumentException
2499
     * @throws InvalidInterfaceException
2500
     * @throws InvalidDataTypeException
2501
     * @throws EE_Error
2502
     * NOTE: if the values haven't changed, returns 0
2503
     */
2504
    public function update_extra_meta($meta_key, $meta_value, $previous_value = null)
2505
    {
2506
        $query_params = array(
2507
            array(
2508
                'EXM_key'  => $meta_key,
2509
                'OBJ_ID'   => $this->ID(),
2510
                'EXM_type' => $this->get_model()->get_this_model_name(),
2511
            ),
2512
        );
2513
        if ($previous_value !== null) {
2514
            $query_params[0]['EXM_value'] = $meta_value;
2515
        }
2516
        $existing_rows_like_that = EEM_Extra_Meta::instance()->get_all($query_params);
2517
        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...
2518
            return $this->add_extra_meta($meta_key, $meta_value);
2519
        }
2520
        foreach ($existing_rows_like_that as $existing_row) {
2521
            $existing_row->save(array('EXM_value' => $meta_value));
2522
        }
2523
        return count($existing_rows_like_that);
2524
    }
2525
2526
2527
    /**
2528
     * Adds a new extra meta record. If $unique is set to TRUE, we'll first double-check
2529
     * no other extra meta for this model object have the same key. Returns TRUE if the
2530
     * extra meta row was entered, false if not
2531
     *
2532
     * @param string $meta_key
2533
     * @param mixed $meta_value
2534
     * @param boolean $unique
2535
     * @return boolean
2536
     * @throws InvalidArgumentException
2537
     * @throws InvalidInterfaceException
2538
     * @throws InvalidDataTypeException
2539
     * @throws EE_Error
2540
     */
2541
    public function add_extra_meta($meta_key, $meta_value, $unique = false)
2542
    {
2543
        if ($unique) {
2544
            $existing_extra_meta = EEM_Extra_Meta::instance()->get_one(
2545
                array(
2546
                    array(
2547
                        'EXM_key'  => $meta_key,
2548
                        'OBJ_ID'   => $this->ID(),
2549
                        'EXM_type' => $this->get_model()->get_this_model_name(),
2550
                    ),
2551
                )
2552
            );
2553
            if ($existing_extra_meta) {
2554
                return false;
2555
            }
2556
        }
2557
        $new_extra_meta = EE_Extra_Meta::new_instance(
2558
            array(
2559
                'EXM_key'   => $meta_key,
2560
                'EXM_value' => $meta_value,
2561
                'OBJ_ID'    => $this->ID(),
2562
                'EXM_type'  => $this->get_model()->get_this_model_name(),
2563
            )
2564
        );
2565
        $new_extra_meta->save();
2566
        return true;
2567
    }
2568
2569
2570
    /**
2571
     * Deletes all the extra meta rows for this record as specified by key. If $meta_value
2572
     * is specified, only deletes extra meta records with that value.
2573
     *
2574
     * @param string $meta_key
2575
     * @param mixed $meta_value
2576
     * @return int number of extra meta rows deleted
2577
     * @throws InvalidArgumentException
2578
     * @throws InvalidInterfaceException
2579
     * @throws InvalidDataTypeException
2580
     * @throws EE_Error
2581
     */
2582
    public function delete_extra_meta($meta_key, $meta_value = null)
2583
    {
2584
        $query_params = array(
2585
            array(
2586
                'EXM_key'  => $meta_key,
2587
                'OBJ_ID'   => $this->ID(),
2588
                'EXM_type' => $this->get_model()->get_this_model_name(),
2589
            ),
2590
        );
2591
        if ($meta_value !== null) {
2592
            $query_params[0]['EXM_value'] = $meta_value;
2593
        }
2594
        return EEM_Extra_Meta::instance()->delete($query_params);
2595
    }
2596
2597
2598
2599
    /**
2600
     * Gets the extra meta with the given meta key. If you specify "single" we just return 1, otherwise
2601
     * an array of everything found. Requires that this model actually have a relation of type EE_Has_Many_Any_Relation.
2602
     * You can specify $default is case you haven't found the extra meta
2603
     *
2604
     * @param string  $meta_key
2605
     * @param boolean $single
2606
     * @param mixed   $default if we don't find anything, what should we return?
2607
     * @return mixed single value if $single; array if ! $single
2608
     * @throws EE_Error
2609
     */
2610
    public function get_extra_meta($meta_key, $single = false, $default = null)
2611
    {
2612
        if ($single) {
2613
            $result = $this->get_first_related('Extra_Meta', array(array('EXM_key' => $meta_key)));
2614
            if ($result instanceof EE_Extra_Meta) {
2615
                return $result->value();
2616
            }
2617
        } else {
2618
            $results = $this->get_many_related('Extra_Meta', array(array('EXM_key' => $meta_key)));
2619
            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...
2620
                $values = array();
2621
                foreach ($results as $result) {
2622
                    if ($result instanceof EE_Extra_Meta) {
2623
                        $values[$result->ID()] = $result->value();
2624
                    }
2625
                }
2626
                return $values;
2627
            }
2628
        }
2629
        //if nothing discovered yet return default.
2630
        return apply_filters(
2631
            'FHEE__EE_Base_Class__get_extra_meta__default_value',
2632
            $default,
2633
            $meta_key,
2634
            $single,
2635
            $this
2636
            );
2637
    }
2638
2639
2640
2641
    /**
2642
     * Returns a simple array of all the extra meta associated with this model object.
2643
     * If $one_of_each_key is true (Default), it will be an array of simple key-value pairs, keys being the
2644
     * extra meta's key, and teh value being its value. However, if there are duplicate extra meta rows with
2645
     * the same key, only one will be used. (eg array('foo'=>'bar','monkey'=>123))
2646
     * If $one_of_each_key is false, it will return an array with the top-level keys being
2647
     * the extra meta keys, but their values are also arrays, which have the extra-meta's ID as their sub-key, and
2648
     * finally the extra meta's value as each sub-value. (eg
2649
     * array('foo'=>array(1=>'bar',2=>'bill'),'monkey'=>array(3=>123)))
2650
     *
2651
     * @param boolean $one_of_each_key
2652
     * @return array
2653
     * @throws EE_Error
2654
     */
2655
    public function all_extra_meta_array($one_of_each_key = true)
2656
    {
2657
        $return_array = array();
2658
        if ($one_of_each_key) {
2659
            $extra_meta_objs = $this->get_many_related('Extra_Meta', array('group_by' => 'EXM_key'));
2660
            foreach ($extra_meta_objs as $extra_meta_obj) {
2661
                if ($extra_meta_obj instanceof EE_Extra_Meta) {
2662
                    $return_array[$extra_meta_obj->key()] = $extra_meta_obj->value();
2663
                }
2664
            }
2665
        } else {
2666
            $extra_meta_objs = $this->get_many_related('Extra_Meta');
2667
            foreach ($extra_meta_objs as $extra_meta_obj) {
2668
                if ($extra_meta_obj instanceof EE_Extra_Meta) {
2669
                    if ( ! isset($return_array[$extra_meta_obj->key()])) {
2670
                        $return_array[$extra_meta_obj->key()] = array();
2671
                    }
2672
                    $return_array[$extra_meta_obj->key()][$extra_meta_obj->ID()] = $extra_meta_obj->value();
2673
                }
2674
            }
2675
        }
2676
        return $return_array;
2677
    }
2678
2679
2680
2681
    /**
2682
     * Gets a pretty nice displayable nice for this model object. Often overridden
2683
     *
2684
     * @return string
2685
     * @throws EE_Error
2686
     */
2687
    public function name()
2688
    {
2689
        //find a field that's not a text field
2690
        $field_we_can_use = $this->get_model()->get_a_field_of_type('EE_Text_Field_Base');
2691
        if ($field_we_can_use) {
2692
            return $this->get($field_we_can_use->get_name());
2693
        } else {
2694
            $first_few_properties = $this->model_field_array();
2695
            $first_few_properties = array_slice($first_few_properties, 0, 3);
2696
            $name_parts = array();
2697
            foreach ($first_few_properties as $name => $value) {
2698
                $name_parts[] = "$name:$value";
2699
            }
2700
            return implode(",", $name_parts);
2701
        }
2702
    }
2703
2704
2705
2706
    /**
2707
     * in_entity_map
2708
     * Checks if this model object has been proven to already be in the entity map
2709
     *
2710
     * @return boolean
2711
     * @throws EE_Error
2712
     */
2713
    public function in_entity_map()
2714
    {
2715
        if ($this->ID() && $this->get_model()->get_from_entity_map($this->ID()) === $this) {
2716
            //well, if we looked, did we find it in the entity map?
2717
            return true;
2718
        } else {
2719
            return false;
2720
        }
2721
    }
2722
2723
2724
2725
    /**
2726
     * refresh_from_db
2727
     * Makes sure the fields and values on this model object are in-sync with what's in the database.
2728
     *
2729
     * @throws EE_Error if this model object isn't in the entity mapper (because then you should
2730
     * just use what's in the entity mapper and refresh it) and WP_DEBUG is TRUE
2731
     */
2732
    public function refresh_from_db()
2733
    {
2734
        if ($this->ID() && $this->in_entity_map()) {
2735
            $this->get_model()->refresh_entity_map_from_db($this->ID());
2736
        } else {
2737
            //if it doesn't have ID, you shouldn't be asking to refresh it from teh database (because its not in the database)
2738
            //if it has an ID but it's not in the map, and you're asking me to refresh it
2739
            //that's kinda dangerous. You should just use what's in the entity map, or add this to the entity map if there's
2740
            //absolutely nothing in it for this ID
2741
            if (WP_DEBUG) {
2742
                throw new EE_Error(
2743
                    sprintf(
2744
                        __('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.',
2745
                            'event_espresso'),
2746
                        $this->ID(),
2747
                        get_class($this->get_model()) . '::instance()->add_to_entity_map()',
2748
                        get_class($this->get_model()) . '::instance()->refresh_entity_map()'
2749
                    )
2750
                );
2751
            }
2752
        }
2753
    }
2754
2755
2756
    /**
2757
     * Gets the money field's amount in subunits (and if the currency has no subunits, gets it in the main units).
2758
     * If you want to use this method, the class must implement UsesMoneyInterface and injected a MoneyFactory in
2759
     * its constructor
2760
     * @param string $money_field_name
2761
     * @return int
2762
     * @throws InvalidEntityException
2763
     * @throws EE_Error
2764
     * @throws DomainException
2765
     * @since $VID:$
2766
     */
2767
    public function moneyInSubunits($money_field_name)
2768
    {
2769
        $this->verifyUsesMoney(__FUNCTION__);
2770
        return $this->getMoneyObject($money_field_name)->amountInSubunits();
2771
    }
2772
2773
2774
    /**
2775
     * Sets the money field's amount based on the incoming monetary subunits (eg pennies). If the currency has no
2776
     * subunits, the amount is actually assumed to be in the currency's main units.
2777
     * If you want to use this method, the class must implement UsesMoneyInterface and injected a MoneyFactory in
2778
     * its constructor
2779
     *
2780
     * @param string $money_field_name
2781
     * @param int    $amount_in_subunits
2782
     * @throws InvalidArgumentException
2783
     * @throws InvalidInterfaceException
2784
     * @throws InvalidIdentifierException
2785
     * @throws InvalidDataTypeException
2786
     * @throws EE_Error
2787
     * @throws DomainException
2788
     * @since $VID:$
2789
     */
2790
    public function setMoneySubunits($money_field_name,$amount_in_subunits)
2791
    {
2792
        $this->verifyUsesMoney(__FUNCTION__);
2793
        $money = $this->money_factory->createFromSubUnits(
2794
            $amount_in_subunits,
2795
            EE_Config::instance()->currency->code
2796
        );
2797
        $this->set($money_field_name, $money);
2798
    }
2799
2800
2801
    /**
2802
     * Checks this class has implemented UsesMoneyInterface
2803
     * @param string $function
2804
     * @throws DomainException
2805
     * @throws EE_Error
2806
     * @since $VID:$
2807
     */
2808
    private function verifyUsesMoney($function)
2809
    {
2810
        if (! $this instanceof UsesMoneyInterface) {
2811
            throw new DomainException(
2812
                sprintf(
2813
                    esc_html__(
2814
                        '%1$s does not use an %2$s object for representing money values, therefore the %3$s method can not be called.',
2815
                        'event_espresso'
2816
                    ),
2817
                    $this->name(),
2818
                    'EventEspresso\core\domain\values\currency\Money',
2819
                    "{$function}()"
2820
                )
2821
            );
2822
        }
2823
    }
2824
2825
2826
2827
    /**
2828
     * Because some other plugins, like Advanced Cron Manager, expect all objects to have this method
2829
     * (probably a bad assumption they have made, oh well)
2830
     *
2831
     * @return string
2832
     */
2833
    public function __toString()
2834
    {
2835
        try {
2836
            return sprintf('%s (%s)', $this->name(), $this->ID());
2837
        } catch (Exception $e) {
2838
            EE_Error::add_error($e->getMessage(), __FILE__, __FUNCTION__, __LINE__);
2839
            return '';
2840
        }
2841
    }
2842
2843
2844
2845
    /**
2846
     * Clear related model objects if they're already in the DB, because otherwise when we
2847
     * UN-serialize this model object we'll need to be careful to add them to the entity map.
2848
     * This means if we have made changes to those related model objects, and want to unserialize
2849
     * the this model object on a subsequent request, changes to those related model objects will be lost.
2850
     * Instead, those related model objects should be directly serialized and stored.
2851
     * Eg, the following won't work:
2852
     * $reg = EEM_Registration::instance()->get_one_by_ID( 123 );
2853
     * $att = $reg->attendee();
2854
     * $att->set( 'ATT_fname', 'Dirk' );
2855
     * update_option( 'my_option', serialize( $reg ) );
2856
     * //END REQUEST
2857
     * //START NEXT REQUEST
2858
     * $reg = get_option( 'my_option' );
2859
     * $reg->attendee()->save();
2860
     * And would need to be replace with:
2861
     * $reg = EEM_Registration::instance()->get_one_by_ID( 123 );
2862
     * $att = $reg->attendee();
2863
     * $att->set( 'ATT_fname', 'Dirk' );
2864
     * update_option( 'my_option', serialize( $reg ) );
2865
     * //END REQUEST
2866
     * //START NEXT REQUEST
2867
     * $att = get_option( 'my_option' );
2868
     * $att->save();
2869
     *
2870
     * @return array
2871
     * @throws EE_Error
2872
     */
2873
    public function __sleep()
2874
    {
2875
        $model = $this->get_model();
2876
        foreach ($model->relation_settings() as $relation_name => $relation_obj) {
2877
            if ($relation_obj instanceof EE_Belongs_To_Relation) {
2878
                $classname = 'EE_' . $model->get_this_model_name();
2879
                if (
2880
                    $this->get_one_from_cache($relation_name) instanceof $classname
2881
                    && $this->get_one_from_cache($relation_name)->ID()
2882
                ) {
2883
                    $this->clear_cache($relation_name, $this->get_one_from_cache($relation_name)->ID());
2884
                }
2885
            }
2886
        }
2887
        $this->_props_n_values_provided_in_constructor = array();
2888
        $properties_to_serialize = get_object_vars($this);
2889
        //don't serialize the model. It's big and that risks recursion
2890
        unset(
2891
            $properties_to_serialize['_model'],
2892
            $properties_to_serialize['money_factory']
2893
        );
2894
        return array_keys($properties_to_serialize);
2895
    }
2896
2897
2898
    /**
2899
     * restore _props_n_values_provided_in_constructor
2900
     * PLZ NOTE: this will reset the array to whatever fields values were present prior to serialization,
2901
     * and therefore should NOT be used to determine if state change has occurred since initial construction.
2902
     * At best, you would only be able to detect if state change has occurred during THIS request.
2903
     * @throws InvalidInterfaceException
2904
     * @throws InvalidDataTypeException
2905
     */
2906
    public function __wakeup()
2907
    {
2908
        $this->_props_n_values_provided_in_constructor = $this->_fields;
2909
        if( $this instanceof UsesMoneyInterface) {
2910
            $this->money_factory = LoaderFactory::getLoader()->getShared('EventEspresso\core\services\currency\MoneyFactory');
2911
        }
2912
    }
2913
2914
2915
2916
}
2917
2918
2919