Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[K4] How to migrate content to native link field #88

Open
medienbaecker opened this issue Jul 16, 2024 · 1 comment
Open

[K4] How to migrate content to native link field #88

medienbaecker opened this issue Jul 16, 2024 · 1 comment

Comments

@medienbaecker
Copy link
Contributor

medienbaecker commented Jul 16, 2024

I created a Kirby CLI command for migrating content to the native link field. It's still a work in progress and may not work correctly if you've used more features of the link field than I have. Please run a dry run first and consider this a starting point. Feel free to expand on it and share your improvements.

The command automatically transforms this structure…

Link:
  type: url
  value: https://example.com

Link2:
  type: email
  value: [email protected]

…to this:

Link: https://example.com

Link2: mailto:[email protected]

It can also handle JSON content (blocks field) and turns this…

"link": {
  "type": "url",
  "value": "https://example.com"
}

…to this:

"link": "https://example.com"

Here's the command:

<?php

declare(strict_types=1);

use Kirby\CLI\CLI;

return [
    'description' => 'Migrate from Link plugin to native K4 link field',
    'args' => [
        'verbose' => [
            'shortPrefix' => 'v',
            'longPrefix' => 'verbose',
            'description' => 'Verbose output',
            'defaultValue' => false,
            'noValue' => true,
        ],
        'dryrun' => [
            'longPrefix' => 'dry-run',
            'description' => 'Dry run',
            'defaultValue' => false,
            'noValue' => true,
        ],
    ],
    'command' => static function (CLI $cli): void {
        $directory = kirby()->roots()->content();
        $processed = 0;
        $replacements = 0;
        $isDryRun = $cli->arg('dryrun');
        $isVerbose = $cli->arg('verbose');

        // Recursively iterate through all files in the content directory
        $filenames = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($directory),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($filenames as $filename) {
            $filepath = $filename->getPathname();
            $relpath = str_replace($directory . '/', '', $filepath);
            
            // Process only .txt files
            if ($filename->isDir() || pathinfo($filepath, PATHINFO_EXTENSION) !== 'txt') {
                continue;
            }

            $content = file_get_contents($filepath);
            $fileReplacements = 0;

            // Process YAML-like structure with JSON content
            $content = preg_replace_callback(
                '/^(\w+):\s*(\[.*\])$/ms',
                function ($matches) use (&$fileReplacements, $isVerbose, $cli, $relpath) {
                    $fieldName = $matches[1];
                    $jsonContent = $matches[2];
                    
                    $json = json_decode($jsonContent, true);
                    if (json_last_error() === JSON_ERROR_NONE) {
                        $json = transformNestedJson($json, $fileReplacements);
                        
                        if ($isVerbose && $fileReplacements > 0) {
                            $cli->out("  Transformed field '{$fieldName}' in {$relpath}");
                        }
                        
                        return $fieldName . ': ' . json_encode($json, JSON_UNESCAPED_SLASHES);
                    }
                    return $matches[0];
                },
                $content
            );

            // Write changes to file if not a dry run
            if (!$isDryRun && $fileReplacements > 0) {
                file_put_contents($filepath, $content);
            }

            // Output processing results
            if ($fileReplacements === 0 && $isVerbose) {
                $cli->out("⏩ [0] {$relpath}");
            } elseif ($fileReplacements > 0) {
                $cli->out("✅ [{$fileReplacements}] {$relpath}");
            }

            $processed++;
            $replacements += $fileReplacements;
        }

        $cli->success("Files processed: {$processed}, Total replacements: {$replacements}");
    }
];

/**
 * Recursively transform nested JSON structures, including those stored as strings
 *
 * @param mixed $data The data to transform
 * @param int $replacements Reference to the replacement counter
 * @return mixed The transformed data
 */
function transformNestedJson($data, &$replacements) {
    if (is_array($data)) {
        foreach ($data as $key => &$value) {
            if (is_array($value)) {
                $value = transformNestedJson($value, $replacements);
            } elseif (is_string($value) && $key === 'categories') {
                // Handle nested JSON stored as a string
                $nestedJson = json_decode($value, true);
                if (json_last_error() === JSON_ERROR_NONE) {
                    $value = json_encode(transformNestedJson($nestedJson, $replacements), JSON_UNESCAPED_SLASHES);
                }
            }
            // Check for link structure and transform if found
            if (is_array($value) && isset($value['type']) &&
                in_array($value['type'], ['url', 'page', 'file', 'email', 'tel'])) {
                if (!isset($value['value']) || empty($value['value'])) {
                    // If there's no value or it's empty, set the entire field to an empty string
                    $value = '';
                } else {
                    $value = transformLink($value['type'], $value['value']);
                }
                $replacements++;
            }
        }
    }
    return $data;
}

/**
 * Transform a single link based on its type
 *
 * @param string $type The type of the link (url, page, file, email, tel)
 * @param string $value The value of the link
 * @return string The transformed link value
 */
function transformLink(string $type, string $value): string {
    if (empty($value)) {
        return '';
    }
    switch ($type) {
        case 'email':
            return 'mailto:' . $value;
        case 'tel':
            return 'tel:' . $value;
        default:
            return $value;
    }
}
@benwest
Copy link

benwest commented Nov 19, 2024

fantastic work - i extended it a little to link to pages and files by UUID

<?php declare(strict_types=1);

use Kirby\CLI\CLI;
use Kirby\Uuid\Uuid;

return [
  'description' => 'Migrate from Link plugin to native K4 link field',
  'args' => [
    'verbose' => [
      'shortPrefix' => 'v',
      'longPrefix' => 'verbose',
      'description' => 'Verbose output',
      'defaultValue' => false,
      'noValue' => true,
    ],
    'dryrun' => [
      'longPrefix' => 'dry-run',
      'description' => 'Dry run',
      'defaultValue' => false,
      'noValue' => true,
    ],
  ],
  'command' => static function (CLI $cli): void {
    $directory = kirby()->roots()->content();
    $processed = 0;
    $replacements = 0;
    $isDryRun = $cli->arg('dryrun');
    $isVerbose = $cli->arg('verbose');

    // Recursively iterate through all files in the content directory
    $filenames = new RecursiveIteratorIterator(
      new RecursiveDirectoryIterator($directory),
      RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($filenames as $filename) {
      $filepath = $filename->getPathname();
      $relpath = str_replace($directory . '/', '', $filepath);

      // Process only .txt files
      if ($filename->isDir() || pathinfo($filepath, PATHINFO_EXTENSION) !== 'txt') {
        continue;
      }

      $content = file_get_contents($filepath);
      $fileReplacements = 0;

      // Process YAML-like structure with JSON content
      $content = preg_replace_callback(
        '/^(\w+):\s*(\[.*\])$/ms',
        function ($matches) use (&$fileReplacements, $isVerbose, $cli, $relpath) {
          $fieldName = $matches[1];
          $jsonContent = $matches[2];

          $json = json_decode($jsonContent, true);
          if (json_last_error() === JSON_ERROR_NONE) {
            $json = transformNestedJson($json, $fileReplacements);

            if ($isVerbose && $fileReplacements > 0) {
              $cli->out("  Transformed field '{$fieldName}' in {$relpath}");
            }

            return $fieldName . ': ' . json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
          }
          return $matches[0];
        },
        $content
      );

      // Write changes to file if not a dry run
      if (!$isDryRun && $fileReplacements > 0) {
        file_put_contents($filepath, $content);
      }

      // Output processing results
      if ($fileReplacements === 0 && $isVerbose) {
        $cli->out("⏩ [0] {$relpath}");
      } elseif ($fileReplacements > 0) {
        $cli->out("✅ [{$fileReplacements}] {$relpath}");
      }

      $processed++;
      $replacements += $fileReplacements;
    }

    $cli->success("Files processed: {$processed}, Total replacements: {$replacements}");
  }
];

/**
 * Recursively transform nested JSON structures, including those stored as strings
 *
 * @param mixed $data The data to transform
 * @param int $replacements Reference to the replacement counter
 * @return mixed The transformed data
 */
function transformNestedJson($data, &$replacements)
{
  if (is_array($data)) {
    foreach ($data as $key => &$value) {
      if (is_array($value)) {
        $value = transformNestedJson($value, $replacements);
      } elseif (is_string($value) && $key === 'categories') {
        // Handle nested JSON stored as a string
        $nestedJson = json_decode($value, true);
        if (json_last_error() === JSON_ERROR_NONE) {
          $value = json_encode(transformNestedJson($nestedJson, $replacements), JSON_UNESCAPED_SLASHES);
        }
      }
      // Check for link structure and transform if found
      if (is_array($value) &&
          isset($value['type']) &&
          in_array($value['type'], ['url', 'page', 'file', 'email', 'tel'])) {
        if (!isset($value['value']) || empty($value['value'])) {
          // If there's no value or it's empty, set the entire field to an empty string
          $value = '';
        } else {
          $value = transformLink($value['type'], $value['value']);
        }
        $replacements++;
      }
    }
  }
  return $data;
}

/**
 * Transform a single link based on its type
 *
 * @param string $type The type of the link (url, page, file, email, tel)
 * @param string $value The value of the link
 * @return string The transformed link value
 */
function transformLink(string $type, string $value): string
{
  if (empty($value)) {
    return '';
  }
  switch ($type) {
    case 'email':
      return 'mailto:' . $value;
    case 'tel':
      return 'tel:' . $value;
    case 'page':
      $page = kirby()->page($value);
      return $page ? Uuid::for($page)->toString() : '';
    case 'file':
      $file = kirby()->file($value);
      return $file ? Uuid::for($file)->toString() : '';
    default:
      return $value;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants