Completed
Branch CASC/delete-transactions3 (0728c4)
by
unknown
10:54 queued 55s
created

PreviewEventDeletion   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 242
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
dl 0
loc 242
rs 10
c 0
b 0
f 0
wmc 20
lcom 1
cbo 12

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B create_job() 0 69 5
A createModelObjNodes() 0 12 2
A getTransactionsToDelete() 0 40 2
B continue_job() 0 53 9
A cleanup_job() 0 8 1
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 EventEspresso\core\exceptions\InvalidClassException;
13
use EventEspresso\core\exceptions\InvalidDataTypeException;
14
use EventEspresso\core\exceptions\InvalidInterfaceException;
15
use EventEspresso\core\services\loaders\LoaderFactory;
16
use EventEspresso\core\services\orm\tree_traversal\ModelObjNode;
17
use EventEspresso\core\services\orm\tree_traversal\NodeGroupDao;
18
use EventEspressoBatchRequest\Helpers\BatchRequestException;
19
use EventEspressoBatchRequest\Helpers\JobParameters;
20
use EventEspressoBatchRequest\Helpers\JobStepResponse;
21
use EventEspressoBatchRequest\JobHandlerBaseClasses\JobHandler;
22
use InvalidArgumentException;
23
use ReflectionException;
24
25
/**
26
 * Class EventDeletion
27
 *
28
 * Given a list of event IDs, identified all the dependent model objects that would need to be deleted in order to not
29
 * leave any orphaned data.
30
 *
31
 * @package     Event Espresso
32
 * @author         Mike Nelson
33
 * @since         $VID:$
34
 *
35
 */
36
class PreviewEventDeletion extends JobHandler
37
{
38
39
    /**
40
     * @var NodeGroupDao
41
     */
42
    protected $model_obj_node_group_persister;
43
44
    public function __construct(NodeGroupDao $model_obj_node_group_persister)
45
    {
46
        $this->model_obj_node_group_persister = $model_obj_node_group_persister;
47
    }
48
49
    // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
50
51
    /**
52
     *
53
     * @param JobParameters $job_parameters
54
     * @return JobStepResponse
55
     * @throws EE_Error
56
     * @throws InvalidDataTypeException
57
     * @throws InvalidInterfaceException
58
     * @throws InvalidArgumentException
59
     * @throws ReflectionException
60
     */
61
    public function create_job(JobParameters $job_parameters)
62
    {
63
        // Set the "root" model objects we will want to delete (record their ID and model)
64
        $event_ids = $job_parameters->request_datum('EVT_IDs', array());
65
        // Find all the root nodes to delete (this isn't just events, because there's other data, like related tickets,
66
        // prices, message templates, etc, whose model definition doesn't make them dependent on events. But,
67
        // we have no UI to access them independent of events, so they may as well get deleted too.)
68
        $roots = [];
69
        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...
70
            $roots[] = new ModelObjNode(
71
                $event_id,
72
                EEM_Event::instance()
73
            );
74
            // Also, we want to delete their related, non-global, tickets, prices and message templates
75
            $related_non_global_tickets = EEM_Ticket::instance()->get_all_deleted_and_undeleted(
76
                [
77
                    [
78
                        'TKT_is_default' => false,
79
                        'Datetime.EVT_ID' => $event_id
80
                    ]
81
                ]
82
            );
83
            foreach ($related_non_global_tickets as $ticket) {
84
                $roots[] = new ModelObjNode(
85
                    $ticket->ID(),
86
                    $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...
87
                    ['Registration']
88
                );
89
            }
90
            $related_non_global_prices = EEM_Price::instance()->get_all_deleted_and_undeleted(
91
                [
92
                    [
93
                        'PRC_is_default' => false,
94
                        'Ticket.Datetime.EVT_ID' => $event_id
95
                    ]
96
                ]
97
            );
98
            foreach ($related_non_global_prices as $price) {
99
                $roots[] = new ModelObjNode(
100
                    $price->ID(),
101
                    $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...
102
                );
103
            }
104
        }
105
        $transactions_ids = $this->getTransactionsToDelete($event_ids);
106
        foreach ($transactions_ids as $transaction_id) {
107
            $roots[] = new ModelObjNode(
108
                $transaction_id,
109
                EEM_Transaction::instance(),
110
                ['Registration']
111
            );
112
        }
113
        $job_parameters->add_extra_data('roots', $roots);
114
        // Set an estimate of how long this will take (we're discovering as we go, so it seems impossible to give
115
        // an accurate count.)
116
        $estimated_work_per_model_obj = 10;
117
        $count_regs = EEM_Registration::instance()->count(
118
            [
119
                [
120
                    'EVT_ID' => ['IN', $event_ids]
121
                ]
122
            ]
123
        );
124
        $job_parameters->set_job_size((count($roots) + $count_regs) * $estimated_work_per_model_obj);
125
        return new JobStepResponse(
126
            $job_parameters,
127
            esc_html__('Generating preview of data to be deleted...', 'event_espresso')
128
        );
129
    }
130
131
    /**
132
     * @since $VID:$
133
     * @param EE_Base_Class[] $model_objs
134
     * @param array $dont_traverse_models
135
     * @return array
136
     * @throws EE_Error
137
     * @throws InvalidArgumentException
138
     * @throws InvalidDataTypeException
139
     * @throws InvalidInterfaceException
140
     * @throws ReflectionException
141
     */
142
    protected function createModelObjNodes($model_objs, array $dont_traverse_models = [])
143
    {
144
        $nodes = [];
145
        foreach ($model_objs as $model_obj) {
146
            $nodes[] = new ModelObjNode(
147
                $model_obj->ID(),
148
                $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...
149
                $dont_traverse_models
150
            );
151
        }
152
        return $nodes;
153
    }
154
155
    /**
156
     * Gets all the transactions related to these events that aren't related to other events. They'll be deleted too.
157
     * (Ones that are related to other events can stay around until those other events are deleted too.)
158
     * @since $VID:$
159
     * @param $event_ids
160
     * @return array of transaction IDs
161
     */
162
    protected function getTransactionsToDelete($event_ids)
163
    {
164
        if (empty($event_ids)) {
165
            return [];
166
        }
167
        global $wpdb;
168
        $event_ids = array_map(
169
            'intval',
170
            $event_ids
171
        );
172
        $imploded_sanitized_event_ids = implode(',', $event_ids);
173
        // Select transactions with registrations for the events $event_ids which also don't have registrations
174
        // for any events NOT in $event_ids.
175
        // Notice the outer query searched for transactions whose registrations ARE in $event_ids,
176
        // whereas the inner query checks if the outer query's transaction has any registrations that are
177
        // NOT IN $event_ids (ie, don't have registrations for events we're not just about to delete.)
178
        return array_map(
179
            'intval',
180
            $wpdb->get_col(
181
                "SELECT 
182
                      DISTINCT t.TXN_ID
183
                    FROM 
184
                      {$wpdb->prefix}esp_transaction t INNER JOIN 
185
                      {$wpdb->prefix}esp_registration r ON t.TXN_ID=r.TXN_ID
186
                    WHERE
187
                       r.EVT_ID IN ({$imploded_sanitized_event_ids})
188
                       AND NOT EXISTS 
189
                       (
190
                         SELECT 
191
                           t.TXN_ID
192
                         FROM 
193
                           {$wpdb->prefix}esp_transaction tsub INNER JOIN 
194
                           {$wpdb->prefix}esp_registration rsub ON tsub.TXN_ID=rsub.TXN_ID
195
                         WHERE
196
                           tsub.TXN_ID=t.TXN_ID AND
197
                           rsub.EVT_ID NOT IN ({$imploded_sanitized_event_ids})
198
                       )"
199
            )
200
        );
201
    }
202
203
    /**
204
     * Performs another step of the job
205
     * @param JobParameters $job_parameters
206
     * @param int $batch_size
207
     * @return JobStepResponse
208
     * @throws BatchRequestException
209
     */
210
    public function continue_job(JobParameters $job_parameters, $batch_size = 50)
211
    {
212
        // Serializing and unserializing is what really makes this drag on (eg on localhost, the ajax requests took
213
        // about 4 seconds when the batch size was 250, but 3 seconds when the batch size was 50. So like
214
        // 50% of the request is just serializing and unserializing.) So, make the batches much bigger.
215
        $batch_size *= 3;
216
        $units_processed = 0;
217
        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...
218
            if ($units_processed >= $batch_size) {
219
                break;
220
            }
221
            if (!$root_node instanceof ModelObjNode) {
222
                throw new InvalidClassException('ModelObjNode');
223
            }
224
            if ($root_node->isComplete()) {
225
                continue;
226
            }
227
            $units_processed += $root_node->visit($batch_size - $units_processed);
228
        }
229
        $job_parameters->mark_processed($units_processed);
230
        // If the most-recently processed root node is complete, we must be all done because we're doing them
231
        // sequentially.
232
        if (isset($root_node) && $root_node instanceof ModelObjNode && $root_node->isComplete()) {
233
            $job_parameters->set_status(JobParameters::status_complete);
234
            // Show a full progress bar.
235
            $job_parameters->set_units_processed($job_parameters->job_size());
236
            $deletion_job_code = $job_parameters->request_datum('deletion_job_code');
237
            $this->model_obj_node_group_persister->persistModelObjNodesGroup(
238
                $job_parameters->extra_datum('roots'),
239
                $deletion_job_code
240
            );
241
            return new JobStepResponse(
242
                $job_parameters,
243
                esc_html__('Finished identifying items for deletion.', 'event_espresso'),
244
                [
245
                    'deletion_job_code' => $deletion_job_code
246
                ]
247
            );
248
        } else {
249
            // Because the job size was a guess, it may have likely been provden wrong. We don't want to show more work
250
            // done than we originally said there would be. So adjust the estimate.
251
            if (($job_parameters->units_processed() / $job_parameters->job_size()) > .8) {
252
                $job_parameters->set_job_size($job_parameters->job_size() * 2);
253
            }
254
            return new JobStepResponse(
255
                $job_parameters,
256
                sprintf(
257
                    esc_html__('Identified %d items for deletion.', 'event_espresso'),
258
                    $units_processed
259
                )
260
            );
261
        }
262
    }
263
264
    /**
265
     * Performs any clean-up logic when we know the job is completed
266
     * @param JobParameters $job_parameters
267
     * @return JobStepResponse
268
     */
269
    public function cleanup_job(JobParameters $job_parameters)
270
    {
271
        // 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
272
        return new JobStepResponse(
273
            $job_parameters,
274
            esc_html__('All done', 'event_espresso')
275
        );
276
    }
277
}
278
// End of file EventDeletion.php
279
// Location: EventEspressoBatchRequest\JobHandlers/EventDeletion.php
280