Skip to content

Commit

Permalink
Merge pull request #1 from ItinerisLtd/feature/csv-input
Browse files Browse the repository at this point in the history
feat: add input csv support
  • Loading branch information
codepuncher authored Jan 21, 2024
2 parents 036e831 + bb67aea commit 8342210
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 42 deletions.
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contr
wp post meta import <file> [--[no-]dry-run] [--yes]
~~~

Processes a JSON array of objects with requried `"url"` property.
Imports post meta for URLs from CSV or JSON files.
URLs will be passed to [url_to_postid()](https://developer.wordpress.org/reference/functions/url_to_postid/) to find a post ID.

Each object property will be treated as a meta field to update.
Meta updates must be provides in a `"key": "value"` pair.
If providing a CSV file, the first row will be used as a header row for meta keys.
If providing a JSON file, each object key:value pair will be used for meta_key:meta_value.

Values will be whitespace trimmed and skipped if empty.
Keys and values will be whitespace trimmed and skipped if empty.

Does not support terms.

**OPTIONS**

<file>
The input JSON file to parse. Path must be relative to ABSPATH.
The input file to parse. Path must be relative to ABSPATH.

[--[no-]dry-run]
Whether to just report on changes or also save changes to database.
Expand All @@ -51,22 +51,30 @@ Does not support terms.
"url": "https://example.com/hello-world",
"my_meta_field": "My new value!",
"_yoast_wpseo_title": "My new SEO title",
"_yoast_wpseo_metadesc": "", # This will not be changed
"_yoast_wpseo_metadesc": "",
"_yoast_wpseo_canonical": "https://example.co.uk/foo-bar"
}
]

**SAMPLE CSV**
url,my_meta_field,_yoast_wpseo_title,_yoast_wpseo_metadesc,_yoast_wpseo_canonical
https://example.com/sample-page,My new value!,My new SEO title,My new SEO description,https://example.co.uk/sample-page
https://example.com/hello-world,My new value!,My new SEO title,,https://example.co.uk/foo-bar

**EXAMPLES**

$ wp post meta import wp-content/uploads/post-meta.json --dry-run
$ wp post meta import wp-content/uploads/post-meta.csv --dry-run
350 detected records to process
Are you ready to process 350 records? [y/n]
...
Finished: Rows processed: 350. Meta processed: 981.
Finished.
Rows processed: 350. Meta processed: 981.
---
$ wp post meta import wp-content/uploads/post-meta.json --yes --no-dry-run
350 detected records to process
...
Finished. Rows processed: 350. Meta processed: 981. Meta updated: 440. Meta skipped: 535. Meta unchanged: 6. Meta failed 0.
Finished.
Rows processed: 350. Meta processed: 981. Meta updated: 440. Meta skipped: 535. Meta unchanged: 6. Meta failed 0.

## Installing

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
}
],
"require": {
"league/csv": "^9.14",
"wp-cli/wp-cli": "^2.5"
},
"require-dev": {
Expand Down
110 changes: 77 additions & 33 deletions src/PostMetaImport.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace ItinerisLtd\PostMetaImport;

use Exception;
use League\Csv\Reader;
use League\Csv\UnavailableStream;
use WP_CLI;
use WP_CLI_Command;

Expand All @@ -22,20 +25,20 @@ class PostMetaImport extends WP_CLI_Command
/**
* Bulk import meta data for posts.
*
* Processes a JSON array of objects with requried `"url"` property.
* Imports post meta for URLs from CSV or JSON files.
* URLs will be passed to [url_to_postid()](https://developer.wordpress.org/reference/functions/url_to_postid/) to find a post ID.
*
* Each object property will be treated as a meta field to update.
* Meta updates must be provides in a `"key": "value"` pair.
* If providing a CSV file, the first row will be used as a header row for meta keys.
* If providing a JSON file, each object key:value pair will be used for meta_key:meta_value.
*
* Values will be whitespace trimmed and skipped if empty.
* Keys and values will be whitespace trimmed and skipped if empty.
*
* Does not support terms.
*
* ## OPTIONS
*
* <file>
* : The input JSON file to parse. Path must be relative to ABSPATH.
* : The input file to parse. Path must be relative to ABSPATH.
*
* [--[no-]dry-run]
* : Whether to just report on changes or also save changes to database.
Expand All @@ -60,22 +63,30 @@ class PostMetaImport extends WP_CLI_Command
* "url": "https://example.com/hello-world",
* "my_meta_field": "My new value!",
* "_yoast_wpseo_title": "My new SEO title",
* "_yoast_wpseo_metadesc": "", # This will not be changed
* "_yoast_wpseo_metadesc": "",
* "_yoast_wpseo_canonical": "https://example.co.uk/foo-bar"
* }
* ]
*
* ## SAMPLE CSV
* url,my_meta_field,_yoast_wpseo_title,_yoast_wpseo_metadesc,_yoast_wpseo_canonical
* https://example.com/sample-page,My new value!,My new SEO title,My new SEO description,https://example.co.uk/sample-page
* https://example.com/hello-world,My new value!,My new SEO title,,https://example.co.uk/foo-bar
*
* ## EXAMPLES
*
* $ wp post meta import wp-content/uploads/post-meta.json --dry-run
* $ wp post meta import wp-content/uploads/post-meta.csv --dry-run
* 350 detected records to process
* Are you ready to process 350 records? [y/n]
* ...
* Finished: Rows processed: 350. Meta processed: 981.
* Finished.
* Rows processed: 350. Meta processed: 981.
* ---
* $ wp post meta import wp-content/uploads/post-meta.json --yes --no-dry-run
* 350 detected records to process
* ...
* Finished. Rows processed: 350. Meta processed: 981. Meta updated: 440. Meta skipped: 535. Meta unchanged: 6. Meta failed 0.
* Finished.
* Rows processed: 350. Meta processed: 981. Meta updated: 440. Meta skipped: 535. Meta unchanged: 6. Meta failed 0.
*/
public function __invoke(array $args, array $assoc_args): void
{
Expand All @@ -84,9 +95,13 @@ public function __invoke(array $args, array $assoc_args): void
WP_CLI::error('No input file provided');
}

if (! str_ends_with($input_file, '.csv') && ! str_ends_with($input_file, '.json')) {
WP_CLI::error('Input file must be .csv or .json');
}

$this->data = $this->toArray($input_file);
if (empty($this->data)) {
WP_CLI::error('Parsed JSON data is empty');
WP_CLI::error('Input file is empty');
}

$this->row_count = count($this->data);
Expand Down Expand Up @@ -136,44 +151,44 @@ protected function run(bool $dry_run = true): void
{
foreach ($this->data as $key => $data) {
$this->rows_processed++;
if (empty($data) || empty($data->url)) {
if (empty($data) || empty($data['url'])) {
$this->rows_failed++;
WP_CLI::error("Data or data->url empty for item with the index: {$key} in json data", false);
WP_CLI::error("Record or URL is empty for item with the index: {$key}", false);
continue;
}

$post_id = url_to_postid($data->url);
$post_id = url_to_postid($data['url']);
if (empty($post_id)) {
$this->rows_failed++;
WP_CLI::error(
"Could not find post ID for {$data->url}; the URL either doesn't exist or is not a post.",
"Could not find post ID for {$data['url']}; the URL either doesn't exist or is not a post.",
false,
);
continue;
}

WP_CLI::log("Post #{$post_id} - {$data->url}");
WP_CLI::log("Post #{$post_id} - {$data['url']}");
$this->updatePost($post_id, $data, $dry_run);
WP_CLI::log(PHP_EOL);
}
}

/**
* Update a posts meta data.
*/
protected function updatePost(int $post_id, object $data, bool $dry_run = false): void
protected function updatePost(int $post_id, array $fields, bool $dry_run = true): void
{
if (empty($post_id) || empty($data)) {
if (empty($post_id) || empty($fields)) {
return;
}

$fields = get_object_vars($data);
$url = $fields['url'];
unset($fields['url']);

foreach ($fields as $key => $value) {
$this->meta_processed++;
$current_value = get_post_meta($post_id, $key, true);
$key = trim($key ?? '');
$new_value = trim($value ?? '');
$current_value = get_post_meta($post_id, $key, true);
if ($dry_run) {
if (empty($new_value)) {
continue;
Expand All @@ -187,7 +202,7 @@ protected function updatePost(int $post_id, object $data, bool $dry_run = false)
}

if (empty($new_value)) {
WP_CLI::warning("The value for field '{$key}' on '{$data->url}' is empty");
WP_CLI::warning("The value for field '{$key}' on '{$url}' is empty");
$this->meta_skipped++;
continue;
}
Expand All @@ -196,18 +211,53 @@ protected function updatePost(int $post_id, object $data, bool $dry_run = false)
if (false === $update_meta) {
if ($current_value === $new_value) {
$this->meta_unchanged++;
WP_CLI::success("Value passed for field '{$key}' is unchanged on {$data->url}.");
WP_CLI::success("Value passed for field '{$key}' is unchanged on {$url}.");
} else {
$this->meta_failed++;
WP_CLI::error("Failed to update value for '{$key}' on {$data->url}.", false);
WP_CLI::error("Failed to update value for '{$key}' on {$url}.", false);
}
} else {
$this->meta_updated++;
WP_CLI::success("Updated '{$key}' field on {$data->url}.");
WP_CLI::success("Updated '{$key}' field on {$url}.");
}
}
}

protected function jsonToArray(string $file_path): array
{
$file_contents = file_get_contents($file_path);
if (empty($file_contents)) {
WP_CLI::error('Could not read input file.');
}

$records = json_decode($file_contents, true);
if (empty($records)) {
WP_CLI::error('Could not run json_decode on input file.');
}

return $records;
}

protected function csvToArray(string $file_path): array
{
try {
$reader = Reader::createFromPath($file_path, 'r');
} catch (UnavailableStream $err) {
WP_CLI::error($err->getMessage(), true);
}

$reader->includeEmptyRecords();
$reader->setHeaderOffset(0);

try {
$records = $reader->getRecords();
} catch (Exception $err) {
WP_CLI::error($err->getMessage(), true);
}

return iterator_to_array($records);
}

protected function toArray(string $input_file): array
{
$file_path = ABSPATH . ltrim($input_file, '/\\');
Expand All @@ -216,16 +266,10 @@ protected function toArray(string $input_file): array
return [];
}

$file_contents = file_get_contents($file_path);
if (empty($file_contents)) {
WP_CLI::error('Could not read input file.');
}

$json = json_decode($file_contents);
if (empty($json)) {
WP_CLI::error('Could not run json_decode on input file.');
if (str_ends_with($input_file, '.json')) {
return $this->jsonToArray($file_path);
}

return $json;
return $this->csvToArray($file_path);
}
}

0 comments on commit 8342210

Please sign in to comment.