Completed
Branch BUG-10911-php-7.2 (ef442d)
by
unknown
104:08 queued 92:42
created

DatetimeOffsetFix::doQueryForAllFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace EventEspressoBatchRequest\JobHandlers;
4
5
use DateTime;
6
use DateTimeZone;
7
use EE_Error;
8
use EE_Model_Field_Base;
9
use EE_Table_Base;
10
use EventEspresso\core\domain\entities\DbSafeDateTime;
11
use EventEspresso\core\exceptions\InvalidDataTypeException;
12
use EventEspresso\core\exceptions\InvalidInterfaceException;
13
use EventEspressoBatchRequest\JobHandlerBaseClasses\JobHandler;
14
use EventEspressoBatchRequest\Helpers\BatchRequestException;
15
use EventEspressoBatchRequest\Helpers\JobParameters;
16
use EventEspressoBatchRequest\Helpers\JobStepResponse;
17
use EE_Registry;
18
use EE_Datetime_Field;
19
use EEM_Base;
20
use EE_Change_Log;
21
use Exception;
22
use InvalidArgumentException;
23
24
defined('EVENT_ESPRESSO_VERSION') || exit('No direct access allowed.');
25
26
class DatetimeOffsetFix extends JobHandler
27
{
28
29
    /**
30
     * Key for the option used to track which models have been processed when doing the batches.
31
     */
32
    const MODELS_TO_PROCESS_OPTION_KEY = 'ee_models_processed_for_datetime_offset_fix';
33
34
35
    const COUNT_OF_MODELS_PROCESSED = 'ee_count_of_ee_models_processed_for_datetime_offset_fixed';
36
37
    /**
38
     * Key for the option used to track what the current offset is that will be applied when this tool is executed.
39
     */
40
    const OFFSET_TO_APPLY_OPTION_KEY = 'ee_datetime_offset_fix_offset_to_apply';
41
42
43
    const OPTION_KEY_OFFSET_RANGE_START_DATE = 'ee_datetime_offset_start_date_range';
44
45
46
    const OPTION_KEY_OFFSET_RANGE_END_DATE = 'ee_datetime_offset_end_date_range';
47
48
49
    /**
50
     * String labelling the datetime offset fix type for change-log entries.
51
     */
52
    const DATETIME_OFFSET_FIX_CHANGELOG_TYPE = 'datetime_offset_fix';
53
54
55
    /**
56
     * String labelling a datetime offset fix error for change-log entries.
57
     */
58
    const DATETIME_OFFSET_FIX_CHANGELOG_ERROR_TYPE = 'datetime_offset_fix_error';
59
60
    /**
61
     * @var EEM_Base[]
62
     */
63
    protected $models_with_datetime_fields = array();
64
65
66
    /**
67
     * Performs any necessary setup for starting the job. This is also a good
68
     * place to setup the $job_arguments which will be used for subsequent HTTP requests
69
     * when continue_job will be called
70
     *
71
     * @param JobParameters $job_parameters
72
     * @return JobStepResponse
73
     * @throws EE_Error
74
     * @throws InvalidArgumentException
75
     * @throws InvalidDataTypeException
76
     * @throws InvalidInterfaceException
77
     */
78
    public function create_job(JobParameters $job_parameters)
79
    {
80
        $models_with_datetime_fields = $this->getModelsWithDatetimeFields();
81
        //we'll be doing each model as a batch.
82
        $job_parameters->set_job_size(count($models_with_datetime_fields));
83
        return new JobStepResponse(
84
            $job_parameters,
85
            esc_html__('Starting Datetime Offset Fix', 'event_espresso')
86
        );
87
    }
88
89
    /**
90
     * Performs another step of the job
91
     *
92
     * @param JobParameters $job_parameters
93
     * @param int           $batch_size
94
     * @return JobStepResponse
95
     * @throws EE_Error
96
     * @throws InvalidArgumentException
97
     * @throws InvalidDataTypeException
98
     * @throws InvalidInterfaceException
99
     */
100
    public function continue_job(JobParameters $job_parameters, $batch_size = 50)
101
    {
102
        $models_to_process = $this->getModelsWithDatetimeFields();
103
        //let's pop off the a model and do the query to apply the offset.
104
        $model_to_process = array_pop($models_to_process);
105
        //update our record
106
        $this->setModelsToProcess($models_to_process);
107
        $this->processModel($model_to_process);
108
        $this->updateCountOfModelsProcessed();
109
        $job_parameters->set_units_processed($this->getCountOfModelsProcessed());
110
        if (count($models_to_process) > 0) {
111
            $job_parameters->set_status(JobParameters::status_continue);
112
        } else {
113
            $job_parameters->set_status(JobParameters::status_complete);
114
        }
115
        return new JobStepResponse(
116
            $job_parameters,
117
            sprintf(
118
                esc_html__('Updated the offset for all datetime fields on the %s model.', 'event_espresso'),
119
                $model_to_process
120
            )
121
        );
122
    }
123
124
    /**
125
     * Performs any clean-up logic when we know the job is completed
126
     *
127
     * @param JobParameters $job_parameters
128
     * @return JobStepResponse
129
     * @throws BatchRequestException
130
     */
131
    public function cleanup_job(JobParameters $job_parameters)
132
    {
133
        //delete important saved options.
134
        delete_option(self::MODELS_TO_PROCESS_OPTION_KEY);
135
        delete_option(self::COUNT_OF_MODELS_PROCESSED);
136
        delete_option(self::OPTION_KEY_OFFSET_RANGE_START_DATE);
137
        delete_option(self::OPTION_KEY_OFFSET_RANGE_END_DATE);
138
        return new JobStepResponse($job_parameters, esc_html__(
139
            'Offset has been applied to all affected fields.',
140
            'event_espresso'
141
        ));
142
    }
143
144
145
    /**
146
     * Contains the logic for processing a model and applying the datetime offset to affected fields on that model.
147
     * @param string $model_class_name
148
     * @throws EE_Error
149
     */
150
    protected function processModel($model_class_name)
151
    {
152
        global $wpdb;
153
        /** @var EEM_Base $model */
154
        $model = $model_class_name::instance();
155
        $original_offset = self::getOffset();
156
        $start_date_range = self::getStartDateRange();
157
        $end_date_range = self::getEndDateRange();
158
        $sql_date_function = $original_offset > 0 ? 'DATE_ADD' : 'DATE_SUB';
159
        $offset = abs($original_offset) * 60;
160
        $date_ranges = array();
0 ignored issues
show
Unused Code introduced by
$date_ranges is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
161
        //since some affected models might have two tables, we have to get our tables and set up a query for each table.
162
        foreach ($model->get_tables() as $table) {
163
            $query = 'UPDATE ' . $table->get_table_name();
164
            $fields_affected = array();
165
            $inner_query = array();
166
            foreach ($model->_get_fields_for_table($table->get_table_alias()) as $model_field) {
167
                if ($model_field instanceof EE_Datetime_Field) {
168
                    $inner_query[$model_field->get_table_column()] = $model_field->get_table_column() . ' = '
169
                                     . $sql_date_function . '('
170
                                     . $model_field->get_table_column()
171
                                     . ", INTERVAL {$offset} MINUTE)";
172
                    $fields_affected[] = $model_field;
173
                }
174
            }
175
            if (! $fields_affected) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields_affected 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...
176
                continue;
177
            }
178
            //do we do one query per column/field or one query for all fields on the model? It all depends on whether
179
            //there is a date range applied or not.
180
            if ($start_date_range instanceof DbSafeDateTime || $end_date_range instanceof DbSafeDateTime) {
181
                $result = $this->doQueryForEachField($query, $inner_query, $start_date_range, $end_date_range);
182
            } else {
183
                $result = $this->doQueryForAllFields($query, $inner_query);
184
            }
185
186
            //record appropriate logs for the query
187
            switch (true) {
188
                case $result === false:
189
                    //record error.
190
                    $error_message = $wpdb->last_error;
191
                    //handle the edgecases where last_error might be empty.
192
                    if (! $error_message) {
193
                        $error_message = esc_html__('Unknown mysql error occured.', 'event_espresso');
194
                    }
195
                    $this->recordChangeLog($model, $original_offset, $table, $fields_affected, $error_message);
196
                    break;
197
                case is_array($result) && ! empty($result):
198
                    foreach ($result as $field_name => $error_message) {
199
                        $this->recordChangeLog($model, $original_offset, $table, array($field_name), $error_message);
200
                    }
201
                    break;
202
                default:
203
                    $this->recordChangeLog($model, $original_offset, $table, $fields_affected);
204
            }
205
        }
206
    }
207
208
209
    /**
210
     * Does the query on each $inner_query individually.
211
     *
212
     * @param string              $query
213
     * @param array               $inner_query
214
     * @param DbSafeDateTime|null $start_date_range
215
     * @param DbSafeDateTime|null $end_date_range
216
     * @return array  An array of any errors encountered and the fields they were for.
217
     */
218
    private function doQueryForEachField($query, array $inner_query, $start_date_range, $end_date_range)
219
    {
220
        global $wpdb;
221
        $errors = array();
222
        foreach ($inner_query as $field_name => $field_query) {
223
            $query_to_run = $query;
224
            $where_conditions = array();
225
            $query_to_run .= ' SET ' . $field_query;
226
            if ($start_date_range instanceof DbSafeDateTime) {
227
                $start_date = $start_date_range->format(EE_Datetime_Field::mysql_timestamp_format);
228
                $where_conditions[] = "{$field_name} > '{$start_date}'";
229
            }
230
            if ($end_date_range instanceof DbSafeDateTime) {
231
                $end_date = $end_date_range->format(EE_Datetime_Field::mysql_timestamp_format);
232
                $where_conditions[] = "{$field_name} < '{$end_date}'";
233
            }
234
            if ($where_conditions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $where_conditions 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...
235
                $query_to_run .= ' WHERE ' . implode(' AND ', $where_conditions);
236
            }
237
            $result = $wpdb->query($query_to_run);
238
            if ($result === false) {
239
                //record error.
240
                $error_message = $wpdb->last_error;
241
                //handle the edgecases where last_error might be empty.
242
                if (! $error_message) {
243
                    $error_message = esc_html__('Unknown mysql error occured.', 'event_espresso');
244
                }
245
                $errors[$field_name] = $error_message;
246
            }
247
        }
248
        return $errors;
249
    }
250
251
252
    /**
253
     * Performs the query for all fields within the inner_query
254
     *
255
     * @param string $query
256
     * @param array  $inner_query
257
     * @return false|int
258
     */
259
    private function doQueryForAllFields($query, array $inner_query)
260
    {
261
        global $wpdb;
262
        $query .= ' SET ' . implode(',', $inner_query);
263
        return $wpdb->query($query);
264
    }
265
266
267
    /**
268
     * Records a changelog entry using the given information.
269
     *
270
     * @param EEM_Base              $model
271
     * @param float                 $offset
272
     * @param EE_Table_Base         $table
273
     * @param EE_Model_Field_Base[] $model_fields_affected
274
     * @param string                $error_message   If present then there was an error so let's record that instead.
275
     * @throws EE_Error
276
     */
277
    private function recordChangeLog(
278
        EEM_Base $model,
279
        $offset,
280
        EE_Table_Base $table,
281
        $model_fields_affected,
282
        $error_message = ''
283
    ) {
284
        //setup $fields list.
285
        $fields = array();
286
        /** @var EE_Datetime_Field $model_field */
287
        foreach ($model_fields_affected as $model_field) {
288
            if (! $model_field instanceof EE_Datetime_Field) {
289
                continue;
290
            }
291
            $fields[] = $model_field->get_name();
292
        }
293
        //setup the message for the changelog entry.
294
        $message = $error_message
295
            ? sprintf(
296
                esc_html__(
297
                    'The %1$s table for the %2$s model did not have the offset of %3$f applied to its fields (%4$s), because of the following error:%5$s',
298
                    'event_espresso'
299
                ),
300
                $table->get_table_name(),
301
                $model->get_this_model_name(),
302
                $offset,
303
                implode(',', $fields),
304
                $error_message
305
            )
306
            : sprintf(
307
                esc_html__(
308
                    'The %1$s table for the %2$s model has had the offset of %3$f applied to its following fields: %4$s',
309
                    'event_espresso'
310
                ),
311
                $table->get_table_name(),
312
                $model->get_this_model_name(),
313
                $offset,
314
                implode(',', $fields)
315
            );
316
        //write to the log
317
        $changelog = EE_Change_Log::new_instance(array(
318
            'LOG_type' => $error_message
319
                ? self::DATETIME_OFFSET_FIX_CHANGELOG_ERROR_TYPE
320
                : self::DATETIME_OFFSET_FIX_CHANGELOG_TYPE,
321
            'LOG_message' => $message
322
        ));
323
        $changelog->save();
324
    }
325
326
327
    /**
328
     * Returns an array of models that have datetime fields.
329
     * This array is added to a short lived transient cache to keep having to build this list to a minimum.
330
     *
331
     * @return array an array of model class names.
332
     * @throws EE_Error
333
     * @throws InvalidDataTypeException
334
     * @throws InvalidInterfaceException
335
     * @throws InvalidArgumentException
336
     */
337
    private function getModelsWithDatetimeFields()
338
    {
339
        $this->getModelsToProcess();
340
        if (! empty($this->models_with_datetime_fields)) {
341
            return $this->models_with_datetime_fields;
342
        }
343
344
        $all_non_abstract_models = EE_Registry::instance()->non_abstract_db_models;
345
        foreach ($all_non_abstract_models as $non_abstract_model) {
346
            //get model instance
347
            /** @var EEM_Base $non_abstract_model */
348
            $non_abstract_model = $non_abstract_model::instance();
349
            if ($non_abstract_model->get_a_field_of_type('EE_Datetime_Field') instanceof EE_Datetime_Field) {
350
                $this->models_with_datetime_fields[] = get_class($non_abstract_model);
351
            }
352
        }
353
        $this->setModelsToProcess($this->models_with_datetime_fields);
354
        return $this->models_with_datetime_fields;
355
    }
356
357
358
    /**
359
     * This simply records the models that have been processed with our tracking option.
360
     * @param array $models_to_set  array of model class names.
361
     */
362
    private function setModelsToProcess($models_to_set)
363
    {
364
        update_option(self::MODELS_TO_PROCESS_OPTION_KEY, $models_to_set);
365
    }
366
367
368
    /**
369
     * Used to keep track of how many models have been processed for the batch
370
     * @param $count
371
     */
372
    private function updateCountOfModelsProcessed($count = 1)
373
    {
374
        $count = $this->getCountOfModelsProcessed() + (int) $count;
375
        update_option(self::COUNT_OF_MODELS_PROCESSED, $count);
376
    }
377
378
379
    /**
380
     * Retrieve the tracked number of models processed between requests.
381
     * @return int
382
     */
383
    private function getCountOfModelsProcessed()
384
    {
385
        return (int) get_option(self::COUNT_OF_MODELS_PROCESSED, 0);
386
    }
387
388
389
    /**
390
     * Returns the models that are left to process.
391
     * @return array  an array of model class names.
392
     */
393
    private function getModelsToProcess()
394
    {
395
        if (empty($this->models_with_datetime_fields)) {
396
            $this->models_with_datetime_fields = get_option(self::MODELS_TO_PROCESS_OPTION_KEY, array());
397
        }
398
        return $this->models_with_datetime_fields;
399
    }
400
401
402
    /**
403
     * Used to record the offset that will be applied to dates and times for EE_Datetime_Field columns.
404
     * @param float $offset
405
     */
406
    public static function updateOffset($offset)
407
    {
408
        update_option(self::OFFSET_TO_APPLY_OPTION_KEY, $offset);
409
    }
410
411
412
    /**
413
     * Used to retrieve the saved offset that will be applied to dates and times for EE_Datetime_Field columns.
414
     *
415
     * @return float
416
     */
417
    public static function getOffset()
418
    {
419
        return (float) get_option(self::OFFSET_TO_APPLY_OPTION_KEY, 0);
420
    }
421
422
423
    /**
424
     * Used to set the saved offset range start date.
425
     * @param DbSafeDateTime|null $start_date
426
     */
427 View Code Duplication
    public static function updateStartDateRange(DbSafeDateTime $start_date = null)
428
    {
429
        $date_to_save = $start_date instanceof DbSafeDateTime
430
            ? $start_date->format('U')
431
            : '';
432
        update_option(self::OPTION_KEY_OFFSET_RANGE_START_DATE, $date_to_save);
433
    }
434
435
436
    /**
437
     * Used to get the saved offset range start date.
438
     * @return DbSafeDateTime|null
439
     */
440 View Code Duplication
    public static function getStartDateRange()
441
    {
442
        $start_date = get_option(self::OPTION_KEY_OFFSET_RANGE_START_DATE, null);
443
        try {
444
            $datetime = DateTime::createFromFormat('U', $start_date, new DateTimeZone('UTC'));
445
            $start_date = $datetime instanceof DateTime
446
                ? DbSafeDateTime::createFromDateTime($datetime)
447
                : null;
448
449
        } catch (Exception $e) {
450
            $start_date = null;
451
        }
452
        return $start_date;
453
    }
454
455
456
457
    /**
458
     * Used to set the saved offset range end date.
459
     * @param DbSafeDateTime|null $end_date
460
     */
461 View Code Duplication
    public static function updateEndDateRange(DbSafeDateTime $end_date = null)
462
    {
463
        $date_to_save = $end_date instanceof DbSafeDateTime
464
            ? $end_date->format('U')
465
            : '';
466
        update_option(self::OPTION_KEY_OFFSET_RANGE_END_DATE, $date_to_save);
467
    }
468
469
470
    /**
471
     * Used to get the saved offset range end date.
472
     * @return DbSafeDateTime|null
473
     */
474 View Code Duplication
    public static function getEndDateRange()
475
    {
476
        $end_date = get_option(self::OPTION_KEY_OFFSET_RANGE_END_DATE, null);
477
        try {
478
            $datetime = DateTime::createFromFormat('U', $end_date, new DateTimeZone('UTC'));
479
            $end_date = $datetime instanceof Datetime
480
                ? DbSafeDateTime::createFromDateTime($datetime)
481
                : null;
482
        } catch (Exception $e) {
483
            $end_date = null;
484
        }
485
        return $end_date;
486
    }
487
}
488