diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2207b31 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ + +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + php-tests: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Run PHP Tests in src directory + uses: alleyinteractive/action-test-php@develop + with: + skip-services: 'true' + wordpress-version: 'false' diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml deleted file mode 100644 index c336407..0000000 --- a/.github/workflows/coding-standards.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Coding Standards - -on: - push: - branches: - - main - pull_request: - schedule: - - cron: '0 0 * * *' - -jobs: - coding-standards: - uses: alleyinteractive/.github/.github/workflows/php-coding-standards.yml@main diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml deleted file mode 100644 index e293dd9..0000000 --- a/.github/workflows/unit-test.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Testing Suite - -on: - push: - branches: - - main - pull_request: - schedule: - - cron: '0 0 * * *' - -jobs: - unit-test: - uses: alleyinteractive/.github/.github/workflows/php-tests.yml@main diff --git a/.gitignore b/.gitignore index a854579..d2e965f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build vendor composer.lock node_modules +.phpunit.cache # Log files *.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 053e5df..5faece9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to Cache Collector will be documented in this file. +## 1.1.0 - 2024-07-16 + +- Fix type error when purging against an unknown term. + +## v1.0.1 - 2023-12-07 + +- Fix to register the post type on `init`. + +## v1.0.0 - 2023-12-02 + +- Initial release. + ## 0.1.1 - 2022-12-07 - Fix bug with post type registration being called before `init`. diff --git a/composer.json b/composer.json index 77b5bb4..d4d1d77 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,14 @@ ], "homepage": "https://github.com/alleyinteractive/cache-collector", "require": { - "php": "^8.0" + "php": "^8.1" }, "require-dev": { - "alleyinteractive/alley-coding-standards": "^1.0", + "alleyinteractive/alley-coding-standards": "^2.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", - "mantle-framework/testkit": "^0.9", - "nunomaduro/collision": "^5.0" + "mantle-framework/testkit": "^1.0", + "php-stubs/wp-cli-stubs": "^2.10", + "szepeviktor/phpstan-wordpress": "^1.3" }, "suggest": { "psr/log": "For logging messages to when purging the cache" @@ -46,9 +47,11 @@ "scripts": { "phpcbf": "phpcbf .", "phpcs": "phpcs .", + "phpstan": "phpstan --memory-limit=512M", "phpunit": "phpunit", "test": [ "@phpcs", + "@phpstan", "@phpunit" ] } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..1a2b4db --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,24 @@ +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + +parameters: + # Level 9 is the highest level + level: max + + paths: + - src/ + - plugin.php + + scanFiles: + - %rootDir%/../../php-stubs/wordpress-stubs/wordpress-stubs.php + - %rootDir%/../../php-stubs/wp-cli-stubs/wp-cli-stubs.php + - %rootDir%/../../php-stubs/wp-cli-stubs/wp-cli-commands-stubs.php + - %rootDir%/../../php-stubs/wp-cli-stubs/wp-cli-i18n-stubs.php + +# ignoreErrors: +# - '#PHPDoc tag @var#' +# +# excludePaths: +# - ./*/*/FileToBeExcluded.php +# +# checkMissingIterableValueType: false diff --git a/phpunit.xml b/phpunit.xml index 0595632..9dc4274 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,15 @@ + - - - tests - - + + + tests + + diff --git a/plugin.php b/plugin.php index 31baf4d..66761aa 100644 --- a/plugin.php +++ b/plugin.php @@ -3,11 +3,12 @@ * Plugin Name: cache-collector * Plugin URI: https://github.com/alleyinteractive/cache-collector * Description: Dynamic cache key collector for easy purging. - * Version: 0.1.0 + * Version: 1.1.0 * Author: Sean Fisher * Author URI: https://github.com/alleyinteractive/cache-collector * Requires at least: 5.9 - * Tested up to: 5.9 + * Requires PHP: 8.1 + * Tested up to: 6.6 * * @package cache-collector */ @@ -23,7 +24,7 @@ /** * Instantiate the plugin. */ -function cache_collector_setup() { +function cache_collector_setup(): void { add_action( 'init', [ Cache_Collector::class, 'register_post_type' ] ); // Register the post/term purge actions. diff --git a/src/class-cache-collector.php b/src/class-cache-collector.php index ea423c7..61fe624 100644 --- a/src/class-cache-collector.php +++ b/src/class-cache-collector.php @@ -56,54 +56,58 @@ class Cache_Collector { * * Array of arrays with the key and group as the values. * - * @var array> + * @var array> */ protected array $pending_keys = []; /** * Create a new Cache_Collector instance for a post. * - * @param WP_Post|int $post Post object/ID. - * @param array ...$args Arguments to pass to the constructor. - * @return static + * @param WP_Post|int $post Post object/ID. + * @param LoggerInterface|null $logger Logger to use. + * @return self * * @throws InvalidArgumentException If the post is invalid. */ - public static function for_post( WP_Post|int $post, array ...$args ): static { + public static function for_post( WP_Post|int $post, ?LoggerInterface $logger = null ): self { if ( is_numeric( $post ) ) { $post_id = $post; $post = get_post( $post ); if ( empty( $post ) ) { - throw new InvalidArgumentException( "Invalid post ID: {$post_id}" ); + throw new InvalidArgumentException( 'Invalid post ID: ' . ( (int) $post_id ) ); } } - return new static( "post-{$post->ID}", $post, ...$args ); + return new self( "post-{$post->ID}", $post, $logger ); } /** * Create a new Cache_Collector instance for a term. * - * @param WP_Term|int $term Term object/ID. - * @param array ...$args Arguments to pass to the constructor. - * @return static + * @param WP_Term|int $term Term object/ID. + * @param LoggerInterface|null $logger Logger to use. + * @return self * * @throws InvalidArgumentException If the term is invalid. */ - public static function for_term( WP_Term|int $term, array ...$args ) { + public static function for_term( WP_Term|int $term, ?LoggerInterface $logger = null ): self { if ( is_numeric( $term ) ) { $term_id = $term; $term = get_term( $term ); + if ( is_wp_error( $term ) ) { + throw new InvalidArgumentException( esc_html( 'Invalid term: ' . $term->get_error_message() ) ); + } + if ( empty( $term ) ) { - throw new InvalidArgumentException( "Invalid term ID: {$term_id}" ); + throw new InvalidArgumentException( 'Invalid term ID: ' . ( (int) $term_id ) ); } } - return new static( "term-{$term->term_id}", $term, ...$args ); + return new self( "term-{$term->term_id}", $term, $logger ); } /** @@ -111,11 +115,11 @@ public static function for_term( WP_Term|int $term, array ...$args ) { * * @param int $post_id Post ID. */ - public static function on_post_update( int $post_id ) { + public static function on_post_update( int $post_id ): void { $post = get_post( $post_id ); if ( $post ) { - static::for_post( $post )->purge(); + self::for_post( $post )->purge(); } } @@ -124,18 +128,18 @@ public static function on_post_update( int $post_id ) { * * @param int[] $ids Term ID. */ - public static function on_term_update( array $ids ) { + public static function on_term_update( array $ids ): void { foreach ( $ids as $id ) { - static::for_term( $id )->purge(); + self::for_term( $id )->purge(); } } /** * Register the post type for the cache collector. */ - public static function register_post_type() { + public static function register_post_type(): void { register_post_type( // phpcs:ignore WordPress.NamingConventions.ValidPostTypeSlug.NotStringLiteral - static::POST_TYPE, + self::POST_TYPE, [ 'public' => false, 'publicly_queryable' => false, @@ -150,7 +154,7 @@ public static function register_post_type() { * it is not older than the threshold, it will check if the keys in the * collection it is storing are expired. */ - public static function cleanup() { + public static function cleanup(): void { $page = 1; $limit = 100; @@ -177,7 +181,7 @@ public static function cleanup() { ], ], 'paged' => $page++, - 'post_type' => static::POST_TYPE, + 'post_type' => self::POST_TYPE, 'posts_per_page' => 100, 'suppress_filters' => false, ] @@ -195,7 +199,9 @@ public static function cleanup() { continue; } - ( new static( $collection, $post ) )->save(); + if ( is_string( $collection ) ) { + ( new self( $collection, $post ) )->save(); + } } } } @@ -241,10 +247,10 @@ public function register( string $key, string $group = '', int $ttl = 0, string } if ( ! in_array( $type, [ self::CACHE_OBJECT_CACHE, self::CACHE_TRANSIENT ], true ) ) { - throw new InvalidArgumentException( "Invalid cache type: {$type}." ); + throw new InvalidArgumentException( esc_html( "Invalid cache type: {$type}." ) ); } - $pending_key = $key . static::DELIMITER . $group; + $pending_key = $key . self::DELIMITER . $group; // Include the pending key for registration. if ( ! isset( $this->pending_keys[ $type ][ $pending_key ] ) ) { @@ -265,7 +271,7 @@ public function save() { $original = $storage; // Check if any of the existing keys are expired. - foreach ( [ static::CACHE_OBJECT_CACHE, static::CACHE_TRANSIENT ] as $type ) { + foreach ( [ self::CACHE_OBJECT_CACHE, self::CACHE_TRANSIENT ] as $type ) { if ( empty( $storage[ $type ] ) ) { continue; } @@ -280,7 +286,7 @@ public function save() { } // Append the pending keys for each cache type. - foreach ( [ static::CACHE_OBJECT_CACHE, static::CACHE_TRANSIENT ] as $type ) { + foreach ( [ self::CACHE_OBJECT_CACHE, self::CACHE_TRANSIENT ] as $type ) { if ( empty( $this->pending_keys[ $type ] ) ) { continue; } @@ -302,7 +308,7 @@ public function save() { } elseif ( empty( $storage ) ) { // Delete the parent object if there are no keys and if the parent // is a cache collection post. - if ( $this->parent instanceof WP_Post && static::POST_TYPE === $this->parent->post_type ) { + if ( $this->parent instanceof WP_Post && self::POST_TYPE === $this->parent->post_type ) { wp_delete_post( $this->parent->ID, true ); if ( $this->logger ) { @@ -336,7 +342,7 @@ public function keys(): array { foreach ( $storage as $type => $keys ) { $collection[ $type ] = array_map( - fn ( string $key ) => explode( static::DELIMITER, $key ), + fn ( string $key ) => explode( self::DELIMITER, $key ), array_keys( $keys ) ); } @@ -362,13 +368,13 @@ public function purge() { $dirty = false; - foreach ( [ static::CACHE_OBJECT_CACHE, static::CACHE_TRANSIENT ] as $type ) { + foreach ( [ self::CACHE_OBJECT_CACHE, self::CACHE_TRANSIENT ] as $type ) { if ( empty( $storage[ $type ] ) ) { continue; } foreach ( $storage[ $type ] as $index => $expiration ) { - [ $key, $cache_group ] = explode( static::DELIMITER, $index ); + [ $key, $cache_group ] = explode( self::DELIMITER, $index ); // Check if the key is expired and should be removed. if ( $expiration && $expiration < time() ) { @@ -381,9 +387,8 @@ public function purge() { // Purge the cache. $deleted = match ( $type ) { - static::CACHE_OBJECT_CACHE => wp_cache_delete( $key, $cache_group ), - static::CACHE_TRANSIENT => delete_transient( $key ), - default => false, + self::CACHE_OBJECT_CACHE => wp_cache_delete( $key, $cache_group ), + self::CACHE_TRANSIENT => delete_transient( $key ), }; if ( $this->logger ) { @@ -461,7 +466,7 @@ public function get_parent_object( bool $create = true ): WP_Post|WP_Term|null { 'name' => $this->get_storage_name(), 'no_found_rows' => true, 'post_status' => 'publish', - 'post_type' => static::POST_TYPE, + 'post_type' => self::POST_TYPE, 'posts_per_page' => 1, 'suppress_filters' => false, ] @@ -481,7 +486,7 @@ public function get_parent_object( bool $create = true ): WP_Post|WP_Term|null { 'post_name' => $this->get_storage_name(), 'post_status' => 'publish', 'post_title' => $this->get_storage_name(), - 'post_type' => static::POST_TYPE, + 'post_type' => self::POST_TYPE, ], true ); @@ -515,14 +520,13 @@ public function get_parent_object( bool $create = true ): WP_Post|WP_Term|null { * * Not intended for public API usage {@see Cache_Collector::keys()}. * - * @return array + * @return array> */ protected function get_storage(): array { if ( $this->parent ) { $keys = match ( $this->parent::class ) { - WP_Post::class => get_post_meta( $this->parent->ID, static::META_KEY, true ), - WP_Term::class => get_term_meta( $this->parent->term_id, static::META_KEY, true ), - default => [], + WP_Post::class => get_post_meta( $this->parent->ID, self::META_KEY, true ), + WP_Term::class => get_term_meta( $this->parent->term_id, self::META_KEY, true ), }; return is_array( $keys ) ? $keys : []; @@ -534,10 +538,10 @@ protected function get_storage(): array { /** * Store keys in the parent post/term. * - * @param array $keys The keys to store. + * @param array> $keys Keys to store. * @return void */ - protected function store_keys( array $keys ) { + protected function store_keys( array $keys ): void { $this->get_parent_object(); if ( ! $this->parent ) { @@ -545,8 +549,8 @@ protected function store_keys( array $keys ) { } match ( $this->parent::class ) { - WP_Post::class => update_post_meta( $this->parent->ID, static::META_KEY, $keys ), - WP_Term::class => update_term_meta( $this->parent->term_id, static::META_KEY, $keys ), + WP_Post::class => update_post_meta( $this->parent->ID, self::META_KEY, $keys ), + WP_Term::class => update_term_meta( $this->parent->term_id, self::META_KEY, $keys ), }; } } diff --git a/src/class-cli.php b/src/class-cli.php index 13bcf33..d562434 100644 --- a/src/class-cli.php +++ b/src/class-cli.php @@ -21,10 +21,9 @@ class CLI { * * : The name of the collection to purge. * - * @param array $args Positional arguments. - * @param array $assoc_args Associative arguments. + * @param array $args Positional arguments. */ - public function purge( $args, $assoc_args ) { + public function purge( $args ): void { [ $collection ] = $args; $instance = new Cache_Collector( $collection, function_exists( 'ai_logger' ) ? ai_logger() : null ); @@ -40,14 +39,13 @@ public function purge( $args, $assoc_args ) { * * : The ID of the post to purge. * - * @param array $args Positional arguments. - * @param array $assoc_args Associative arguments. + * @param array $args Positional arguments. */ - public function purge_post( $args, $assoc_args ) { + public function purge_post( $args ): void { [ $post ] = $args; try { - Cache_Collector::for_post( $post )->purge(); + Cache_Collector::for_post( (int) $post )->purge(); } catch ( Throwable $e ) { \WP_CLI::error( 'Error purging: ' . $e->getMessage() ); } @@ -61,14 +59,13 @@ public function purge_post( $args, $assoc_args ) { * * : The ID of the term to purge. * - * @param array $args Positional arguments. - * @param array $assoc_args Associative arguments. + * @param array $args Positional arguments. */ - public function purge_term( $args, $assoc_args ) { + public function purge_term( $args ): void { [ $term ] = $args; try { - Cache_Collector::for_term( $term )->purge(); + Cache_Collector::for_term( (int) $term )->purge(); } catch ( Throwable $e ) { \WP_CLI::error( 'Error purging: ' . $e->getMessage() ); } diff --git a/tests/test-cache-collector.php b/tests/CacheCollectorTest.php similarity index 99% rename from tests/test-cache-collector.php rename to tests/CacheCollectorTest.php index 514ce1a..90a1df5 100644 --- a/tests/test-cache-collector.php +++ b/tests/CacheCollectorTest.php @@ -7,7 +7,7 @@ /** * Visit {@see https://mantle.alley.co/testing/test-framework.html} to learn more. */ -class Cache_Collector_Test extends Test_Case { +class CacheCollectorTest extends Test_Case { public function test_register_key() { $instance = new Cache_Collector( __FUNCTION__ ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1d4cbc0..cb8fe4d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -9,4 +9,5 @@ \Mantle\Testing\manager() // Load the main file of the plugin. ->loaded( fn () => require_once __DIR__ . '/../plugin.php' ) + ->with_sqlite() ->install();