Completed
Push — 7.4 ( 671048...f04f32 )
by André
89:32 queued 70:15
created

UpdateTimestampsToUTCCommand::execute()   D

Complexity

Conditions 16
Paths 83

Size

Total Lines 116

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 83
nop 2
dl 0
loc 116
rs 4.4532
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the eZ Publish Kernel package.
4
 *
5
 * @copyright Copyright (C) eZ Systems AS. All rights reserved
6
 * @license For full copyright and license information view LICENSE file distributed with this source code
7
 */
8
namespace eZ\Bundle\EzPublishCoreBundle\Command;
9
10
use DateTime;
11
use DateTimeZone;
12
use Doctrine\DBAL\Connection;
13
use RuntimeException;
14
use Symfony\Component\Process\Process;
15
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
16
use Symfony\Component\Console\Input\InputInterface;
17
use Symfony\Component\Console\Output\OutputInterface;
18
use Symfony\Component\Console\Input\InputArgument;
19
use Symfony\Component\Console\Input\InputOption;
20
use Symfony\Component\Console\Helper\ProgressBar;
21
use Symfony\Component\Console\Question\ConfirmationQuestion;
22
use Symfony\Component\Process\PhpExecutableFinder;
23
use PDO;
24
25
class UpdateTimestampsToUTCCommand extends ContainerAwareCommand
26
{
27
    const MAX_TIMESTAMP_VALUE = 2147483647;
28
    const DEFAULT_ITERATION_COUNT = 100;
29
    const MODES = [
30
        'date' => ['ezdate'],
31
        'datetime' => ['ezdatetime'],
32
        'all' => ['ezdate', 'ezdatetime'],
33
    ];
34
35
    /**
36
     * @var int
37
     */
38
    protected $done = 0;
39
40
    /**
41
     * @var string
42
     */
43
    protected $timezone;
44
45
    /**
46
     * @var string
47
     */
48
    private $mode;
49
50
    /**
51
     * @var string
52
     */
53
    private $from;
54
55
    /**
56
     * @var string
57
     */
58
    private $to;
59
60
    /**
61
     * @var \Doctrine\DBAL\Connection
62
     */
63
    private $connection;
64
65
    /**
66
     * @var string
67
     */
68
    private $phpPath;
69
70
    /**
71
     * @var bool
72
     */
73
    private $dryRun;
74
75
    protected function configure()
76
    {
77
        $this
78
            ->setName('ezplatform:timestamps:to-utc')
79
            ->setDescription('Updates ezdate & ezdatetime timestamps to UTC')
80
            ->addArgument(
81
                'timezone',
82
                InputArgument::OPTIONAL,
83
                'Original timestamp\'s TimeZone',
84
                null
85
            )
86
            ->addOption(
87
                'dry-run',
88
                null,
89
                InputOption::VALUE_NONE,
90
                'Execute a dry run'
91
            )
92
            ->addOption(
93
                'mode',
94
                null,
95
                InputOption::VALUE_REQUIRED,
96
                'Select conversion scope: date, datetime, all',
97
                'all'
98
            )
99
            ->addOption(
100
                'from',
101
                null,
102
                InputOption::VALUE_REQUIRED,
103
                'Only versions AFTER this date will be converted',
104
                null
105
            )
106
            ->addOption(
107
                'to',
108
                null,
109
                InputOption::VALUE_REQUIRED,
110
                'Only versions BEFORE this date will be converted',
111
                null
112
            )
113
            ->addOption(
114
                'offset',
115
                null,
116
                InputArgument::OPTIONAL,
117
                'Offset for updating records',
118
                0
119
            )
120
            ->addOption(
121
                'iteration-count',
122
                null,
123
                InputArgument::OPTIONAL,
124
                'Limit how much records get updated by single process',
125
                self::DEFAULT_ITERATION_COUNT
126
            )
127
            ->setHelp(
128
                <<<'EOT'
129
The command <info>%command.name%</info> updates field
130
data_int in configured Legacy Storage database for a given field type.
131
132
This is to be used either when migrating from eZ Publish to eZ Platform 
133
(when using platform backend instead of legacy), or when upgrading legacy 
134
to v2019.03 which has been adapted to use UTC.
135
136
<warning>During the script execution the database should not be modified.
137
138
You are advised to create a backup or execute a dry run before 
139
proceeding with actual update.</warning>
140
141
<warning>This command should be only ran ONCE.</warning>
142
143
Since this script can potentially run for a very long time, to avoid memory
144
exhaustion run it in production environment using <info>--env=prod</info> switch.
145
EOT
146
            );
147
    }
148
149
    protected function initialize(InputInterface $input, OutputInterface $output)
150
    {
151
        /* @var \Doctrine\DBAL\Connection $databaseHandler */
152
        $this->connection = $this->getContainer()->get('ezpublish.api.search_engine.legacy.connection');
153
    }
154
155
    /**
156
     * @param \Symfony\Component\Console\Input\InputInterface $input
157
     * @param \Symfony\Component\Console\Output\OutputInterface $output
158
     */
159
    protected function execute(InputInterface $input, OutputInterface $output)
160
    {
161
        $iterationCount = (int) $input->getOption('iteration-count');
162
        $this->dryRun = $input->getOption('dry-run');
0 ignored issues
show
Documentation Bug introduced by
It seems like $input->getOption('dry-run') can also be of type string or array<integer,string>. However, the property $dryRun is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
163
        $this->mode = $input->getOption('mode');
0 ignored issues
show
Documentation Bug introduced by
It seems like $input->getOption('mode') can also be of type array<integer,string> or boolean. However, the property $mode is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
164
165
        if (!isset(self::MODES[$this->mode])) {
166
            $output->writeln(
167
                sprintf('Selected mode is not supported, please use one of: %s', implode(', ', array_keys(self::MODES)))
168
            );
169
170
            return;
171
        }
172
173
        $from = $input->getOption('from');
174
        $to = $input->getOption('to');
175
176
        if ($from && !$this->validateDateTimeString($from, $output)) {
177
            return;
178
        }
179
        if ($to && !$this->validateDateTimeString($to, $output)) {
180
            return;
181
        }
182
        if ($from) {
183
            $this->from = $this->dateStringToTimestamp($from);
0 ignored issues
show
Documentation Bug introduced by
The property $from was declared of type string, but $this->dateStringToTimestamp($from) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
184
        }
185
        if ($to) {
186
            $this->to = $this->dateStringToTimestamp($to);
0 ignored issues
show
Documentation Bug introduced by
The property $to was declared of type string, but $this->dateStringToTimestamp($to) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
187
        }
188
189
        $consoleScript = $_SERVER['argv'][0];
190
191
        if (getenv('INNER_CALL')) {
192
            $this->timezone = $input->getArgument('timezone');
0 ignored issues
show
Documentation Bug introduced by
It seems like $input->getArgument('timezone') can also be of type array<integer,string>. However, the property $timezone is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
193
            $this->processTimestamps((int) $input->getOption('offset'), $iterationCount, $output);
194
            $output->writeln($this->done);
195
        } else {
196
            $timezone = $input->getArgument('timezone');
197
            $this->timezone = $this->validateTimezone($timezone, $output);
0 ignored issues
show
Bug introduced by
It seems like $timezone defined by $input->getArgument('timezone') on line 196 can also be of type array<integer,string> or null; however, eZ\Bundle\EzPublishCoreB...and::validateTimezone() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
198
199
            $output->writeln([
200
                sprintf('Converting timestamps for fields: %s', implode(', ', self::MODES[$this->mode])),
201
                'Calculating number of Field values to update...',
202
            ]);
203
            $count = $this->countTimestampBasedFields();
204
            $output->writeln([
205
                sprintf('Found total of Field values for update: %d', $count),
206
                '',
207
            ]);
208
209
            if ($count == 0) {
210
                $output->writeln('Nothing to process, exiting.');
211
212
                return;
213
            }
214
215
            $helper = $this->getHelper('question');
216
            $question = new ConfirmationQuestion(
217
                '<question>Are you sure you want to proceed?</question> ',
218
                false
219
            );
220
221
            if (!$helper->ask($input, $output, $question)) {
222
                $output->writeln('');
223
224
                return;
225
            }
226
227
            $progressBar = $this->getProgressBar($count, $output);
228
            $progressBar->start();
229
230
            for ($offset = 0; $offset < $count; $offset += $iterationCount) {
231
                $processScriptFragments = [
232
                    $this->getPhpPath(),
233
                    $consoleScript,
234
                    $this->getName(),
235
                    $this->timezone,
236
                    '--mode=' . $this->mode,
237
                    '--offset=' . $offset,
238
                    '--iteration-count=' . $iterationCount,
239
                ];
240
241
                if ($from) {
242
                    $processScriptFragments[] = '--from=' . $from;
243
                }
244
                if ($to) {
245
                    $processScriptFragments[] = '--to=' . $to;
246
                }
247
                if ($this->dryRun) {
248
                    $processScriptFragments[] = '--dry-run';
249
                }
250
251
                $process = new Process(
252
                    implode(' ', $processScriptFragments)
253
                );
254
255
                $process->setEnv(['INNER_CALL' => 1]);
256
                $process->run();
257
258
                if (!$process->isSuccessful()) {
259
                    throw new RuntimeException($process->getErrorOutput());
260
                }
261
262
                $doneInProcess = (int) $process->getOutput();
263
                $this->done += $doneInProcess;
264
265
                $progressBar->advance($doneInProcess);
266
            }
267
268
            $progressBar->finish();
269
            $output->writeln([
270
                '',
271
                sprintf('Done: %d', $this->done),
272
            ]);
273
        }
274
    }
275
276
    /**
277
     * @param int $offset
278
     * @param int $limit
279
     * @param \Symfony\Component\Console\Output\OutputInterface $output
280
     */
281
    protected function processTimestamps($offset, $limit, $output)
0 ignored issues
show
Unused Code introduced by
The parameter $output is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
282
    {
283
        $timestampBasedFields = $this->getTimestampBasedFields($offset, $limit);
284
285
        $dateTimeInUTC = new DateTime();
286
        $dateTimeInUTC->setTimezone(new DateTimeZone('UTC'));
287
288
        foreach ($timestampBasedFields as $timestampBasedField) {
289
            $timestamp = $timestampBasedField['data_int'];
290
            $dateTimeInUTC->setTimestamp($timestamp);
291
            $newTimestamp = $this->convertToUtcTimestamp($timestamp);
292
293
            //failsafe for int field limitation (dates/datetimes after 01/19/2038 @ 4:14am (UTC))
294
            if ($newTimestamp <= self::MAX_TIMESTAMP_VALUE && !$this->dryRun) {
295
                $this->updateTimestampToUTC($timestampBasedField['id'], $newTimestamp);
296
            }
297
            ++$this->done;
298
        }
299
    }
300
301
    /**
302
     * @param int $offset
303
     * @param int $limit
304
     *
305
     * @return array
306
     */
307
    protected function getTimestampBasedFields($offset, $limit)
308
    {
309
        $query = $this->connection->createQueryBuilder();
310
        $query
311
            ->select('a.id, a.data_int')
312
            ->from('ezcontentobject_attribute', 'a')
313
            ->join('a', 'ezcontentobject_version', 'v', 'a.contentobject_id = v.contentobject_id')
314
            ->where(
315
                $query->expr()->in(
316
                    'a.data_type_string',
317
                    $query->createNamedParameter(self::MODES[$this->mode], Connection::PARAM_STR_ARRAY)
318
                )
319
            )
320
            ->andWhere('a.data_int is not null')
321
            ->andWhere('a.data_int > 0')
322
            ->andWhere('v.version = a.version')
323
            ->setFirstResult($offset)
324
            ->setMaxResults($limit);
325
326
        if ($this->from) {
327
            $query
328
                ->andWhere('v.modified >= :fromTimestamp')
329
                ->setParameter('fromTimestamp', $this->from);
330
        }
331
        if ($this->to) {
332
            $query
333
                ->andWhere('v.modified <= :toTimestamp')
334
                ->setParameter('toTimestamp', $this->to);
335
        }
336
337
        $statement = $query->execute();
338
339
        return $statement->fetchAll(PDO::FETCH_ASSOC);
340
    }
341
342
    /**
343
     * Counts affected timestamp based fields using captured "mode", "from" and "to" command options.
344
     *
345
     * @return int
346
     */
347
    protected function countTimestampBasedFields()
348
    {
349
        $query = $this->connection->createQueryBuilder();
350
        $query
351
            ->select('count(*) as count')
352
            ->from('ezcontentobject_attribute', 'a')
353
            ->join('a', 'ezcontentobject_version', 'v', 'a.contentobject_id = v.contentobject_id')
354
            ->where(
355
                $query->expr()->in(
356
                    'a.data_type_string',
357
                    $query->createNamedParameter(self::MODES[$this->mode], Connection::PARAM_STR_ARRAY)
358
                )
359
            )
360
            ->andWhere('a.data_int is not null')
361
            ->andWhere('a.data_int > 0')
362
            ->andWhere('v.version = a.version');
363
364
        if ($this->from) {
365
            $query
366
                ->andWhere('v.modified >= :fromTimestamp')
367
                ->setParameter('fromTimestamp', $this->from);
368
        }
369
        if ($this->to) {
370
            $query
371
                ->andWhere('v.modified <= :toTimestamp')
372
                ->setParameter('toTimestamp', $this->to);
373
        }
374
375
        $statement = $query->execute();
376
377
        return (int) $statement->fetchColumn();
378
    }
379
380
    /**
381
     * @param int $timestamp
382
     * @return int
383
     */
384
    protected function convertToUtcTimestamp($timestamp)
385
    {
386
        $dateTimeZone = new DateTimeZone($this->timezone);
387
        $dateTimeZoneUTC = new DateTimeZone('UTC');
388
389
        $dateTime = new DateTime(null, $dateTimeZone);
390
        $dateTime->setTimestamp($timestamp);
391
        $dateTimeUTC = new DateTime($dateTime->format('Y-m-d H:i:s'), $dateTimeZoneUTC);
392
393
        return $dateTimeUTC->getTimestamp();
394
    }
395
396
    /**
397
     * @param string $dateTimeString
398
     * @param OutputInterface $output
399
     * @return bool
400
     */
401
    protected function validateDateTimeString($dateTimeString, OutputInterface $output)
402
    {
403
        try {
404
            new \DateTime($dateTimeString);
405
        } catch (\Exception $exception) {
406
            $output->writeln('The --from and --to options must be a valid Date string.');
407
408
            return false;
409
        }
410
411
        return true;
412
    }
413
414
    /**
415
     * @param string $timezone
416
     * @param OutputInterface $output
417
     * @return string
418
     */
419
    protected function validateTimezone($timezone, OutputInterface $output)
420
    {
421
        if (!$timezone) {
422
            $timezone = date_default_timezone_get();
423
            $output->writeln([
424
                sprintf('No Timezone set, using server Timezone: %s', $timezone),
425
                '',
426
            ]);
427
        } else {
428
            if (!\in_array($timezone, timezone_identifiers_list())) {
429
                $output->writeln([
430
                    sprintf('% is not correct Timezone.', $timezone),
431
                    '',
432
                ]);
433
434
                return;
435
            }
436
437
            $output->writeln([
438
                sprintf('Using timezone: %s', $timezone),
439
                '',
440
            ]);
441
        }
442
443
        return $timezone;
444
    }
445
446
    /**
447
     * Return configured progress bar helper.
448
     *
449
     * @param int $maxSteps
450
     * @param \Symfony\Component\Console\Output\OutputInterface $output
451
     *
452
     * @return \Symfony\Component\Console\Helper\ProgressBar
453
     */
454
    protected function getProgressBar($maxSteps, OutputInterface $output)
455
    {
456
        $progressBar = new ProgressBar($output, $maxSteps);
457
        $progressBar->setFormat(
458
            ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'
459
        );
460
461
        return $progressBar;
462
    }
463
464
    /**
465
     * @param int $contentAttributeId
466
     * @param int $newTimestamp
467
     */
468
    protected function updateTimestampToUTC(
469
        $contentAttributeId,
470
        $newTimestamp
471
    ) {
472
        $query = $this->connection->createQueryBuilder();
473
        $query
474
            ->update('ezcontentobject_attribute', 'a')
475
            ->set('a.data_int', $newTimestamp)
476
            ->set('a.sort_key_int', $newTimestamp)
477
            ->where('a.id = :id')
478
            ->setParameter(':id', $contentAttributeId);
479
480
        $query->execute();
481
    }
482
483
    /**
484
     * @return string
485
     */
486 View Code Duplication
    private function getPhpPath()
487
    {
488
        if ($this->phpPath) {
489
            return $this->phpPath;
490
        }
491
        $phpFinder = new PhpExecutableFinder();
492
        $this->phpPath = $phpFinder->find();
0 ignored issues
show
Documentation Bug introduced by
It seems like $phpFinder->find() can also be of type false. However, the property $phpPath is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
493
        if (!$this->phpPath) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->phpPath of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
494
            throw new RuntimeException(
495
                'The php executable could not be found, it\'s needed for executing parable sub processes, so add it to your PATH environment variable and try again'
496
            );
497
        }
498
499
        return $this->phpPath;
500
    }
501
502
    /**
503
     * @param $dateString string
504
     * @throws \Exception
505
     * @return int
506
     */
507
    private function dateStringToTimestamp($dateString)
508
    {
509
        $date = new \DateTime($dateString);
510
511
        return $date->getTimestamp();
512
    }
513
}
514