From b45393141d8c15a3a32e9b999ca62cd7a112af7a Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Tue, 10 Feb 2026 17:32:20 +0530 Subject: [PATCH] Add PHP 8.2 strict types, PHPCS, and PHPStan linting - Bump minimum PHP to >=8.2, add declare(strict_types=1) to all files - Change composer type from package to library - Add PHPCS with WordPress coding standards (WPCS 3.0) - Add PHPStan at level 6 with szepeviktor/phpstan-wordpress - Add GitHub Actions CI workflow for linting on push/PR - Fix all esc_html__() calls to include shadow-taxonomy text domain - Add file/class docblocks, @param tags for WP-CLI methods - Suppress intentional slow query and unused parameter warnings - Fix return type issues in get_associated_post and get_related_post_by_slug - Simplify post_type_already_in_sync (remove always-true isset checks) --- .github/workflows/lint.yml | 47 ++++++++++++++++ composer.json | 35 ++++++++---- includes/shadow-taxonomy-cli.php | 97 +++++++++++++++++++++++--------- includes/shadow-taxonomy.php | 46 ++++++++------- index.php | 2 + phpcs.xml.dist | 57 +++++++++++++++++++ phpstan.neon.dist | 35 ++++++++++++ 7 files changed, 262 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 phpcs.xml.dist create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a576d56 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,47 @@ +name: Lint + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + phpcs: + name: PHPCS + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: cs2pr + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHPCS + run: composer phpcs -- --report-full --report-checkstyle=./phpcs-report.xml + + - name: Show PHPCS results in PR + if: always() + run: cs2pr ./phpcs-report.xml + + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHPStan + run: composer phpstan diff --git a/composer.json b/composer.json index d4455f9..9fbf007 100644 --- a/composer.json +++ b/composer.json @@ -1,30 +1,43 @@ { "name": "spock/shadow-taxonomies", - "description": "A library used for create relationships between CPT's", - "type": "package", - "keywords": [], + "description": "A Composer library for creating relationships between custom post types using shadow taxonomies in WordPress.", + "type": "library", + "keywords": ["wordpress", "taxonomy", "custom-post-type", "relationships"], "homepage": "https://github.com/patelutkarsh/shadow-taxonomy", "license": "GPL-2.0+", "author": { "name": "Utkarsh Patel", "email": "utkarshpatel@outlook.com", - "homepage": "utkarshpatel.com", + "homepage": "https://utkarshpatel.com", "role": "Developer" }, - "repositories": [ - { - "type": "composer", - "url": "https://packagist.org" - } - ], "require": { - "php": ">=7.2" + "php": ">=8.2" }, "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "wp-coding-standards/wpcs": "^3.0", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpstan/phpstan": "^2.0", + "szepeviktor/phpstan-wordpress": "^2.0" }, "autoload": { "files": [ "index.php" ] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "scripts": { + "phpcs": "phpcs", + "phpcbf": "phpcbf", + "phpstan": "phpstan analyse", + "lint": [ + "@phpcs", + "@phpstan" + ] } } diff --git a/includes/shadow-taxonomy-cli.php b/includes/shadow-taxonomy-cli.php index 215126f..5e5467a 100644 --- a/includes/shadow-taxonomy-cli.php +++ b/includes/shadow-taxonomy-cli.php @@ -1,7 +1,15 @@ 'publish', 'posts_per_page' => self::BATCH_SIZE, 'paged' => $page, - 'meta_query' => [ + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required to find posts missing shadow meta. [ 'key' => Core\get_meta_key( $tax, 'term_id' ), 'compare' => 'NOT EXISTS', @@ -61,14 +72,14 @@ private function get_posts_missing_shadow_meta( string $cpt, string $tax ): arra ] ); if ( is_wp_error( $query ) ) { - \WP_CLI::error( esc_html__( 'An error occurred while searching for posts.' ) ); + \WP_CLI::error( esc_html__( 'An error occurred while searching for posts.', 'shadow-taxonomy' ) ); } if ( ! empty( $query->posts ) ) { $all_posts = array_merge( $all_posts, $query->posts ); } - $page++; + ++$page; } while ( $page <= $query->max_num_pages ); return $all_posts; @@ -142,7 +153,7 @@ private function delete_orphan_term( $term, string $tax, bool $verbose = false ) * @param array $items Array of [ 'action' => string, 'count' => int ] items. */ private function output_dry_run_table( array $items ): void { - \WP_CLI::warning( esc_html__( 'View the below table to see how many terms will be created or deleted.' ) ); + \WP_CLI::warning( esc_html__( 'View the below table to see how many terms will be created or deleted.', 'shadow-taxonomy' ) ); \WP_CLI\Utils\format_items( 'table', $items, [ 'action', 'count' ] ); } @@ -166,6 +177,9 @@ private function output_dry_run_table( array $items ): void { * : Allows you to see the number of shadow terms which need to be created or deleted. * * @subcommand sync + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function sync_shadow_terms( $args, $assoc_args ) { $tax = $assoc_args['tax']; @@ -180,7 +194,7 @@ public function sync_shadow_terms( $args, $assoc_args ) { */ $posts = $this->get_posts_missing_shadow_meta( $cpt, $tax ); - $terms_to_create = array_filter( $posts, function( $post ) use ( $tax ) { + $terms_to_create = array_filter( $posts, function ( $post ) use ( $tax ) { return empty( Core\get_associated_term( $post->ID, $tax ) ); } ); @@ -189,7 +203,7 @@ public function sync_shadow_terms( $args, $assoc_args ) { */ $all_terms = $this->get_all_terms( $tax ); - $terms_to_delete = array_filter( $all_terms, function( $term ) use ( $cpt ) { + $terms_to_delete = array_filter( $all_terms, function ( $term ) use ( $cpt ) { return empty( Core\get_associated_post( $term, $cpt ) ); } ); @@ -200,20 +214,26 @@ public function sync_shadow_terms( $args, $assoc_args ) { */ if ( $dry_run ) { $this->output_dry_run_table( [ - [ 'action' => 'Create', 'count' => count( $terms_to_create ) ], - [ 'action' => 'Delete', 'count' => count( $terms_to_delete ) ], + [ + 'action' => 'Create', + 'count' => count( $terms_to_create ), + ], + [ + 'action' => 'Delete', + 'count' => count( $terms_to_delete ), + ], ] ); return; } if ( 0 === $count ) { - \WP_CLI::success( esc_html__( 'Shadow Taxonomy is in sync, no action needed.' ) ); + \WP_CLI::success( esc_html__( 'Shadow Taxonomy is in sync, no action needed.', 'shadow-taxonomy' ) ); return; } /** * Process Shadow Taxonomy Additions and Deletions. - */ + */ \WP_CLI::log( sprintf( 'Processing %d items...', absint( $count ) ) ); foreach ( $terms_to_create as $post ) { @@ -243,11 +263,14 @@ public function sync_shadow_terms( $args, $assoc_args ) { * : The taxonomy name for the shadow relationship. * * @subcommand check + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function check_sync( $args, $assoc_args ) { if ( ! isset( $assoc_args['tax'] ) || ! taxonomy_exists( $assoc_args['tax'] ) ) { - \WP_CLI::error( esc_html__( 'Please provide a valid taxonomy using --tax.' ) ); + \WP_CLI::error( esc_html__( 'Please provide a valid taxonomy using --tax.', 'shadow-taxonomy' ) ); } if ( 'post_type' === $args[0] ) { @@ -306,6 +329,9 @@ public function check_sync( $args, $assoc_args ) { * : Allows you to see the number of shadow terms which need to be created or deleted. * * @subcommand sync-terms + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function migrate_shadow_terms( $args, $assoc_args ) { $tax = $assoc_args['tax']; @@ -320,7 +346,7 @@ public function migrate_shadow_terms( $args, $assoc_args ) { */ $posts = $this->get_posts_missing_shadow_meta( $cpt, $tax ); - $terms_to_create = []; + $terms_to_create = []; $posts_missing_metadata = []; foreach ( $posts as $post ) { @@ -353,14 +379,14 @@ public function migrate_shadow_terms( $args, $assoc_args ) { 'post_type' => $cpt, 'posts_per_page' => 1, 'post_status' => 'publish', - 'tax_query' => [ + 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Required to find posts by taxonomy term. [ 'taxonomy' => $tax, 'field' => 'id', 'terms' => $term->term_id, ], ], - 'no_found_rows' => true, + 'no_found_rows' => true, ] ); if ( empty( $term_query->posts ) || is_wp_error( $term_query ) ) { @@ -382,10 +408,22 @@ public function migrate_shadow_terms( $args, $assoc_args ) { */ if ( $dry_run ) { $this->output_dry_run_table( [ - [ 'action' => 'Create', 'count' => count( $terms_to_create ) ], - [ 'action' => 'Delete', 'count' => count( $terms_to_delete ) ], - [ 'action' => 'Missing Term Meta', 'count' => count( $terms_missing_metadata ) ], - [ 'action' => 'Missing Post Meta', 'count' => count( $posts_missing_metadata ) ], + [ + 'action' => 'Create', + 'count' => count( $terms_to_create ), + ], + [ + 'action' => 'Delete', + 'count' => count( $terms_to_delete ), + ], + [ + 'action' => 'Missing Term Meta', + 'count' => count( $terms_missing_metadata ), + ], + [ + 'action' => 'Missing Post Meta', + 'count' => count( $posts_missing_metadata ), + ], ] ); return; } @@ -394,7 +432,7 @@ public function migrate_shadow_terms( $args, $assoc_args ) { empty( $terms_to_delete ) && empty( $terms_missing_metadata ) && empty( $posts_missing_metadata ) ) { - \WP_CLI::success( esc_html__( 'Shadow Taxonomy is in sync, no action needed.' ) ); + \WP_CLI::success( esc_html__( 'Shadow Taxonomy is in sync, no action needed.', 'shadow-taxonomy' ) ); return; } @@ -460,6 +498,9 @@ public function migrate_shadow_terms( $args, $assoc_args ) { * : Allows you to see the number of shadow terms which need to be created or deleted. * * @subcommand deep-sync + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function deep_sync( $args, $assoc_args ) { $tax = $assoc_args['tax']; @@ -474,7 +515,7 @@ public function deep_sync( $args, $assoc_args ) { */ $posts = $this->get_posts_missing_shadow_meta( $cpt, $tax ); - $terms_to_create = array_filter( $posts, function( $post ) use ( $tax ) { + $terms_to_create = array_filter( $posts, function ( $post ) use ( $tax ) { $shadow_term = get_post_meta( $post->ID, Core\get_meta_key( $tax, 'term_id' ), true ); if ( ! empty( $shadow_term ) ) { @@ -493,13 +534,16 @@ public function deep_sync( $args, $assoc_args ) { */ if ( $dry_run ) { $this->output_dry_run_table( [ - [ 'action' => 'Create', 'count' => $count ], + [ + 'action' => 'Create', + 'count' => $count, + ], ] ); return; } if ( 0 === $count ) { - \WP_CLI::success( esc_html__( 'Shadow Taxonomy is in sync, no action needed.' ) ); + \WP_CLI::success( esc_html__( 'Shadow Taxonomy is in sync, no action needed.', 'shadow-taxonomy' ) ); return; } @@ -514,5 +558,4 @@ public function deep_sync( $args, $assoc_args ) { \WP_CLI::success( sprintf( 'Process Complete. Successfully synced %d posts and terms.', absint( $count ) ) ); } - } diff --git a/includes/shadow-taxonomy.php b/includes/shadow-taxonomy.php index 2cd3569..1eece9c 100644 --- a/includes/shadow-taxonomy.php +++ b/includes/shadow-taxonomy.php @@ -1,4 +1,12 @@ post_title, $taxonomy, [ 'slug' => $post->post_name ] ); @@ -154,11 +162,7 @@ function create_shadow_taxonomy_term( int $post_id, $post, string $taxonomy ) { * @return bool True if in sync, false otherwise. */ function post_type_already_in_sync( $term, $post ): bool { - if ( isset( $term->slug ) && isset( $post->post_name ) ) { - return $term->name === $post->post_title && $term->slug === $post->post_name; - } - - return $term->name === $post->post_title; + return $term->name === $post->post_title && $term->slug === $post->post_name; } /** @@ -179,11 +183,13 @@ function get_related_post_by_slug( $term, string $post_type ) { 'no_found_rows' => true, ] ); - if ( empty( $query->posts ) || is_wp_error( $query ) ) { + if ( empty( $query->posts ) ) { return false; } - return $query->posts[0]; + $post = $query->posts[0]; + + return $post instanceof \WP_Post ? $post : false; } /** @@ -200,12 +206,12 @@ function get_associated_post_id( $term ) { /** * Find the shadow or associated post for the input taxonomy term. * - * @param \WP_Term $term WP Term object. - * @param string $post_type Post Type slug. + * @param \WP_Term|false $term WP Term object, or false if not found. + * @param string $post_type Post Type slug. * * @return \WP_Post|false The associated post object, or false if not found. */ -function get_associated_post( $term, string $post_type ) { +function get_associated_post( $term, string $post_type ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Public API; $post_type kept for signature compatibility. if ( empty( $term ) ) { return false; } @@ -216,7 +222,9 @@ function get_associated_post( $term, string $post_type ) { return false; } - return get_post( $post_id ); + $post = get_post( $post_id ); + + return $post instanceof \WP_Post ? $post : false; } /** @@ -267,7 +275,7 @@ function get_the_posts( int $post_id, string $taxonomy, string $cpt ) { $terms = get_the_terms( $post_id, $taxonomy ); if ( ! empty( $terms ) ) { - $posts = array_filter( array_map( function( $term ) use ( $cpt ) { + $posts = array_filter( array_map( function ( $term ) use ( $cpt ) { return get_associated_post( $term, $cpt ); }, $terms ) ); @@ -280,10 +288,10 @@ function get_the_posts( int $post_id, string $taxonomy, string $cpt ) { /** * Helper function to register a shadow taxonomy and establish the relationship. * - * @param array $from_post_types Post types to register the taxonomy on (where checkboxes appear). - * @param array $to_post_types Post types that shadow terms will be created from. - * @param string $taxonomy The taxonomy slug to use for the registered connection. - * @param array $taxonomy_args Arguments to use for the registration of the shadow taxonomy. + * @param array $from_post_types Post types to register the taxonomy on (where checkboxes appear). + * @param array $to_post_types Post types that shadow terms will be created from. + * @param string $taxonomy The taxonomy slug to use for the registered connection. + * @param array $taxonomy_args Arguments to use for the registration of the shadow taxonomy. */ function register_shadow_taxonomy( array $from_post_types, array $to_post_types, string $taxonomy, array $taxonomy_args ): void { register_taxonomy( diff --git a/index.php b/index.php index dfa511d..f92ad87 100644 --- a/index.php +++ b/index.php @@ -8,6 +8,8 @@ * @package Shadow_Taxonomy */ +declare(strict_types=1); + namespace Shadow_Taxonomy; require_once __DIR__ . '/includes/shadow-taxonomy.php'; diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..a1fe3fe --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,57 @@ + + + PHPCS ruleset for Shadow Taxonomy library. + + + index.php + includes/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..e36fdd2 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,35 @@ +parameters: + level: 6 + paths: + - index.php + - includes/ + scanDirectories: + - vendor/ + bootstrapFiles: + - vendor/autoload.php + ignoreErrors: + # WP-CLI classes are not available in static analysis without dedicated stubs. + - + identifier: class.notFound + reportUnmatched: false + - + identifier: function.notFound + reportUnmatched: false + # WP_Query constructor never returns WP_Error, but the defensive check is intentional. + - + identifier: function.impossibleType + reportUnmatched: false + # WP-CLI command methods use standard array $args / array $assoc_args signatures. + - + identifier: missingType.iterableValue + reportUnmatched: false + # WP-CLI command methods have no meaningful return type (void in practice). + - + identifier: missingType.return + reportUnmatched: false + excludePaths: + analyseAndScan: + # WP-CLI bootstrap guard — file returns early when WP_CLI is not defined. + # PHPStan cannot resolve the WP_CLI_Command base class without stubs. + # The CLI file is still scanned for symbol discovery but not analysed. + - includes/shadow-taxonomy-cli.php