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

PreviewEventDeletion::createModelObjNodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace EventEspressoBatchRequest\JobHandlers;
4
5
use EE_Base_Class;
6
use EE_Error;
7
use EEM_Event;
8
use EEM_Price;
9
use EEM_Registration;
10
use EEM_Ticket;
11
use EEM_Transaction;
12
use EETests\bootstrap\CoreLoader;
13
use EventEspresso\core\exceptions\InvalidClassException;
14
use EventEspresso\core\exceptions\InvalidDataTypeException;
15
use EventEspresso\core\exceptions\InvalidInterfaceException;
16
use EventEspresso\core\services\loaders\LoaderFactory;
17
use EventEspresso\core\services\orm\tree_traversal\ModelObjNode;
18
use EventEspresso\core\services\orm\tree_traversal\NodeGroupDao;
19
use EventEspressoBatchRequest\Helpers\BatchRequestException;
20
use EventEspressoBatchRequest\Helpers\JobParameters;
21
use EventEspressoBatchRequest\Helpers\JobStepResponse;
22
use EventEspressoBatchRequest\JobHandlerBaseClasses\JobHandler;
23
use InvalidArgumentException;
24
use ReflectionException;
25
26
/**
27
 * Class EventDeletion
28
 *
29
 * Given a list of event IDs, identified all the dependent model objects that would need to be deleted in order to not
30
 * leave any orphaned data.
31
 *
32
 * @package     Event Espresso
33
 * @author         Mike Nelson
34
 * @since         $VID:$
35
 *
36
 */
37
class PreviewEventDeletion extends JobHandler
38
{
39
40
    /**
41
     * @var NodeGroupDao
42
     */
43
    protected $model_obj_node_group_persister;
44
45
    public function __construct(NodeGroupDao $model_obj_node_group_persister)
46
    {
47
        $this->model_obj_node_group_persister = $model_obj_node_group_persister;
48
    }
49
50
    // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
51
52
    /**
53
     *
54
     * @param JobParameters $job_parameters
55
     * @return JobStepResponse
56
     * @throws EE_Error
57
     * @throws InvalidDataTypeException
58
     * @throws InvalidInterfaceException
59
     * @throws InvalidArgumentException
60
     * @throws ReflectionException
61
     */
62
    public function create_job(JobParameters $job_parameters)
63
    {
64
        // Set the "root" model objects we will want to delete (record their ID and model)
65
        $event_ids = $job_parameters->request_datum('EVT_IDs', array());
66
        // Find all the root nodes to delete (this isn't just events, because there's other data, like related tickets,
67
        // prices, message templates, etc, whose model definition doesn't make them dependent on events. But,
68
        // we have no UI to access them independent of events, so they may as well get deleted too.)
69
        $roots = [];
70
        foreach ($event_ids as $event_id) {
0 ignored issues
show
Bug introduced by
The expression $event_ids of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
71
            $roots[] = new ModelObjNode(
72
                $event_id,
73
                EEM_Event::instance()
74
            );
75
            // Also, we want to delete their related, non-global, tickets, prices and message templates
76
            $related_non_global_tickets = EEM_Ticket::instance()->get_all_deleted_and_undeleted(
77
                [
78
                    [
79
                        'TKT_is_default' => false,
80
                        'Datetime.EVT_ID' => $event_id
81
                    ]
82
                ]
83
            );
84
            foreach ($related_non_global_tickets as $ticket) {
85
                $roots[] = new ModelObjNode(
86
                    $ticket->ID(),
87
                    $ticket->get_model(),
0 ignored issues
show
Bug introduced by
It seems like $ticket->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...
88
                    ['Registration']
89
                );
90
            }
91
            $related_non_global_prices = EEM_Price::instance()->get_all_deleted_and_undeleted(
92
                [
93
                    [
94
                        'PRC_is_default' => false,
95
                        'Ticket.Datetime.EVT_ID' => $event_id
96
                    ]
97
                ]
98
            );
99
            foreach ($related_non_global_prices as $price) {
100
                $roots[] = new ModelObjNode(
101
                    $price->ID(),
102
                    $price->get_model()
0 ignored issues
show
Bug introduced by
It seems like $price->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...
103
                );
104
            }
105
        }
106
        $transactions_ids = $this->getTransactionsToDelete($event_ids);
107
        foreach ($transactions_ids as $transaction_id) {
108
            $roots[] = new ModelObjNode(
109
                $transaction_id,
110
                EEM_Transaction::instance(),
111
                ['Registration']
112
            );
113
        }
114
        $job_parameters->add_extra_data('roots', $roots);
115
        // Set an estimate of how long this will take (we're discovering as we go, so it seems impossible to give
116
        // an accurate count.)
117
        $estimated_work_per_model_obj = 10;
118
        $count_regs = EEM_Registration::instance()->count(
119
            [
120
                [
121
                    'EVT_ID' => ['IN', $event_ids]
122
                ]
123
            ]
124
        );
125
        $job_parameters->set_job_size((count($roots) + $count_regs) * $estimated_work_per_model_obj);
126
        return new JobStepResponse(
127
            $job_parameters,
128
            esc_html__('Generating preview of data to be deleted...', 'event_espresso')
129
        );
130
    }
131
132
    /**
133
     * @since $VID:$
134
     * @param EE_Base_Class[] $model_objs
135
     * @param array $dont_traverse_models
136
     * @return array
137
     * @throws EE_Error
138
     * @throws InvalidArgumentException
139
     * @throws InvalidDataTypeException
140
     * @throws InvalidInterfaceException
141
     * @throws ReflectionException
142
     */
143
    protected function createModelObjNodes($model_objs, array $dont_traverse_models = [])
144
    {
145
        $nodes = [];
146
        foreach ($model_objs as $model_obj) {
147
            $nodes[] = new ModelObjNode(
148
                $model_obj->ID(),
149
                $model_obj->get_model(),
0 ignored issues
show
Bug introduced by
It seems like $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...
150
                $dont_traverse_models
151
            );
152
        }
153
        return $nodes;
154
    }
155
156
    /**
157
     * Gets all the transactions related to these events that aren't related to other events. They'll be deleted too.
158
     * (Ones that are related to other events can stay around until those other events are deleted too.)
159
     * @since $VID:$
160
     * @param $event_ids
161
     * @return array of transaction IDs
162
     */
163
    protected function getTransactionsToDelete($event_ids)
164
    {
165
        if (empty($event_ids)) {
166
            return [];
167
        }
168
        global $wpdb;
169
        $event_ids = array_map(
170
            'intval',
171
            $event_ids
172
        );
173
        $imploded_sanitized_event_ids = implode(',', $event_ids);
174
        // Select transactions with registrations for the events $event_ids which also don't have registrations
175
        // for any events NOT in $event_ids.
176
        // Notice the outer query searched for transactions whose registrations ARE in $event_ids,
177
        // whereas the inner query checks if the outer query's transaction has any registrations that are
178
        // NOT IN $event_ids (ie, don't have registrations for events we're not just about to delete.)
179
        return array_map(
180
            'intval',
181
            $wpdb->get_col(
182
                "SELECT 
183
                      DISTINCT t.TXN_ID
184
                    FROM 
185
                      {$wpdb->prefix}esp_transaction t INNER JOIN 
186
                      {$wpdb->prefix}esp_registration r ON t.TXN_ID=r.TXN_ID
187
                    WHERE
188
                       r.EVT_ID IN ({$imploded_sanitized_event_ids})
189
                       AND NOT EXISTS 
190
                       (
191
                         SELECT 
192
                           t.TXN_ID
193
                         FROM 
194
                           {$wpdb->prefix}esp_transaction tsub INNER JOIN 
195
                           {$wpdb->prefix}esp_registration rsub ON tsub.TXN_ID=rsub.TXN_ID
196
                         WHERE
197
                           tsub.TXN_ID=t.TXN_ID AND
198
                           rsub.EVT_ID NOT IN ({$imploded_sanitized_event_ids})
199
                       )"
200
            )
201
        );
202
    }
203
204
    /**
205
     * Performs another step of the job
206
     * @param JobParameters $job_parameters
207
     * @param int $batch_size
208
     * @return JobStepResponse
209
     * @throws BatchRequestException
210
     */
211
    public function continue_job(JobParameters $job_parameters, $batch_size = 50)
212
    {
213
        // Serializing and unserializing is what really makes this drag on (eg on localhost, the ajax requests took
214
        // about 4 seconds when the batch size was 250, but 3 seconds when the batch size was 50. So like
215
        // 50% of the request is just serializing and unserializing.) So, make the batches much bigger.
216
        $batch_size *= 3;
217
        $units_processed = 0;
218
        foreach ($job_parameters->extra_datum('roots', array()) as $root_node) {
0 ignored issues
show
Bug introduced by
The expression $job_parameters->extra_datum('roots', array()) of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
219
            if ($units_processed >= $batch_size) {
220
                break;
221
            }
222
            if (!$root_node instanceof ModelObjNode) {
223
                throw new InvalidClassException('ModelObjNode');
224
            }
225
            if ($root_node->isComplete()) {
226
                continue;
227
            }
228
            $units_processed += $root_node->visit($batch_size - $units_processed);
229
        }
230
        $job_parameters->mark_processed($units_processed);
231
        // If the most-recently processed root node is complete, we must be all done because we're doing them
232
        // sequentially.
233
        if (isset($root_node) && $root_node instanceof ModelObjNode && $root_node->isComplete()) {
234
            $job_parameters->set_status(JobParameters::status_complete);
235
            // Show a full progress bar.
236
            $job_parameters->set_units_processed($job_parameters->job_size());
237
            $deletion_job_code = $job_parameters->request_datum('deletion_job_code');
238
            $this->model_obj_node_group_persister->persistModelObjNodesGroup(
239
                $job_parameters->extra_datum('roots'),
240
                $deletion_job_code
241
            );
242
            return new JobStepResponse(
243
                $job_parameters,
244
                esc_html__('Finished identifying items for deletion.', 'event_espresso'),
245
                [
246
                    'deletion_job_code' => $deletion_job_code
247
                ]
248
            );
249
        } else {
250
            // Because the job size was a guess, it may have likely been provden wrong. We don't want to show more work
251
            // done than we originally said there would be. So adjust the estimate.
252
            if (($job_parameters->units_processed() / $job_parameters->job_size()) > .8) {
253
                $job_parameters->set_job_size($job_parameters->job_size() * 2);
254
            }
255
            return new JobStepResponse(
256
                $job_parameters,
257
                sprintf(
258
                    esc_html__('Identified %d items for deletion.', 'event_espresso'),
259
                    $units_processed
260
                )
261
            );
262
        }
263
    }
264
265
    /**
266
     * Performs any clean-up logic when we know the job is completed
267
     * @param JobParameters $job_parameters
268
     * @return JobStepResponse
269
     */
270
    public function cleanup_job(JobParameters $job_parameters)
271
    {
272
        // Nothing much to do. We can't delete the option with the built tree because we may need it in a moment for the deletion
273
        return new JobStepResponse(
274
            $job_parameters,
275
            esc_html__('All done', 'event_espresso')
276
        );
277
    }
278
}
279
// End of file EventDeletion.php
280
// Location: EventEspressoBatchRequest\JobHandlers/EventDeletion.php
281