diff --git a/.travis.yml b/.travis.yml index 8162294..877d6b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ env: - WORDPRESS_DB_USER=wp - WORDPRESS_DB_PASS=password - WORDPRESS_DB_NAME=wp_tests - - WP_VERSION=4.9 + - WP_VERSION=5.2 - WP_MULTISITE=0 matrix: diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d781c..dd2db36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased PHP version >= 7.2 & WordPress version >= 5.1 will be required by version `2.0`. +## [1.7.0] - 2019-11-04 +### Added +- `RestApi\PostTypeFilter`, `Api\WpCacheTrait` & `Api\WpQueryTrait`. +- `PostTypeFilter` can be initiated to add `filter[]` querying to the rest endpoints +(enhanced rest filter of [WP-API/rest-filter](https://github.com/WP-API/rest-filter)) +- Default action/filter prefix constant in `TheFrosty\WpUtilities\Plugin\Plugin` called `TAG` +### Updated +- Add missing dependency "phpcompatibility" for require-dev. +- Bumped WordPress version to 5.2 in travis. + ## [1.6.2] - 2019-10-28 ### Updated - In the `BaseModel` class update the get method call to use the `getMethod` helper in `toArray`. diff --git a/bin/phpcs.sh b/bin/phpcs.sh index cc6a613..dce9b0f 100644 --- a/bin/phpcs.sh +++ b/bin/phpcs.sh @@ -18,7 +18,7 @@ fi commitFiles=`git diff --name-only $(git merge-base develop ${against})` -args="-s --colors --extensions=php --tab-width=4 --standard=phpcs.ruleset.xml --runtime-set testVersion 7.1-" +args="-s --colors --extensions=php --tab-width=4 --standard=phpcs-ruleset.xml --runtime-set testVersion 7.1-" phpFiles=""; phpFilesCount=0; for f in ${commitFiles}; do diff --git a/composer.json b/composer.json index 7a18d8e..61c77b7 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "thefrosty/wp-utilities", "description": "A library containing my standard development resources", - "version": "1.6.2", + "version": "1.7.0", "license": "MIT", "authors": [ { @@ -22,6 +22,7 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3", + "phpcompatibility/php-compatibility": "^9.3", "phpunit/phpunit": "^7", "phpmd/phpmd": "^2.6", "squizlabs/php_codesniffer": "^3.2", diff --git a/src/Api/WpCacheTrait.php b/src/Api/WpCacheTrait.php new file mode 100644 index 0000000..8491030 --- /dev/null +++ b/src/Api/WpCacheTrait.php @@ -0,0 +1,82 @@ +group; + } + + /** + * Optional. Set the cache group. + * + * @param string $group + */ + protected function setCacheGroup(string $group): void + { + $this->group = $group; + } + + /** + * Retrieve object from cache. + * + * @param string $key The key under which to store the value. + * @param string|null $group The group value appended to the $key. + * @param bool $force Optional. Whether to force an update of the local cache from the persistent + * cache. Default false. + * @param bool $found Optional. Whether the key was found in the cache. Disambiguates a return of false, + * a storable value. Passed by reference. Default null. + * + * @return mixed Cached object value. + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + protected function getCache(string $key, ?string $group = null, bool $force = false, ?bool &$found = null) + { + return \wp_cache_get($key, $group ?? $this->group, $force, $found); + } + + /** + * Sets a value in cache. + * + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string|null $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * + * @return bool Returns TRUE on success or FALSE on failure. + */ + protected function setCache(string $key, $value, ?string $group = '', int $expiration = 0): bool + { + return \wp_cache_set($key, $value, $group ?? $this->group, $expiration); + } +} diff --git a/src/Api/WpQueryTrait.php b/src/Api/WpQueryTrait.php new file mode 100644 index 0000000..69585d3 --- /dev/null +++ b/src/Api/WpQueryTrait.php @@ -0,0 +1,98 @@ + $post_type, + 'posts_per_page' => 1000, + 'post_status' => ['publish', 'pending', 'future', 'draft'], + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + ]; + + return new WP_Query(\wp_parse_args($args, $defaults)); + } + + /** + * Return a cached WP_Query object. + * + * @param string $post_type + * @param array $args Additional WP_Query parameters. + * @param int|null $expiration The expiration time, defaults to `MINUTE_IN_SECONDS`. + * + * @return WP_Query + */ + protected function wpQueryCached(string $post_type, array $args = [], ?int $expiration = null): WP_Query + { + $cache_key = $this->getHashedKey( + \sprintf('%s/query_%s_by_%s', Plugin::TAG, $post_type, \md5(\json_encode($args))) + ); + $query = $this->getCache($cache_key); + if ($query === false || !($query instanceof WP_Query)) { + $query = $this->wpQuery($post_type, $args); + if ($query->have_posts()) { + $this->setCache($cache_key, $query, $this->getCacheGroup(), $expiration ?? \MINUTE_IN_SECONDS); + \wp_reset_postdata(); + } + } + + return $query; + } + + /** + * Return an array of cached WP_Query post ID's. This will do a large loop to get *all* posts within + * the `$post_type`. So when you are aware of thousands of posts, and might need them all use this method. + * + * @param string $post_type + * @param array $args Additional WP_Query parameters. + * + * @return array An array of all post type IDs + */ + protected function wpQueryGetAllIds(string $post_type, array $args = []): array + { + static $paged; + $post_ids = []; + do { + $paged++; // phpcs:ignore + $defaults = [ + 'fields' => 'ids', + 'posts_per_page' => 100, + 'no_found_rows' => false, // We need pagination & the count for all posts found. + 'paged' => $paged, + 'update_post_term_cache' => false, + 'update_post_meta_cache' => false, + ]; + $query = $this->wpQueryCached($post_type, \wp_parse_args($args, $defaults)); + if ($query->have_posts()) { + foreach ($query->posts as $id) { + $post_ids[] = $id; + } + } + } while ($query->max_num_pages > $paged); + + return $post_ids; + } +} diff --git a/src/Plugin/Plugin.php b/src/Plugin/Plugin.php index 816816d..8311b97 100644 --- a/src/Plugin/Plugin.php +++ b/src/Plugin/Plugin.php @@ -11,4 +11,6 @@ class Plugin extends AbstractPlugin { use ContainerAwareTrait; + + public const TAG = 'thefrosty/wp_utilities'; } diff --git a/src/Plugin/PluginFactory.php b/src/Plugin/PluginFactory.php index a39c67a..11e0928 100644 --- a/src/Plugin/PluginFactory.php +++ b/src/Plugin/PluginFactory.php @@ -76,7 +76,7 @@ private static function setContainer(Plugin $plugin) : Plugin $plugin->setContainer(new Container()); } } catch (\InvalidArgumentException $exception) { - if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + if (\defined('WP_DEBUG_LOG') && \WP_DEBUG_LOG) { \error_log( \sprintf( '[DEBUG] The `Psr\Container\ContainerInterface` couldn\'t initiate. message: %s', diff --git a/src/RestApi/PostTypeFilter.php b/src/RestApi/PostTypeFilter.php new file mode 100644 index 0000000..a8e69b8 --- /dev/null +++ b/src/RestApi/PostTypeFilter.php @@ -0,0 +1,95 @@ +post_types = \get_post_types(['show_in_rest' => true], 'names'); + } + + /** + * Add class hooks. + */ + public function addHooks(): void + { + $this->addAction('rest_api_init', [$this, 'addRestFilters']); + } + + /** + * Allow the `filter[]` to REST responses for our post types. + * Taken from https://github.com/WP-API/rest-filter + */ + protected function addRestFilters(): void + { + $post_types = \array_filter( + \apply_filters(\sprintf('%/filter_post_types', Plugin::TAG), $this->post_types) + ); + \array_walk($post_types, function (string $slug): void { + $post_type = \get_post_type_object($slug); + if ($post_type instanceof \WP_Post_Type) { + $this->addFilter("rest_{$post_type->name}_query", [$this, 'addFilterParam'], 10, 2); + } + }); + } + + /** + * Add the `filter` parameter to the REST call query which is then passed to WP_Query. + * + * @see https://developer.wordpress.org/reference/classes/wp_query/ For available params to pass to the filter. + * + * @param array $args The query arguments. + * @param \WP_REST_Request $request Full details about the request. + * + * @return array $args. + */ + protected function addFilterParam(array $args, \WP_REST_Request $request): array + { + // Bail out if no filter parameter is set. + if (empty($request->get_params()) || empty($request->get_params()[self::QUERY_PARAM])) { + return $args; + } + $filter = $request->get_params()[self::QUERY_PARAM]; + if (isset($filter['posts_per_page']) && + ((int)$filter['posts_per_page'] >= 1 && + (int)$filter['posts_per_page'] <= 100) + ) { + $args['posts_per_page'] = $filter['posts_per_page']; + } + $vars = \apply_filters('rest_query_vars', $GLOBALS['wp']->public_query_vars); + // Allow valid meta query vars. + $vars = \array_unique(\array_merge($vars, ['meta_query', 'meta_key', 'meta_value', 'meta_compare'])); + foreach ($vars as $var) { + if (isset($filter[$var])) { + $args[$var] = $filter[$var]; + } + } + + return $args; + } +}