Completed
Branch CASC/base (79f9d1)
by
unknown
16:50 queued 08:50
created

RelationNode::visitAlreadyDiscoveredNodes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 2
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
namespace EventEspresso\core\services\orm\tree_traversal;
4
5
use EE_Base_Class;
6
use EE_Error;
7
use EE_Has_Many_Any_Relation;
8
use EE_Model_Relation_Base;
9
use EE_Registry;
10
use EEM_Base;
11
use EventEspresso\core\exceptions\InvalidDataTypeException;
12
use EventEspresso\core\exceptions\InvalidInterfaceException;
13
use EventEspresso\core\services\payment_methods\forms\PayPalSettingsForm;
14
use InvalidArgumentException;
15
use ReflectionException;
16
17
/**
18
 * Class RelationNode
19
 *
20
 * Wraps a model object and one of its model's relations; stores how many related model objects exist across that
21
 * relation, and eventually createsa a ModelObjNode for each of its related model objects.
22
 *
23
 * @package     Event Espresso
24
 * @author         Mike Nelson
25
 * @since         $VID:$
26
 *
27
 */
28
class RelationNode extends BaseNode
29
{
30
31
    /**
32
     * @var string|int
33
     */
34
    protected $id;
35
36
    /**
37
     * @var EEM_Base
38
     */
39
    protected $main_model;
40
41
    /**
42
     * @var int
43
     */
44
    protected $count;
45
46
    /**
47
     * @var EEM_Base
48
     */
49
    protected $related_model;
50
51
    /**
52
     * @var ModelObjNode[]
53
     */
54
    protected $nodes;
55
56
    /**
57
     * RelationNode constructor.
58
     * @param $main_model_obj_id
59
     * @param EEM_Base $main_model
60
     * @param EEM_Base $related_model
61
     * @param array $dont_traverse_models array of model names we DON'T want to traverse
62
     */
63
    public function __construct(
64
        $main_model_obj_id,
65
        EEM_Base $main_model,
66
        EEM_Base $related_model,
67
        array $dont_traverse_models = []
68
    ) {
69
        $this->id = $main_model_obj_id;
70
        $this->main_model = $main_model;
71
        $this->related_model = $related_model;
72
        $this->nodes = [];
73
        $this->dont_traverse_models = $dont_traverse_models;
74
    }
75
76
77
    /**
78
     * Here is where most of the work happens. We've counted how many related model objects exist, here we identify
79
     * them (ie, learn their IDs). But its recursive, so we'll also find their related dependent model objects etc.
80
     * @since $VID:$
81
     * @param int $model_objects_to_identify
82
     * @return int
83
     * @throws EE_Error
84
     * @throws InvalidArgumentException
85
     * @throws InvalidDataTypeException
86
     * @throws InvalidInterfaceException
87
     * @throws ReflectionException
88
     */
89
    protected function work($model_objects_to_identify)
90
    {
91
        $num_identified = $this->visitAlreadyDiscoveredNodes($this->nodes, $model_objects_to_identify);
92
        if ($num_identified < $model_objects_to_identify) {
93
            $related_model_objs = $this->related_model->get_all(
94
                [
95
                    $this->whereQueryParams(),
96
                    'limit' => [
97
                        count($this->nodes),
98
                        $model_objects_to_identify - $num_identified
99
                    ]
100
                ]
101
            );
102
            $new_item_nodes = [];
103
104
            // Add entity nodes for each of the model objects we fetched.
105
            foreach ($related_model_objs as $related_model_obj) {
106
                $entity_node = new ModelObjNode($related_model_obj->ID(), $related_model_obj->get_model(), $this->dont_traverse_models);
0 ignored issues
show
Bug introduced by
It seems like $related_model_obj->get_model() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
107
                $this->nodes[ $related_model_obj->ID() ] = $entity_node;
108
                $new_item_nodes[ $related_model_obj->ID() ] = $entity_node;
109
            }
110
            $num_identified += count($new_item_nodes);
111
            if ($num_identified < $model_objects_to_identify) {
112
                // And lastly do the work.
113
                $num_identified += $this->visitAlreadyDiscoveredNodes(
114
                    $new_item_nodes,
115
                    $model_objects_to_identify - $num_identified
116
                );
117
            }
118
        }
119
120 View Code Duplication
        if (count($this->nodes) >= $this->count && $this->allChildrenComplete()) {
121
            $this->complete = true;
122
        }
123
        return $num_identified;
124
    }
125
126
    /**
127
     * Checks if all the identified child nodes are complete or not.
128
     * @since $VID:$
129
     * @return bool
130
     */
131
    protected function allChildrenComplete()
132
    {
133
        foreach ($this->nodes as $model_obj_node) {
134
            if (! $model_obj_node->isComplete()) {
135
                return false;
136
            }
137
        }
138
        return true;
139
    }
140
141
    /**
142
     * Visits the provided nodes and keeps track of how much work was done, making sure to not go over budget.
143
     * @since $VID:$
144
     * @param ModelObjNode[] $model_obj_nodes
145
     * @param $work_budget
146
     * @return int
147
     */
148
    protected function visitAlreadyDiscoveredNodes($model_obj_nodes, $work_budget)
149
    {
150
        $work_done = 0;
151
        if (! $model_obj_nodes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $model_obj_nodes of type EventEspresso\core\servi...raversal\ModelObjNode[] 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...
152
            return 0;
153
        }
154
        foreach ($model_obj_nodes as $model_obj_node) {
155
            if ($work_done >= $work_budget) {
156
                break;
157
            }
158
            $work_done += $model_obj_node->visit($work_budget - $work_done);
159
        }
160
        return $work_done;
161
    }
162
163
    /**
164
     * Whether this item has already been initialized
165
     */
166
    protected function isDiscovered()
167
    {
168
        return $this->count !== null;
169
    }
170
171
    /**
172
     * @since $VID:$
173
     * @return boolean
174
     */
175
    public function isComplete()
176
    {
177
        if ($this->complete === null) {
178 View Code Duplication
            if (count($this->nodes) === $this->count) {
179
                $this->complete = true;
180
            } else {
181
                $this->complete = false;
182
            }
183
        }
184
        return $this->complete;
185
    }
186
187
    /**
188
     * Discovers how many related model objects exist.
189
     * @since $VID:$
190
     * @return mixed|void
191
     * @throws EE_Error
192
     * @throws InvalidArgumentException
193
     * @throws InvalidDataTypeException
194
     * @throws InvalidInterfaceException
195
     * @throws ReflectionException
196
     */
197
    protected function discover()
198
    {
199
        $this->count = $this->related_model->count([$this->whereQueryParams()]);
200
    }
201
202
    /**
203
     * @since $VID:$
204
     * @return array
205
     * @throws EE_Error
206
     * @throws InvalidDataTypeException
207
     * @throws InvalidInterfaceException
208
     * @throws InvalidArgumentException
209
     * @throws ReflectionException
210
     */
211
    protected function whereQueryParams()
212
    {
213
        $where_params =  [
214
            $this->related_model->get_foreign_key_to(
215
                $this->main_model->get_this_model_name()
216
            )->get_name() => $this->id
217
        ];
218
        try {
219
            $relation_settings = $this->main_model->related_settings_for($this->related_model->get_this_model_name());
220
        } catch (EE_Error $e) {
221
            // This will happen for has-and-belongs-to-many relations, when this node's related model is that join table
222
            // which hasn't been explicitly declared in the main model object's model's relations.
223
            $relation_settings = null;
224
        }
225
        if ($relation_settings instanceof EE_Has_Many_Any_Relation) {
226
            $where_params[ $this->related_model->get_field_containing_related_model_name()->get_name() ] = $this->main_model->get_this_model_name();
227
        }
228
        return $where_params;
229
    }
230
    /**
231
     * @since $VID:$
232
     * @return array
233
     */
234
    public function toArray()
235
    {
236
        $tree = [
237
            'count' => $this->count,
238
            'complete' => $this->isComplete(),
239
            'objs' => []
240
        ];
241
        foreach ($this->nodes as $id => $model_obj_node) {
242
            $tree['objs'][ $id ] = $model_obj_node->toArray();
243
        }
244
        return $tree;
245
    }
246
247
    /**
248
     * Gets the IDs of all the model objects to delete; indexed first by model object name.
249
     * @since $VID:$
250
     * @return array
251
     */
252
    public function getIds()
253
    {
254
        if (empty($this->nodes)) {
255
            return [];
256
        }
257
        $ids = [
258
            $this->related_model->get_this_model_name() => array_combine(
259
                array_keys($this->nodes),
260
                array_keys($this->nodes)
261
            )
262
        ];
263
        foreach ($this->nodes as $model_obj_node) {
264
            $ids = array_replace_recursive($ids, $model_obj_node->getIds());
265
        }
266
        return $ids;
267
    }
268
269
    /**
270
     * Returns the number of sub-nodes found (ie, related model objects across this relation.)
271
     * @since $VID:$
272
     * @return int
273
     */
274
    public function countSubNodes()
275
    {
276
        return count($this->nodes);
277
    }
278
279
    /**
280
     * Don't serialize the models. Just record their names on some dynamic properties.
281
     * @since $VID:$
282
     */
283
    public function __sleep()
284
    {
285
        $this->m = $this->main_model->get_this_model_name();
0 ignored issues
show
Bug introduced by
The property m does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
286
        $this->rm = $this->related_model->get_this_model_name();
0 ignored issues
show
Bug introduced by
The property rm does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
287
        return array_merge(
288
            [
289
                'm',
290
                'rm',
291
                'id',
292
                'count',
293
                'nodes',
294
            ],
295
            parent::__sleep()
296
        );
297
    }
298
299
    /**
300
     * Use the dynamic properties to instantiate the models we use.
301
     * @since $VID:$
302
     * @throws EE_Error
303
     * @throws InvalidArgumentException
304
     * @throws InvalidDataTypeException
305
     * @throws InvalidInterfaceException
306
     * @throws ReflectionException
307
     */
308
    public function __wakeup()
309
    {
310
        $this->main_model = EE_Registry::instance()->load_model($this->m);
311
        $this->related_model = EE_Registry::instance()->load_model($this->rm);
312
        parent::__wakeup();
313
    }
314
}
315
// End of file RelationNode.php
316
// Location: EventEspresso\core\services\orm\tree_traversal/RelationNode.php
317