Skip to content

Instantly share code, notes, and snippets.

@dereuromark
Created March 10, 2024 23:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dereuromark/7fda4bb9d320b525db903fce535fd9b3 to your computer and use it in GitHub Desktop.
Save dereuromark/7fda4bb9d320b525db903fce535fd9b3 to your computer and use it in GitHub Desktop.
JsonTypeUpgradeCommand.php
<?php
declare(strict_types=1);
namespace App\Command;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Database\Type\StringType;
use Cake\Database\TypeFactory;
use Exception;
use RuntimeException;
class JsonTypeUpgradeCommand extends Command
{
/**
* Adjust each table class and map it from json to json_string:
* $schema->setColumnType('...', 'json_string');
* Then run this command.
*
* @param \Cake\Console\Arguments $args The command arguments.
* @param \Cake\Console\ConsoleIo $io The console io
*
* @throws \RuntimeException
*
* @return int|null|void The exit code or null for success
*/
public function execute(Arguments $args, ConsoleIo $io)
{
$models = [
'EventImports',
'Feedback.FeedbackItems',
'FileStorage.FileStorage',
];
TypeFactory::map('json_string', StringType::class);
$modelsToProcess = [];
foreach ($models as $model) {
$table = $this->getTableLocator()->get($model);
$columns = $table->getSchema()->columns();
$columnsToProcess = [];
foreach ($columns as $column) {
if ($table->getSchema()->getColumnType($column) !== 'json_string') {
continue;
}
$columnsToProcess[] = $column;
}
if (!$columnsToProcess) {
$io->warning('No columns to process for ' . $model . ', skipping');
continue;
}
$modelsToProcess[$model] = $columnsToProcess;
}
$io->out('Processing');
foreach ($modelsToProcess as $model => $columnsToProcess) {
$io->out('- ' . $model . ': ' . implode(', ', $columnsToProcess));
}
$optionsBefore = 0;
$optionsAfter = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
foreach ($modelsToProcess as $model => $columnsToProcess) {
$table = $this->getTableLocator()->get($model);
$fields = array_merge(['id'], $columnsToProcess);
/** @var \Cake\Database\Type\JsonType $type */
$type = TypeFactory::build('json');
$type->setEncodingOptions($optionsBefore);
//TODO: paginate using Tools.Reset behavior?
$query = $table->find()
->select($fields);
if ($args->getOption('limit')) {
$query->limit((int)$args->getOption('limit'));
}
/** @var array<\Cake\Datasource\EntityInterface> $records */
$records = $query
->all()
->toArray();
foreach ($records as $record) {
foreach ($columnsToProcess as $column) {
$encodedStringOld = $record->get($column);
if (!$encodedStringOld) {
continue;
}
$array = json_decode($encodedStringOld, true);
$encodedStringNew = json_encode($array, $optionsAfter);
if ($encodedStringNew === false) {
throw new RuntimeException('Cannot encode JSON for ' . $model . '.' . $column);
}
if ($encodedStringNew === $encodedStringOld) {
continue;
}
$io->verbose($encodedStringOld);
$io->verbose($encodedStringNew);
$record->set($column, $encodedStringNew);
$record->setDirty($column);
}
}
$count = 0;
foreach ($records as $record) {
if (!$record->isDirty()) {
continue;
}
if (!$args->getOption('dry-run')) {
try {
$behaviors = $table->behaviors()->loaded();
foreach ($behaviors as $behavior) {
$table->behaviors()->unload($behavior);
}
$table->saveOrFail($record, ['checkRules' => false, 'checkExisting' => false]);
} catch (Exception $e) {
$io->error(get_class($table) . ' ' . $e->getMessage() . ': ' . print_r($record->getErrors(), true));
$io->warning(print_r($record->toArray(), true));
$io->abort('Aborted on ' . $model . ' record ' . $record->get('id'));
}
}
$count++;
}
$io->success($model . ': ' . $count . '/' . count($records) . ' records needed updated and got updated.');
}
}
/**
* Display help for this console.
*
* @param \Cake\Console\ConsoleOptionParser $parser The parser to update
*
* @return \Cake\Console\ConsoleOptionParser
*/
public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription(
'Tmp.',
);
$parser->addOption('dry-run', [
'short' => 'd',
'help' => 'Dry run the command.',
'boolean' => true,
]);
$parser->addOption('limit', [
'short' => 'l',
'help' => 'Limit of records.',
]);
return $parser;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment