Completed
Branch FET-10304-welcome-to-vue (644299)
by
unknown
94:54 queued 83:07
created

EE_Model_Relation_Base::getSchemaProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
use EventEspresso\core\entities\interfaces\HasSchemaInterface;
3
4
defined('EVENT_ESPRESSO_VERSION') || exit;
5
6
/**
7
 * Class EE_Model_Relation_Base
8
 * Model Relation classes are for defining relationships between models, and facilitating JOINs
9
 * between them during querying. They require knowing at least the model names of the two models
10
 * they join, and require each to have proper Private and Foreign key fields setup. (HABTM are different)
11
 * Once those two models are setup correctly, and the relation object has the names of each, it can
12
 * magically figure out what tables must be joined on what fields during querying.
13
 *
14
 * @package       Event Espresso
15
 * @subpackage    core
16
 * @author        Michael Nelson
17
 */
18
abstract class EE_Model_Relation_Base implements HasSchemaInterface
19
{
20
    /**
21
     * The model name of which this relation is a component (ie, the model that called new EE_Model_Relation_Base)
22
     *
23
     * @var string eg Event, Question_Group, Registration
24
     */
25
    private $_this_model_name;
26
    /**
27
     * The model name pointed to by this relation (ie, the model we want to establish a relationship to)
28
     *
29
     * @var string eg Event, Question_Group, Registration
30
     */
31
    private $_other_model_name;
32
33
    /**
34
     * this is typically used when calling the relation models to make sure they inherit any set timezone from the
35
     * initiating model.
36
     *
37
     * @var string
38
     */
39
    protected $_timezone;
40
41
    /**
42
     * If you try to delete "this_model", and there are related "other_models",
43
     * and this isn't null, then abandon the deletion and add this warning.
44
     * This effectively makes it impossible to delete "this_model" while there are
45
     * related "other_models" along this relation.
46
     *
47
     * @var string (internationalized)
48
     */
49
    protected $_blocking_delete_error_message;
50
51
    protected $_blocking_delete = false;
52
53
    /**
54
     * Object representing the relationship between two models. This knows how to join the models,
55
     * get related models across the relation, and add-and-remove the relationships.
56
     *
57
     * @param boolean $block_deletes                 if there are related models across this relation, block (prevent
58
     *                                               and add an error) the deletion of this model
59
     * @param string  $blocking_delete_error_message a customized error message on blocking deletes instead of the
60
     *                                               default
61
     */
62
    public function __construct($block_deletes, $blocking_delete_error_message)
63
    {
64
        $this->_blocking_delete               = $block_deletes;
65
        $this->_blocking_delete_error_message = $blocking_delete_error_message;
66
    }
67
68
69
    /**
70
     * @param $this_model_name
71
     * @param $other_model_name
72
     * @throws EE_Error
73
     */
74
    public function _construct_finalize_set_models($this_model_name, $other_model_name)
75
    {
76
        $this->_this_model_name  = $this_model_name;
77
        $this->_other_model_name = $other_model_name;
78
        if (is_string($this->_blocking_delete)) {
79
            throw new EE_Error(sprintf(__("When instantiating the relation of type %s from %s to %s, the \$block_deletes argument should be a boolean, not a string (%s)",
80
                "event_espresso"),
81
                get_class($this), $this_model_name, $other_model_name, $this->_blocking_delete));
82
        }
83
    }
84
85
86
    /**
87
     * Gets the model where this relation is defined.
88
     *
89
     * @return EEM_Base
90
     */
91
    public function get_this_model()
92
    {
93
        return $this->_get_model($this->_this_model_name);
94
    }
95
96
97
    /**
98
     * Gets the model which this relation establishes the relation TO (ie,
99
     * this relation object was defined on get_this_model(), get_other_model() is the other one)
100
     *
101
     * @return EEM_Base
102
     */
103
    public function get_other_model()
104
    {
105
        return $this->_get_model($this->_other_model_name);
106
    }
107
108
109
    /**
110
     * Internally used by get_this_model() and get_other_model()
111
     *
112
     * @param string $model_name like Event, Question_Group, etc. omit the EEM_
113
     * @return EEM_Base
114
     */
115
    protected function _get_model($model_name)
116
    {
117
        $modelInstance = EE_Registry::instance()->load_model($model_name);
118
        $modelInstance->set_timezone($this->_timezone);
119
        return $modelInstance;
120
    }
121
122
123
    /**
124
     * entirely possible that relations may be called from a model and we need to make sure those relations have their
125
     * timezone set correctly.
126
     *
127
     * @param string $timezone timezone to set.
128
     */
129
    public function set_timezone($timezone)
130
    {
131
        if ($timezone !== null) {
132
            $this->_timezone = $timezone;
133
        }
134
    }
135
136
137
    /**
138
     * @param        $other_table
139
     * @param        $other_table_alias
140
     * @param        $other_table_column
141
     * @param        $this_table_alias
142
     * @param        $this_table_join_column
143
     * @param string $extra_join_sql
144
     * @return string
145
     */
146
    protected function _left_join(
147
        $other_table,
148
        $other_table_alias,
149
        $other_table_column,
150
        $this_table_alias,
151
        $this_table_join_column,
152
        $extra_join_sql = ''
153
    ) {
154
        return " LEFT JOIN " . $other_table . " AS " . $other_table_alias . " ON " . $other_table_alias . "." . $other_table_column . "=" . $this_table_alias . "." . $this_table_join_column . ($extra_join_sql ? " AND $extra_join_sql" : '');
155
    }
156
157
158
    /**
159
     * Gets all the model objects of type of other model related to $model_object,
160
     * according to this relation. This is the same code for EE_HABTM_Relation and EE_Has_Many_Relation.
161
     * For both of those child classes, $model_object must be saved so that it has an ID before querying,
162
     * otherwise an error will be thrown. Note: by default we disable default_where_conditions
163
     * EE_Belongs_To_Relation doesn't need to be saved before querying.
164
     *
165
     * @param EE_Base_Class|int $model_object_or_id                      or the primary key of this model
166
     * @param array             $query_params                            like EEM_Base::get_all's $query_params
167
     * @param boolean           $values_already_prepared_by_model_object @deprecated since 4.8.1
168
     * @return EE_Base_Class[]
169
     * @throws \EE_Error
170
     */
171
    public function get_all_related(
172
        $model_object_or_id,
173
        $query_params = array(),
174
        $values_already_prepared_by_model_object = false
175
    ) {
176
        if ($values_already_prepared_by_model_object !== false) {
177
            EE_Error::doing_it_wrong('EE_Model_Relation_Base::get_all_related',
178
                __('The argument $values_already_prepared_by_model_object is no longer used.', 'event_espresso'),
179
                '4.8.1');
180
        }
181
        $query_params                                      = $this->_disable_default_where_conditions_on_query_param($query_params);
0 ignored issues
show
Documentation introduced by
$query_params is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
182
        $query_param_where_this_model_pk                   = $this->get_this_model()->get_this_model_name()
183
                                                             . "."
184
                                                             . $this->get_this_model()->get_primary_key_field()->get_name();
185
        $model_object_id                                   = $this->_get_model_object_id($model_object_or_id);
186
        $query_params[0][$query_param_where_this_model_pk] = $model_object_id;
187
        return $this->get_other_model()->get_all($query_params);
0 ignored issues
show
Documentation introduced by
$query_params is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
188
    }
189
190
191
    /**
192
     * Alters the $query_params to disable default where conditions, unless otherwise specified
193
     *
194
     * @param string $query_params
195
     * @return array
196
     */
197
    protected function _disable_default_where_conditions_on_query_param($query_params)
198
    {
199
        if (! isset($query_params['default_where_conditions'])) {
200
            $query_params['default_where_conditions'] = 'none';
201
        }
202
        return $query_params;
203
    }
204
205
206
    /**
207
     * Deletes the related model objects which meet the query parameters. If no
208
     * parameters are specified, then all related model objects will be deleted.
209
     * Note: If the related model is extends EEM_Soft_Delete_Base, then the related
210
     * model objects will only be soft-deleted.
211
     *
212
     * @param EE_Base_Class|int|string $model_object_or_id
213
     * @param array                    $query_params
214
     * @return int of how many related models got deleted
215
     * @throws \EE_Error
216
     */
217
    public function delete_all_related($model_object_or_id, $query_params = array())
218
    {
219
        //for each thing we would delete,
220
        $related_model_objects = $this->get_all_related($model_object_or_id, $query_params);
221
        //determine if it's blocked by anything else before it can be deleted
222
        $deleted_count = 0;
223
        foreach ($related_model_objects as $related_model_object) {
224
            $delete_is_blocked = $this->get_other_model()->delete_is_blocked_by_related_models($related_model_object,
225
                $model_object_or_id);
226
            /* @var $model_object_or_id EE_Base_Class */
227
            if (! $delete_is_blocked) {
228
                $this->remove_relation_to($model_object_or_id, $related_model_object);
229
                $related_model_object->delete();
230
                $deleted_count++;
231
            }
232
        }
233
        return $deleted_count;
234
    }
235
236
237
    /**
238
     * Deletes the related model objects which meet the query parameters. If no
239
     * parameters are specified, then all related model objects will be deleted.
240
     * Note: If the related model is extends EEM_Soft_Delete_Base, then the related
241
     * model objects will only be soft-deleted.
242
     *
243
     * @param EE_Base_Class|int|string $model_object_or_id
244
     * @param array                    $query_params
245
     * @return int of how many related models got deleted
246
     * @throws \EE_Error
247
     */
248
    public function delete_related_permanently($model_object_or_id, $query_params = array())
249
    {
250
        //for each thing we would delete,
251
        $related_model_objects = $this->get_all_related($model_object_or_id, $query_params);
252
        //determine if it's blocked by anything else before it can be deleted
253
        $deleted_count = 0;
254
        foreach ($related_model_objects as $related_model_object) {
255
            $delete_is_blocked = $this->get_other_model()->delete_is_blocked_by_related_models($related_model_object,
256
                $model_object_or_id);
257
            /* @var $model_object_or_id EE_Base_Class */
258
            if ($related_model_object instanceof EE_Soft_Delete_Base_Class) {
259
                $this->remove_relation_to($model_object_or_id, $related_model_object);
260
                $deleted_count++;
261
                if (! $delete_is_blocked) {
262
                    $related_model_object->delete_permanently();
263
                } else {
264
                    //delete is blocked
265
                    //brent and darren, in this case, wanted to just soft delete it then
266
                    $related_model_object->delete();
267
                }
268
            } else {
269
                //its not a soft-deletable thing anyways. do the normal logic.
270
                if (! $delete_is_blocked) {
271
                    $this->remove_relation_to($model_object_or_id, $related_model_object);
272
                    $related_model_object->delete();
273
                    $deleted_count++;
274
                }
275
            }
276
        }
277
        return $deleted_count;
278
    }
279
280
281
    /**
282
     * this just returns a model_object_id for incoming item that could be an object or id.
283
     *
284
     * @param  EE_Base_Class|int $model_object_or_id model object or the primary key of this model
285
     * @throws EE_Error
286
     * @return int
287
     */
288
    protected function _get_model_object_id($model_object_or_id)
289
    {
290
        $model_object_id = $model_object_or_id;
291
        if ($model_object_or_id instanceof EE_Base_Class) {
292
            $model_object_id = $model_object_or_id->ID();
293
        }
294 View Code Duplication
        if (! $model_object_id) {
295
            throw new EE_Error(sprintf(__("Sorry, we cant get the related %s model objects to %s model object before it has an ID. You can solve that by just saving it before trying to get its related model objects",
296
                "event_espresso"), $this->get_other_model()->get_this_model_name(),
297
                $this->get_this_model()->get_this_model_name()));
298
        }
299
        return $model_object_id;
300
    }
301
302
303
    /**
304
     * Gets the SQL string for performing the join between this model and the other model.
305
     *
306
     * @param string $model_relation_chain like 'Event.Event_Venue.Venue'
307
     * @return string of SQL, eg "LEFT JOIN table_name AS table_alias ON this_model_primary_table.pk =
308
     *                other_model_primary_table.fk" etc
309
     */
310
    abstract public function get_join_statement($model_relation_chain);
311
312
313
    /**
314
     * Adds a relationships between the two model objects provided. Each type of relationship handles this differently
315
     * (EE_Belongs_To is a slight exception, it should more accurately be called set_relation_to(...), as this
316
     * relationship only allows this model to be related to a single other model of this type)
317
     *
318
     * @param       $this_obj_or_id
319
     * @param       $other_obj_or_id
320
     * @param array $extra_join_model_fields_n_values
321
     * @return \EE_Base_Class the EE_Base_Class which was added as a relation. (Convenient if you only pass an ID for
322
     *                        $other_obj_or_id)
323
     */
324
    abstract public function add_relation_to(
325
        $this_obj_or_id,
326
        $other_obj_or_id,
327
        $extra_join_model_fields_n_values = array()
328
    );
329
330
331
    /**
332
     * Similar to 'add_relation_to(...)', performs the opposite action of removing the relationship between the two
333
     * model objects
334
     *
335
     * @param       $this_obj_or_id
336
     * @param       $other_obj_or_id
337
     * @param array $where_query
338
     * @return bool
339
     */
340
    abstract public function remove_relation_to($this_obj_or_id, $other_obj_or_id, $where_query = array());
341
342
343
    /**
344
     * Removes ALL relation instances for this relation obj
345
     *
346
     * @param EE_Base_Class|int $this_obj_or_id
347
     * @param array             $where_query_param like EEM_Base::get_all's $query_params[0] (where conditions)
348
     * @return EE_Base_Class[]
349
     * @throws \EE_Error
350
     */
351
    public function remove_relations($this_obj_or_id, $where_query_param = array())
352
    {
353
        $related_things = $this->get_all_related($this_obj_or_id, array($where_query_param));
354
        $objs_removed   = array();
355
        foreach ($related_things as $related_thing) {
356
            $objs_removed[] = $this->remove_relation_to($this_obj_or_id, $related_thing);
357
        }
358
        return $objs_removed;
359
    }
360
361
362
    /**
363
     * If you aren't allowed to delete this model when there are related models across this
364
     * relation object, return true. Otherwise, if you can delete this model even though
365
     * related objects exist, returns false.
366
     *
367
     * @return boolean
368
     */
369
    public function block_delete_if_related_models_exist()
370
    {
371
        return $this->_blocking_delete;
372
    }
373
374
375
    /**
376
     * Gets the error message to show
377
     *
378
     * @return string
379
     */
380
    public function get_deletion_error_message()
381
    {
382
        if ($this->_blocking_delete_error_message) {
383
            return $this->_blocking_delete_error_message;
384
        } else {
385
//			return sprintf(__('Cannot delete %1$s when there are related %2$s', "event_espresso"),$this->get_this_model()->item_name(2),$this->get_other_model()->item_name(2));
386
            return sprintf(
387
                __('This %1$s is currently linked to one or more %2$s records. If this %1$s is incorrect, then please remove it from all %3$s before attempting to delete it.',
388
                    "event_espresso"),
389
                $this->get_this_model()->item_name(1),
390
                $this->get_other_model()->item_name(1),
391
                $this->get_other_model()->item_name(2)
392
            );
393
        }
394
    }
395
396
    /**
397
     * Returns whatever is set as the nicename for the object.
398
     *
399
     * @return string
400
     */
401
    public function getSchemaDescription()
402
    {
403
        $description = $this instanceof EE_Belongs_To_Relation
404
            ? esc_html__('The related %1$s entity to the %2$s.', 'event_espresso')
405
            : esc_html__('The related %1$s entities to the %2$s.', 'event_espresso');
406
        return sprintf(
407
            $description,
408
            $this->get_other_model()->get_this_model_name(),
409
            $this->get_this_model()->get_this_model_name()
410
        );
411
    }
412
413
    /**
414
     * Returns whatever is set as the $_schema_type property for the object.
415
     * Note: this will automatically add 'null' to the schema if the object is_nullable()
416
     *
417
     * @return string|array
418
     */
419
    public function getSchemaType()
420
    {
421
        return $this instanceof EE_Belongs_To_Relation ? 'object' : 'array';
422
    }
423
424
    /**
425
     * This is usually present when the $_schema_type property is 'object'.  Any child classes will need to override
426
     * this method and return the properties for the schema.
427
     * The reason this is not a property on the class is because there may be filters set on the values for the property
428
     * that won't be exposed on construct.  For example enum type schemas may have the enum values filtered.
429
     *
430
     * @return array
431
     */
432
    public function getSchemaProperties()
433
    {
434
        return array();
435
    }
436
437
    /**
438
     * If a child class has enum values, they should override this method and provide a simple array
439
     * of the enum values.
440
     * The reason this is not a property on the class is because there may be filterable enum values that
441
     * are set on the instantiated object that could be filtered after construct.
442
     *
443
     * @return array
444
     */
445
    public function getSchemaEnum()
446
    {
447
        return array();
448
    }
449
450
    /**
451
     * This returns the value of the $_schema_format property on the object.
452
     *
453
     * @return string
454
     */
455
    public function getSchemaFormat()
456
    {
457
        return array();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type declared by the interface EventEspresso\core\entit...erface::getSchemaFormat of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
458
    }
459
460
    /**
461
     * This returns the value of the $_schema_readonly property on the object.
462
     *
463
     * @return bool
464
     */
465
    public function getSchemaReadonly()
466
    {
467
        return true;
468
    }
469
470
    /**
471
     * This returns elements used to represent this field in the json schema.
472
     *
473
     * @link http://json-schema.org/
474
     * @return array
475
     */
476
    public function getSchema()
477
    {
478
        $schema = array(
479
            'description' => $this->getSchemaDescription(),
480
            'type' => $this->getSchemaType(),
481
            'relation' => true,
482
            'relation_type' => get_class($this),
483
            'readonly' => $this->getSchemaReadonly()
484
        );
485
486
        if ($this instanceof EE_HABTM_Relation) {
487
            $schema['joining_model_name'] = $this->get_join_model()->get_this_model_name();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class EE_Model_Relation_Base as the method get_join_model() does only exist in the following sub-classes of EE_Model_Relation_Base: EE_HABTM_Any_Relation, EE_HABTM_Relation. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
488
        }
489
490
        if ($this->getSchemaType() === 'array') {
491
            $schema['items'] = array(
492
                'type' => 'object'
493
            );
494
        }
495
496
        return $schema;
497
    }
498
}
499