robotstxt-og/includes/class-robotstxt-og-cli.php
2026-02-19 11:16:44 +00:00

337 lines
9.1 KiB
PHP

<?php
/**
* WP-CLI Commands Class
*
* Provides WP-CLI commands for managing OG image fallbacks.
*
* @package ROBOTSTXT_OG
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class Robotstxt_OG_CLI
*
* WP-CLI commands for OpenGraph (by ROBOTSTXT) plugin.
*
* ## EXAMPLES
*
* # Resolve fallback images for all posts
* $ wp og-fallback resolve --all
* Success: Resolved 45 posts. Failed: 2.
*
* # Resolve a single post
* $ wp og-fallback resolve 42
* Success: Post 42 resolved to https://example.com/image.jpg
*
* # Clear all cached fallback URLs
* $ wp og-fallback clear-cache --all
* Success: Cleared 45 cached fallback URLs.
*
* # Preview without making changes
* $ wp og-fallback resolve --all --dry-run
* Found 47 posts with featured images. (dry-run, no changes made)
*
* @since 1.0.0
*/
class Robotstxt_OG_CLI extends WP_CLI_Command {
/**
* Image resolver instance.
*
* @since 1.0.0
* @var Robotstxt_OG_Image_Resolver
*/
private Robotstxt_OG_Image_Resolver $resolver;
/**
* Constructor.
*
* @since 1.0.0
*
* @param Robotstxt_OG_Image_Resolver $resolver Image resolver instance.
*/
public function __construct( Robotstxt_OG_Image_Resolver $resolver ) {
$this->resolver = $resolver;
}
/**
* Resolve fallback images for posts.
*
* Clears cached fallback URL and re-resolves it for the specified post(s).
* Use --all to process all posts with featured images.
*
* ## OPTIONS
*
* [<post_id>]
* : The ID of a single post to resolve.
*
* [--all]
* : Resolve fallback images for all posts with featured images.
*
* [--dry-run]
* : Preview what would be done without making changes.
*
* [--post-type=<type>]
* : Limit resolution to a specific post type. Default: any.
*
* ## EXAMPLES
*
* wp og-fallback resolve --all
* wp og-fallback resolve --all --dry-run
* wp og-fallback resolve 42
* wp og-fallback resolve --all --post-type=post
*
* @since 1.0.0
*
* @param array $args Positional arguments.
* @param array $assoc_args Associative arguments.
* @return void
*/
public function resolve( array $args, array $assoc_args ): void {
$all = isset( $assoc_args['all'] );
$dry_run = isset( $assoc_args['dry-run'] );
$post_type = isset( $assoc_args['post-type'] ) ? $assoc_args['post-type'] : 'any';
if ( $all ) {
$this->resolve_all( $dry_run, $post_type );
return;
}
if ( ! empty( $args[0] ) ) {
$post_id = absint( $args[0] );
$this->resolve_single( $post_id, $dry_run );
return;
}
WP_CLI::error( 'Please specify a post ID or use --all flag.' );
}
/**
* Clear cached fallback URLs.
*
* ## OPTIONS
*
* [<post_id>]
* : The ID of a single post to clear cache for.
*
* [--all]
* : Clear all cached fallback URLs.
*
* [--dry-run]
* : Preview what would be done without making changes.
*
* ## EXAMPLES
*
* wp og-fallback clear-cache --all
* wp og-fallback clear-cache 42
* wp og-fallback clear-cache --all --dry-run
*
* @since 1.0.0
*
* @param array $args Positional arguments.
* @param array $assoc_args Associative arguments.
* @return void
*/
public function clear_cache( array $args, array $assoc_args ): void {
$all = isset( $assoc_args['all'] );
$dry_run = isset( $assoc_args['dry-run'] );
if ( $all ) {
$this->clear_all_caches( $dry_run );
return;
}
if ( ! empty( $args[0] ) ) {
$post_id = absint( $args[0] );
$this->clear_single_cache( $post_id, $dry_run );
return;
}
WP_CLI::error( 'Please specify a post ID or use --all flag.' );
}
/**
* Resolve fallback images for all posts.
*
* @since 1.0.0
*
* @param bool $dry_run Whether to run without making changes.
* @param string $post_type Post type to limit to.
* @return void
*/
private function resolve_all( bool $dry_run, string $post_type ): void {
$posts = get_posts(
array(
'post_type' => $post_type,
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_thumbnail_id',
'compare' => 'EXISTS',
),
),
'fields' => 'ids',
)
);
$count = count( $posts );
if ( $dry_run ) {
WP_CLI::line(
/* translators: %d: number of posts found */
sprintf( __( 'Found %d posts with featured images. (dry-run, no changes made)', 'robotstxt-og' ), $count )
);
return;
}
if ( 0 === $count ) {
WP_CLI::warning( __( 'No posts with featured images found.', 'robotstxt-og' ) );
return;
}
$success = 0;
$failed = 0;
$progress = WP_CLI\Utils\make_progress_bar(
/* translators: %d: number of posts to process */
sprintf( __( 'Resolving %d posts', 'robotstxt-og' ), $count ),
$count
);
foreach ( $posts as $post_id ) {
$this->resolver->clear_cache( $post_id );
$url = $this->resolver->resolve_image( $post_id );
if ( ! empty( $url ) ) {
++$success;
} else {
++$failed;
}
$progress->tick();
}
$progress->finish();
WP_CLI::success(
/* translators: 1: successful count, 2: failed count */
sprintf( __( 'Resolved %1$d posts. Failed: %2$d.', 'robotstxt-og' ), $success, $failed )
);
}
/**
* Resolve fallback image for a single post.
*
* @since 1.0.0
*
* @param int $post_id Post ID.
* @param bool $dry_run Whether to run without making changes.
* @return void
*/
private function resolve_single( int $post_id, bool $dry_run ): void {
$post = get_post( $post_id );
if ( ! $post ) {
/* translators: %d: post ID */
WP_CLI::error( sprintf( __( 'Post %d not found.', 'robotstxt-og' ), $post_id ) );
return;
}
if ( $dry_run ) {
$thumb_id = get_post_thumbnail_id( $post_id );
$thumb = $thumb_id ? wp_get_attachment_url( $thumb_id ) : '';
/* translators: 1: post ID, 2: image URL */
WP_CLI::line( sprintf( __( 'Post %1$d has featured image: %2$s (dry-run, no changes made)', 'robotstxt-og' ), $post_id, ! empty( $thumb ) ? $thumb : 'none' ) );
return;
}
$this->resolver->clear_cache( $post_id );
$url = $this->resolver->resolve_image( $post_id );
if ( ! empty( $url ) ) {
/* translators: 1: post ID, 2: resolved URL */
WP_CLI::success( sprintf( __( 'Post %1$d resolved to %2$s', 'robotstxt-og' ), $post_id, $url ) );
} else {
/* translators: %d: post ID */
WP_CLI::warning( sprintf( __( 'Post %d: no compatible image found.', 'robotstxt-og' ), $post_id ) );
}
}
/**
* Clear all cached fallback URLs.
*
* @since 1.0.0
*
* @param bool $dry_run Whether to run without making changes.
* @return void
*/
private function clear_all_caches( bool $dry_run ): void {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
// WP-CLI single-invocation context; no persistent cache layer is appropriate here.
$count = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s",
'_og_image_fallback_url'
)
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( $dry_run ) {
/* translators: %d: number of cache entries found */
WP_CLI::line( sprintf( __( 'Found %d cached fallback URLs. (dry-run, no changes made)', 'robotstxt-og' ), $count ) );
return;
}
if ( 0 === $count ) {
WP_CLI::warning( __( 'No cached fallback URLs found.', 'robotstxt-og' ) );
return;
}
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
// Bulk delete of all cached postmeta rows — no WP API equivalent for this operation.
$deleted = $wpdb->delete(
$wpdb->postmeta,
array( 'meta_key' => '_og_image_fallback_url' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( '%s' )
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
/* translators: %d: number of cleared entries */
WP_CLI::success( sprintf( __( 'Cleared %d cached fallback URLs.', 'robotstxt-og' ), (int) $deleted ) );
}
/**
* Clear cached fallback URL for a single post.
*
* @since 1.0.0
*
* @param int $post_id Post ID.
* @param bool $dry_run Whether to run without making changes.
* @return void
*/
private function clear_single_cache( int $post_id, bool $dry_run ): void {
$cached = get_post_meta( $post_id, '_og_image_fallback_url', true );
if ( $dry_run ) {
/* translators: 1: post ID, 2: cached URL or 'none' */
WP_CLI::line( sprintf( __( 'Post %1$d cached URL: %2$s (dry-run, no changes made)', 'robotstxt-og' ), $post_id, ! empty( $cached ) ? $cached : 'none' ) );
return;
}
if ( empty( $cached ) ) {
/* translators: %d: post ID */
WP_CLI::warning( sprintf( __( 'Post %d has no cached fallback URL.', 'robotstxt-og' ), $post_id ) );
return;
}
$this->resolver->clear_cache( $post_id );
/* translators: %d: post ID */
WP_CLI::success( sprintf( __( 'Cleared cached fallback URL for post %d.', 'robotstxt-og' ), $post_id ) );
}
}