diff --git a/composer.lock b/composer.lock index 3b6b2606..9f65c357 100644 --- a/composer.lock +++ b/composer.lock @@ -1492,16 +1492,16 @@ }, { "name": "illuminate/collections", - "version": "v9.20.0", + "version": "v9.24.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "b53d26fbcfb623c4f7538eadd9bc5083e0a59bdd" + "reference": "f9eddfa8599dd224df618b08b2502720027d1f10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/b53d26fbcfb623c4f7538eadd9bc5083e0a59bdd", - "reference": "b53d26fbcfb623c4f7538eadd9bc5083e0a59bdd", + "url": "https://api.github.com/repos/illuminate/collections/zipball/f9eddfa8599dd224df618b08b2502720027d1f10", + "reference": "f9eddfa8599dd224df618b08b2502720027d1f10", "shasum": "" }, "require": { @@ -1543,20 +1543,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-07-01T20:41:17+00:00" + "time": "2022-08-09T13:26:41+00:00" }, { "name": "illuminate/conditionable", - "version": "v9.20.0", + "version": "v9.24.0", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", - "reference": "46b7beed47948bd2e67f523d0a76daa62775031e" + "reference": "5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/46b7beed47948bd2e67f523d0a76daa62775031e", - "reference": "46b7beed47948bd2e67f523d0a76daa62775031e", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883", + "reference": "5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883", "shasum": "" }, "require": { @@ -1589,20 +1589,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-05-31T14:47:50+00:00" + "time": "2022-07-29T19:44:19+00:00" }, { "name": "illuminate/contracts", - "version": "v9.20.0", + "version": "v9.24.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "e014cf88ef46065b8b1f078893c01189b95ffb11" + "reference": "ac7f63520e18721f214e80fa7e8f0a5c77ed2719" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/e014cf88ef46065b8b1f078893c01189b95ffb11", - "reference": "e014cf88ef46065b8b1f078893c01189b95ffb11", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/ac7f63520e18721f214e80fa7e8f0a5c77ed2719", + "reference": "ac7f63520e18721f214e80fa7e8f0a5c77ed2719", "shasum": "" }, "require": { @@ -1637,20 +1637,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-06-07T19:28:00+00:00" + "time": "2022-07-26T14:41:38+00:00" }, { "name": "illuminate/macroable", - "version": "v9.20.0", + "version": "v9.24.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", - "reference": "25a2c6dac2b7541ecbadef952702e84ae15f5354" + "reference": "e3bfaf6401742a9c6abca61b9b10e998e5b6449a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/macroable/zipball/25a2c6dac2b7541ecbadef952702e84ae15f5354", - "reference": "25a2c6dac2b7541ecbadef952702e84ae15f5354", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e3bfaf6401742a9c6abca61b9b10e998e5b6449a", + "reference": "e3bfaf6401742a9c6abca61b9b10e998e5b6449a", "shasum": "" }, "require": { @@ -1683,20 +1683,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-02-01T14:44:21+00:00" + "time": "2022-08-09T13:29:29+00:00" }, { "name": "illuminate/support", - "version": "v9.20.0", + "version": "v9.24.0", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "153993a1dfc8d1d5fb029b2f74a6df3c0712d89a" + "reference": "dc872a2a5a6ac8fe14431d7286f4ec4f70b6ac4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/153993a1dfc8d1d5fb029b2f74a6df3c0712d89a", - "reference": "153993a1dfc8d1d5fb029b2f74a6df3c0712d89a", + "url": "https://api.github.com/repos/illuminate/support/zipball/dc872a2a5a6ac8fe14431d7286f4ec4f70b6ac4b", + "reference": "dc872a2a5a6ac8fe14431d7286f4ec4f70b6ac4b", "shasum": "" }, "require": { @@ -1752,7 +1752,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-07-13T13:11:02+00:00" + "time": "2022-08-09T06:51:20+00:00" }, { "name": "justinrainbow/json-schema", @@ -1994,6 +1994,9 @@ "illuminate/support": ">=4.0.0", "php": ">=5.3.0" }, + "replace": { + "mikemclin/laravel-wp-password": "self.version" + }, "require-dev": { "mockery/mockery": "~0.9", "phpunit/phpunit": "~4.0", @@ -2151,16 +2154,16 @@ }, { "name": "nesbot/carbon", - "version": "2.59.1", + "version": "2.61.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "a9000603ea337c8df16cc41f8b6be95a65f4d0f5" + "reference": "bdf4f4fe3a3eac4de84dbec0738082a862c68ba6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/a9000603ea337c8df16cc41f8b6be95a65f4d0f5", - "reference": "a9000603ea337c8df16cc41f8b6be95a65f4d0f5", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bdf4f4fe3a3eac4de84dbec0738082a862c68ba6", + "reference": "bdf4f4fe3a3eac4de84dbec0738082a862c68ba6", "shasum": "" }, "require": { @@ -2249,7 +2252,7 @@ "type": "tidelift" } ], - "time": "2022-06-29T21:43:55+00:00" + "time": "2022-08-06T12:41:24+00:00" }, { "name": "nikic/php-parser", @@ -4807,16 +4810,16 @@ }, { "name": "symfony/browser-kit", - "version": "v5.4.3", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "18e73179c6a33d520de1b644941eba108dd811ad" + "reference": "081fe28a26b6bd671dea85ef3a4b5003f3c88027" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/18e73179c6a33d520de1b644941eba108dd811ad", - "reference": "18e73179c6a33d520de1b644941eba108dd811ad", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/081fe28a26b6bd671dea85ef3a4b5003f3c88027", + "reference": "081fe28a26b6bd671dea85ef3a4b5003f3c88027", "shasum": "" }, "require": { @@ -4859,7 +4862,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.4.3" + "source": "https://github.com/symfony/browser-kit/tree/v5.4.11" }, "funding": [ { @@ -4875,20 +4878,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2022-07-27T15:50:05+00:00" }, { "name": "symfony/console", - "version": "v5.4.10", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "4d671ab4ddac94ee439ea73649c69d9d200b5000" + "reference": "535846c7ee6bc4dd027ca0d93220601456734b10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/4d671ab4ddac94ee439ea73649c69d9d200b5000", - "reference": "4d671ab4ddac94ee439ea73649c69d9d200b5000", + "url": "https://api.github.com/repos/symfony/console/zipball/535846c7ee6bc4dd027ca0d93220601456734b10", + "reference": "535846c7ee6bc4dd027ca0d93220601456734b10", "shasum": "" }, "require": { @@ -4958,7 +4961,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.10" + "source": "https://github.com/symfony/console/tree/v5.4.11" }, "funding": [ { @@ -4974,20 +4977,20 @@ "type": "tidelift" } ], - "time": "2022-06-26T13:00:04+00:00" + "time": "2022-07-22T10:42:43+00:00" }, { "name": "symfony/css-selector", - "version": "v5.4.3", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b0a190285cd95cb019237851205b8140ef6e368e" + "reference": "c1681789f059ab756001052164726ae88512ae3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b0a190285cd95cb019237851205b8140ef6e368e", - "reference": "b0a190285cd95cb019237851205b8140ef6e368e", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/c1681789f059ab756001052164726ae88512ae3d", + "reference": "c1681789f059ab756001052164726ae88512ae3d", "shasum": "" }, "require": { @@ -5024,7 +5027,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.3" + "source": "https://github.com/symfony/css-selector/tree/v5.4.11" }, "funding": [ { @@ -5040,7 +5043,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2022-06-27T16:58:25+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5111,16 +5114,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.4.9", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "a213cbc80382320b0efdccdcdce232f191fafe3a" + "reference": "0b900ca5576ecd59e08c76127e616667cfe427a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/a213cbc80382320b0efdccdcdce232f191fafe3a", - "reference": "a213cbc80382320b0efdccdcdce232f191fafe3a", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/0b900ca5576ecd59e08c76127e616667cfe427a7", + "reference": "0b900ca5576ecd59e08c76127e616667cfe427a7", "shasum": "" }, "require": { @@ -5166,7 +5169,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.9" + "source": "https://github.com/symfony/dom-crawler/tree/v5.4.11" }, "funding": [ { @@ -5182,7 +5185,7 @@ "type": "tidelift" } ], - "time": "2022-05-04T14:46:32+00:00" + "time": "2022-06-27T16:58:25+00:00" }, { "name": "symfony/event-dispatcher", @@ -5350,16 +5353,16 @@ }, { "name": "symfony/finder", - "version": "v5.4.8", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9" + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9b630f3427f3ebe7cd346c277a1408b00249dad9", - "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9", + "url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c", + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c", "shasum": "" }, "require": { @@ -5393,7 +5396,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.8" + "source": "https://github.com/symfony/finder/tree/v5.4.11" }, "funding": [ { @@ -5409,7 +5412,7 @@ "type": "tidelift" } ], - "time": "2022-04-15T08:07:45+00:00" + "time": "2022-07-29T07:37:50+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5905,16 +5908,16 @@ }, { "name": "symfony/process", - "version": "v6.0.8", + "version": "v6.0.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d074154ea8b1443a96391f6e39f9e547b2dd01b9" + "reference": "44270a08ccb664143dede554ff1c00aaa2247a43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d074154ea8b1443a96391f6e39f9e547b2dd01b9", - "reference": "d074154ea8b1443a96391f6e39f9e547b2dd01b9", + "url": "https://api.github.com/repos/symfony/process/zipball/44270a08ccb664143dede554ff1c00aaa2247a43", + "reference": "44270a08ccb664143dede554ff1c00aaa2247a43", "shasum": "" }, "require": { @@ -5946,7 +5949,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.0.8" + "source": "https://github.com/symfony/process/tree/v6.0.11" }, "funding": [ { @@ -5962,7 +5965,7 @@ "type": "tidelift" } ], - "time": "2022-04-12T16:11:42+00:00" + "time": "2022-06-27T17:10:44+00:00" }, { "name": "symfony/service-contracts", @@ -6048,16 +6051,16 @@ }, { "name": "symfony/string", - "version": "v6.0.10", + "version": "v6.0.11", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "1b3adf02a0fc814bd9118d7fd68a097a599ebc27" + "reference": "042b6bf0f6ccca6d456a0572eb788cfb8b1ff809" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1b3adf02a0fc814bd9118d7fd68a097a599ebc27", - "reference": "1b3adf02a0fc814bd9118d7fd68a097a599ebc27", + "url": "https://api.github.com/repos/symfony/string/zipball/042b6bf0f6ccca6d456a0572eb788cfb8b1ff809", + "reference": "042b6bf0f6ccca6d456a0572eb788cfb8b1ff809", "shasum": "" }, "require": { @@ -6113,7 +6116,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.0.10" + "source": "https://github.com/symfony/string/tree/v6.0.11" }, "funding": [ { @@ -6129,20 +6132,20 @@ "type": "tidelift" } ], - "time": "2022-06-26T16:34:50+00:00" + "time": "2022-07-27T15:50:26+00:00" }, { "name": "symfony/translation", - "version": "v6.0.9", + "version": "v6.0.11", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "9ba011309943955a3807b8236c17cff3b88f67b6" + "reference": "55ffbe4b690156100af1ae42e1f94c5873085bca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/9ba011309943955a3807b8236c17cff3b88f67b6", - "reference": "9ba011309943955a3807b8236c17cff3b88f67b6", + "url": "https://api.github.com/repos/symfony/translation/zipball/55ffbe4b690156100af1ae42e1f94c5873085bca", + "reference": "55ffbe4b690156100af1ae42e1f94c5873085bca", "shasum": "" }, "require": { @@ -6208,7 +6211,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.0.9" + "source": "https://github.com/symfony/translation/tree/v6.0.11" }, "funding": [ { @@ -6224,7 +6227,7 @@ "type": "tidelift" } ], - "time": "2022-05-06T14:27:17+00:00" + "time": "2022-07-20T13:45:53+00:00" }, { "name": "symfony/translation-contracts", @@ -6306,16 +6309,16 @@ }, { "name": "symfony/yaml", - "version": "v5.4.10", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "04e42926429d9e8b39c174387ab990bf7817f7a2" + "reference": "05d4ea560f3402c6c116afd99fdc66e60eda227e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/04e42926429d9e8b39c174387ab990bf7817f7a2", - "reference": "04e42926429d9e8b39c174387ab990bf7817f7a2", + "url": "https://api.github.com/repos/symfony/yaml/zipball/05d4ea560f3402c6c116afd99fdc66e60eda227e", + "reference": "05d4ea560f3402c6c116afd99fdc66e60eda227e", "shasum": "" }, "require": { @@ -6361,7 +6364,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.10" + "source": "https://github.com/symfony/yaml/tree/v5.4.11" }, "funding": [ { @@ -6377,7 +6380,7 @@ "type": "tidelift" } ], - "time": "2022-06-20T11:50:59+00:00" + "time": "2022-06-27T16:58:25+00:00" }, { "name": "theseer/tokenizer", @@ -6667,16 +6670,16 @@ }, { "name": "wp-cli/php-cli-tools", - "version": "v0.11.13", + "version": "v0.11.14", "source": { "type": "git", "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "a2866855ac1abc53005c102e901553ad5772dc04" + "reference": "f8f340e4a87687549d046e2da516242f7f36c934" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/a2866855ac1abc53005c102e901553ad5772dc04", - "reference": "a2866855ac1abc53005c102e901553ad5772dc04", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/f8f340e4a87687549d046e2da516242f7f36c934", + "reference": "f8f340e4a87687549d046e2da516242f7f36c934", "shasum": "" }, "require": { @@ -6715,9 +6718,9 @@ ], "support": { "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.13" + "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.14" }, - "time": "2021-07-01T15:08:16+00:00" + "time": "2022-07-04T21:44:34+00:00" }, { "name": "wp-cli/wp-cli", @@ -6972,5 +6975,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.0.0" } diff --git a/pr152-wp-graphql-smart-cache.zip b/pr152-wp-graphql-smart-cache.zip new file mode 100644 index 00000000..6daf2cdf Binary files /dev/null and b/pr152-wp-graphql-smart-cache.zip differ diff --git a/src/Admin/Settings.php b/src/Admin/Settings.php index bfa707b2..ade9c5ab 100644 --- a/src/Admin/Settings.php +++ b/src/Admin/Settings.php @@ -68,10 +68,6 @@ function () { if ( $current_setting !== $value ) { // Action for those listening to purge_all do_action( 'wpgraphql_cache_purge_all' ); - - // Purge the local cache results if enabled - $cache_object = new Results(); - $cache_object->purge_all(); } } return $value; @@ -180,10 +176,7 @@ function () { // Trigger action when cache purge_all is invoked do_action( 'wpgraphql_cache_purge_all' ); - $cache_object = new Results(); - if ( true === $cache_object->purge_all() ) { - return gmdate( 'D, d M Y H:i T' ); - } + return gmdate( 'D, d M Y H:i T' ); } return $existing_purge_all_time; diff --git a/src/Cache/Collection.php b/src/Cache/Collection.php index 9436bae0..5a82d73a 100644 --- a/src/Cache/Collection.php +++ b/src/Cache/Collection.php @@ -66,7 +66,7 @@ class Collection extends Query { // initialize the cache collection public function init() { - add_action( 'graphql_return_response', [ $this, 'save_query_mapping_cb' ], 10, 7 ); + add_action( 'graphql_return_response', [ $this, 'save_query_mapping_cb' ], 10, 8 ); add_filter( 'pre_graphql_execute_request', [ $this, 'before_executing_query_cb' ], 10, 2 ); add_filter( 'graphql_dataloader_get_model', [ $this, 'data_loaded_process_cb' ], 10, 1 ); @@ -343,17 +343,6 @@ public function node_key( $id ) { return 'node:' . $id; } - /** - * When save or retrieve urls for a specific Unique identifier for this request for use in the collection map - * - * @param string $id Id for the node - * - * @return string unique id for this request - */ - public function url_key( $id ) { - return 'url:' . $id; - } - /** * @param string $key The identifier to the list * @param string $content to add @@ -381,18 +370,6 @@ public function retrieve_nodes( $id ) { return $this->get( $key ); } - /** - * Get the list of urls associated with the content/node/list id - * - * @param mixed|string|int $id The content node identifier - * - * @return array The unique list of content stored - */ - public function retrieve_urls( $id ) { - $key = $this->url_key( $id ); - return $this->get( $key ); - } - /** * When a query response is being returned to the client, build map for each item and this * query/queryId That way we will know what to invalidate on data change. @@ -405,7 +382,8 @@ public function retrieve_urls( $id ) { * @param string $operation The name of the Operation * @param string $query The query string * @param array $variables The variables for the query - * @param Request The WPGraphQL Request object + * @param Request $request The WPGraphQL Request object + * @param string|null $query_id The query id that GraphQL executed * * @return void */ @@ -416,34 +394,19 @@ public function save_query_mapping_cb( $operation, $query, $variables, - $request + $request, + $query_id ) { - $request_key = $this->build_key( $request->params->queryId, $request->params->query, $request->params->variables, $request->params->operation ); + $request_key = $this->build_key( $query_id, $query, $variables, $operation ); - // Only store mappings of urls when it's a GET request - $map_the_url = false; - if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' === $_SERVER['REQUEST_METHOD'] ) { - $map_the_url = true; - } - - // We don't want POSTs during mutations or nothing on the url. cause it'll purge /graphql* - if ( $map_the_url && ! empty( $_SERVER['REQUEST_URI'] ) ) { - //phpcs:ignore - $url_to_save = wp_unslash( $_SERVER['REQUEST_URI'] ); - - // Save the url this query request came in on, so we can purge it later when something changes - $urls = $this->store_content( $this->url_key( $request_key ), $url_to_save ); - - //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r - error_log( "Graphql Save Urls: $request_key " . print_r( $urls, 1 ) ); - } + do_action( 'wpgraphql_cache_save_request', $request_key, $query_id, $query, $variables, $operation, $this->runtime_nodes, $this->list_types ); // Save/add the node ids for this query. When one of these change in the future, we can purge the query foreach ( $this->runtime_nodes as $node_id ) { $this->store_content( $this->node_key( $node_id ), $request_key ); } - // For each connection resolver, store the url key + // For each connection resolver, store the list types associated with this graphql query request if ( ! empty( $this->list_types ) && is_array( $this->list_types ) ) { $this->list_types = array_unique( $this->list_types ); foreach ( $this->list_types as $type_name ) { diff --git a/src/Cache/Query.php b/src/Cache/Query.php index 9394025a..21087ed2 100644 --- a/src/Cache/Query.php +++ b/src/Cache/Query.php @@ -56,9 +56,9 @@ public function build_key( $query_id, $query, $variables = null, $operation = nu $user = wp_get_current_user(); $parts = [ - 'query' => $query, - 'variables' => $variables, - 'operation' => $operation, + 'query' => $query ?: null, + 'variables' => $variables ?: null, + 'operation' => $operation ?: null, 'user' => $user->ID, ]; diff --git a/src/Cache/Results.php b/src/Cache/Results.php index 56dcb6ed..e7cbf88a 100644 --- a/src/Cache/Results.php +++ b/src/Cache/Results.php @@ -13,26 +13,18 @@ class Results extends Query { const GLOBAL_DEFAULT_TTL = 600; /** - * The cache key for the executed GraphQL Document + * Indicator of the GraphQL Query execution cached or not. * - * @var string + * @array bool */ - protected $cache_key = ''; - - /** - * The cached response of a GraphQL Query execution. False if it doesn't exist. - * - * @var mixed|bool|array|object - */ - protected $cached_result; + protected $is_cached = []; public function init() { - $this->cached_result = false; - add_filter( 'pre_graphql_execute_request', [ $this, 'get_query_results_from_cache_cb' ], 10, 2 ); - add_action( 'graphql_return_response', [ $this, 'save_query_results_to_cache_cb' ], 10, 7 ); + add_action( 'graphql_return_response', [ $this, 'save_query_results_to_cache_cb' ], 10, 8 ); add_action( 'wpgraphql_cache_purge_nodes', [ $this, 'purge_nodes_cb' ], 10, 2 ); - add_filter( 'graphql_request_results', [ $this, 'add_cache_key_to_response_extensions' ], 10, 1 ); + add_action( 'wpgraphql_cache_purge_all', [ $this, 'purge_all_cb' ], 10, 0 ); + add_filter( 'graphql_request_results', [ $this, 'add_cache_key_to_response_extensions' ], 10, 7 ); parent::init(); } @@ -47,9 +39,8 @@ public function init() { * * @return string|false unique id for this request or false if query not provided */ - public function the_results_key( $query_id, $query, $variables = null, $operation = null ) { - $this->cache_key = $this->build_key( $query_id, $query, $variables, $operation ); - return $this->cache_key; + public function the_results_key( $query_id, $query, $variables = null, $operation_name = null ) { + return $this->build_key( $query_id, $query, $variables, $operation_name ); } @@ -57,24 +48,41 @@ public function the_results_key( $query_id, $query, $variables = null, $operatio * Add a message to the extensions when a GraphQL request is returned from the GraphQL Object Cache * * @param mixed|array|object $response The response of the GraphQL Request + * @param WPSchema $schema The schema object for the root query + * @param string $operation The name of the operation + * @param string $query The query that GraphQL executed + * @param array|null $variables Variables to passed to your GraphQL request + * @param Request $request Instance of the Request + * @param string|null $query_id The query id that GraphQL executed * * @return array|mixed */ - public function add_cache_key_to_response_extensions( $response ) { - $message = []; - - // if there's no cache key, or there is no cached_result return the response as-is - if ( ! empty( $this->cache_key ) && ! empty( $this->cached_result ) ) { - $message = [ - 'message' => __( 'This response was not executed at run-time but has been returned from the GraphQL Object Cache', 'wp-graphql-smart-cache' ), - 'cacheKey' => $this->cache_key, - ]; - } + public function add_cache_key_to_response_extensions( + $response, + $schema, + $operation_name, + $query_string, + $variables, + $request, + $query_id + ) { + $key = $this->the_results_key( $query_id, $query_string, $variables, $operation_name ); + if ( $key ) { + $message = []; + + // If we know that the results were pulled from cache, add messaging + if ( isset( $this->is_cached[ $key ] ) && true === $this->is_cached[ $key ] ) { + $message = [ + 'message' => __( 'This response was not executed at run-time but has been returned from the GraphQL Object Cache', 'wp-graphql-smart-cache' ), + 'cacheKey' => $key, + ]; + } - if ( is_array( $response ) ) { - $response['extensions']['graphqlSmartCache']['graphqlObjectCache'] = $message; - } if ( is_object( $response ) ) { - $response->extensions['graphqlSmartCache']['graphqlObjectCache'] = $message; + if ( is_array( $response ) ) { + $response['extensions']['graphqlSmartCache']['graphqlObjectCache'] = $message; + } if ( is_object( $response ) ) { + $response->extensions['graphqlSmartCache']['graphqlObjectCache'] = $message; + } } // return the modified response with the graphqlSmartCache message in the extensions output @@ -91,30 +99,72 @@ public function add_cache_key_to_response_extensions( $response ) { * @return mixed|array|object|null The response or null if not found in cache */ public function get_query_results_from_cache_cb( $result, $request ) { - // if caching is not enabled or the request is authenticated, bail early // right now we're not supporting GraphQL cache for authenticated requests. // Possibly in the future. if ( ! Settings::caching_enabled() || is_user_logged_in() ) { return $result; } - $key = $this->the_results_key( $request->params->queryId, $request->params->query, $request->params->variables, $request->params->operation ); + + // Loop over each request and load the response. If any one are empty, not in cache, return so all get reloaded. + if ( is_array( $request->params ) ) { + $result = []; + foreach ( $request->params as $req ) { + //phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $response = $this->get_result( $req->queryId, $req->query, $req->variables, $req->operation ); + // If any one is null, return all are null. + if ( null === $response ) { + return null; + } + $result[] = $response; + } + } else { + //phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $result = $this->get_result( $request->params->queryId, $request->params->query, $request->params->variables, $request->params->operation ); + } + return $result; + } + + /** + * Unique identifier for this request is normalized query string, operation and variables + * + * @param string $query_id queryId from the graphql query request + * @param string $query query string + * @param array $variables Variables sent with request or null + * @param string $operation Name of operation if specified on the request or null + * + * @return string|null The response or null if not found in cache + */ + public function get_result( $query_id, $query_string, $variables, $operation_name ) { + $key = $this->the_results_key( $query_id, $query_string, $variables, $operation_name ); if ( ! $key ) { return null; } - $this->cached_result = $this->get( $key ); + $result = $this->get( $key ); + if ( false === $result ) { + return null; + } + + $this->is_cached[ $key ] = true; - return ( false === $this->cached_result ) ? null : $this->cached_result; + return $result; } /** * When a query response is being returned to the client, build map for each item and this query/queryId * That way we will know what to invalidate on data change. * - * @param $filtered_response GraphQL\Executor\ExecutionResult - * @param $response GraphQL\Executor\ExecutionResult - * @param $request WPGraphQL\Request + * @param ExecutionResult $filtered_response The response after GraphQL Execution has been + * completed and passed through filters + * @param ExecutionResult $response The raw, unfiltered response of the GraphQL + * Execution + * @param Schema $schema The WPGraphQL Schema + * @param string $operation The name of the Operation + * @param string $query The query string + * @param array $variables The variables for the query + * @param Request $request The WPGraphQL Request object + * @param string|null $query_id The query id that GraphQL executed * * @return void */ @@ -122,10 +172,11 @@ public function save_query_results_to_cache_cb( $filtered_response, $response, $schema, - $operation, + $operation_name, $query, $variables, - $request + $request, + $query_id ) { // if caching is not enabled or the request is authenticated, bail early // right now we're not supporting GraphQL cache for authenticated requests. @@ -134,7 +185,7 @@ public function save_query_results_to_cache_cb( return; } - $key = $this->the_results_key( $request->params->queryId, $request->params->query, $request->params->variables, $request->params->operation ); + $key = $this->the_results_key( $query_id, $query, $variables, $operation_name ); if ( ! $key ) { return; } @@ -145,7 +196,7 @@ public function save_query_results_to_cache_cb( if ( false === $cached_result ) { $expiration = \get_graphql_setting( 'global_ttl', self::GLOBAL_DEFAULT_TTL, 'graphql_cache_section' ); - $this->save( $key, $response, $expiration ); + $this->save( $key, $filtered_response, $expiration ); } } @@ -177,4 +228,11 @@ public function purge_nodes_cb( $id, $nodes ) { graphql_debug( 'Graphql delete nodes', [ 'nodes' => $nodes ] ); } } + + /** + * Purge the local cache results if enabled + */ + public function purge_all_cb() { + $this->purge_all(); + } } diff --git a/src/Document.php b/src/Document.php index ded5c8f5..b481d639 100644 --- a/src/Document.php +++ b/src/Document.php @@ -19,6 +19,7 @@ class Document { public function init() { add_filter( 'graphql_request_data', [ $this, 'graphql_query_contains_queryid_cb' ], 10, 2 ); + add_filter( 'graphql_execute_query_params', [ $this, 'graphql_execute_query_params_cb' ], 10, 2 ); add_action( 'post_updated', [ $this, 'after_updated_cb' ], 10, 3 ); @@ -174,6 +175,26 @@ public function graphql_query_contains_queryid_cb( $parsed_body_params, $request return $parsed_body_params; } + /** + * During invoking 'graphql()', not as an http request, if queryId is present, look it up and return the query string + * + * @param string $query The graphql query sring. + * @param mixed|array|OperationParams| $params The graphql request params, containing queryId + */ + public function graphql_execute_query_params_cb( $query, $params ) { + if ( empty( $query ) ) { + //phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + if ( isset( $params->queryId ) ) { + //phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $query_id = $params->queryId; + } elseif ( isset( $params['queryId'] ) ) { + $query_id = $params['queryId']; + } + $query = $this->get( $query_id ); + } + return $query; + } + /** * If existing post is edited, verify query string in content is valid graphql * diff --git a/tests/functional/BatchQueryCest.php b/tests/functional/BatchQueryCest.php new file mode 100644 index 00000000..92f5ce92 --- /dev/null +++ b/tests/functional/BatchQueryCest.php @@ -0,0 +1,81 @@ +query_alias = uniqid( "savedquery_posts_" ); + $query_string = sprintf( "query %s { posts { nodes { id title } } }", $this->query_alias ); + + $I->sendPost('graphql', [ + 'query' => $query_string, + 'queryId' =>$this->query_alias + ] ); + + // Create a published post for our queries + $I->havePostInDatabase( [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'foo', + 'post_content' => 'foo bar. biz bang.', + 'post_name' => 'foo-slug', + ] ); + + // Enable the local cache transient cache for these tests + $I->haveOptionInDatabase( 'graphql_cache_section', [ 'cache_toggle' => 'on' ] ); + } + + public function _after( FunctionalTester $I ) { + // Make sure that is gone. + $I->dontHavePostInDatabase(['post_title' => 'foo']); + + // clean up and persisted queries terms in the taxonomy + $I->dontHavePostInDatabase( [ 'post_type' => 'graphql_document' ] ); + $I->dontHaveTermInDatabase( [ 'taxonomy' => 'graphql_query_alias'] ); + + $I->dontHaveOptionInDatabase( 'graphql_cache_section' ); + + } + + public function testBatchQueryIsCached( FunctionalTester $I ) { + // Test saved/persisted query. + $query_string = sprintf( "query %s { posts { nodes { uri id databaseId } } }", $this->query_alias ); + + // Initial queries should not come from cache. + // Use individual queries here as an example that they are the same as when batched. + $I->sendGet('graphql', [ 'queryId' => $this->query_alias ] ); + $I->seeResponseContainsJson( [ + 'extensions' => [ + 'graphqlSmartCache' => [ + 'graphqlObjectCache' => [] + ] + ] + ]); + $I->sendGet('graphql', [ 'query' => $query_string ] ); + $I->seeResponseContainsJson( [ + 'extensions' => [ + 'graphqlSmartCache' => [ + 'graphqlObjectCache' => [] + ] + ] + ]); + + // Batch queries, reusing query id to prove caching works + $query = + [ + [ "queryId" => $this->query_alias ], + [ "query" => $query_string ], + ] + ; + + $I->sendPost('graphql', $query ); + + $response = json_decode( $I->grabResponse(), 1 ); + codecept_debug( $response ); + $I->assertEquals( "This response was not executed at run-time but has been returned from the GraphQL Object Cache", $response[0]['extensions']['graphqlSmartCache']['graphqlObjectCache']['message'] ); + $I->assertEquals( "This response was not executed at run-time but has been returned from the GraphQL Object Cache", $response[1]['extensions']['graphqlSmartCache']['graphqlObjectCache']['message'] ); + } +} diff --git a/tests/functional/CacheCollectionPostCest.php b/tests/functional/CacheCollectionPostCest.php deleted file mode 100644 index d6a41ecc..00000000 --- a/tests/functional/CacheCollectionPostCest.php +++ /dev/null @@ -1,132 +0,0 @@ -grabColumnFromDatabase( 'wp_options', 'option_name', [ 'option_name like' => '_transient_gql_cache_%' ] ); - foreach( $transients as $transient ) { - $I->dontHaveOptionInDatabase( $transient ); - } - } - - public function queryContentPostTest( FunctionalTester $I ) { - $I->wantTo( 'Execute a graphql query and verify post nodes are in memory for my url' ); - - $I->havePostInDatabase( [ - 'post_type' => 'post', - 'post_status' => 'publish', - 'post_title' => 'foo', - 'post_content' => 'foo bar. biz bang.', - 'post_name' => 'foo-slug', - ] ); - - $I->sendGet( 'graphql', [ 'query' => '{ posts { nodes { id title content } } }' ] ); - $I->seeResponseContainsJson( [ - 'data' => [ - 'posts' => [ - 'nodes' => [ - 'title' => 'foo' - ] - ] - ] - ]); - - $post_id = $I->grabDataFromResponseByJsonPath("$.data.posts.nodes[*].id")[0]; - codecept_debug( $post_id ); - - // Get the stored information for the post node after we ran the graphql GET query. - // Verify the stored url matches our GET request - // NOTE: the node is stored with the model class as a prefix - $transient_name = "_transient_gql_cache_node:WPGraphQL\Model\Post:$post_id"; - $query_key = unserialize( $I->grabFromDatabase( 'wp_options', 'option_value', [ 'option_name' => $transient_name ] ) ); - $query_key = $query_key[0]; - codecept_debug( $query_key ); - - // Now take that value of the query request hash and look up the urls for that query - // Example '_transient_gql_cache_url:10756d547c7be4686f65c2980cf4b3be4936c2b0c95eb6bdcf0a4668fc5ce5b3'; - $transient_name = "_transient_gql_cache_url:$query_key"; - $urls = unserialize( $I->grabFromDatabase( 'wp_options', 'option_value', [ 'option_name' => $transient_name ] ) ); - $url = $urls[0]; - codecept_debug( $url ); - - // This is what the url looks like for the query. Should be stored in the collection map - $expected_url = '/graphql?query=%7B+posts+%7B+nodes+%7B+id+title+content+%7D+%7D+%7D'; - $I->assertEquals($expected_url, $url); - - // clean up - $I->dontHavePostInDatabase( ['post_name' => 'foo-slug'] ); - - $query_key = unserialize( $I->grabFromDatabase( 'wp_options', 'option_value', [ 'option_name like' => '_transient_gql_cache_%' ] ) ); - codecept_debug( $query_key ); - - } - - public function queryContentAuthorTest( FunctionalTester $I ) { - $I->wantTo( 'Execute a graphql query and verify author nodes are in memory for my url' ); - - $userId = $I->haveUserInDatabase( - 'tester', - 'editor', - [ - 'meta' => [ - 'first_name' => 'Lavender', - 'last_name' => 'Gooms' - ] - ] - ); - - $I->havePostInDatabase( [ - 'post_type' => 'post', - 'post_status' => 'publish', - 'post_title' => 'foo', - 'post_content' => 'foo bar. biz bang.', - 'post_name' => 'foo-slug', - 'post_author' => $userId, - ] ); - - $I->sendGet( 'graphql', [ 'query' => '{ posts { nodes { id title authorId author { node { firstName lastName } } } } }' ] ); - - $I->seeResponseContainsJson( [ - 'data' => [ - 'posts' => [ - 'nodes' => [ - 'author' => [ - 'node' => [ - 'firstName' => 'Lavender', - 'lastName' => 'Gooms', - ] - ] - ] - ] - ] - ]); - - $author_id = $I->grabDataFromResponseByJsonPath("$.data.posts.nodes[*].authorId")[0]; - codecept_debug( $author_id ); - - // Get the stored information for the node after we ran the graphql GET query. - // Verify the stored url matches our GET request - $transient_name = "_transient_gql_cache_node:WPGraphQL\Model\User:$author_id"; - $query_key = unserialize( $I->grabFromDatabase( 'wp_options', 'option_value', [ 'option_name' => $transient_name ] ) ); - $query_key = $query_key[0]; - codecept_debug( $query_key ); - - // Now take that value of the query request hash and look up the urls for that query - // Example '_transient_gql_cache_url:10756d547c7be4686f65c2980cf4b3be4936c2b0c95eb6bdcf0a4668fc5ce5b3'; - $transient_name = "_transient_gql_cache_url:$query_key"; - $urls = unserialize( $I->grabFromDatabase( 'wp_options', 'option_value', [ 'option_name' => $transient_name ] ) ); - $url = $urls[0]; - codecept_debug( $url ); - - // This is what the url looks like for the query. Should be stored in the collection map - $expected_url = '/graphql?query=%7B+posts+%7B+nodes+%7B+id+title+authorId+author+%7B+node+%7B+firstName+lastName+%7D+%7D+%7D+%7D+%7D'; - $I->assertEquals($expected_url, $url); - - // clean up - $I->dontHavePostInDatabase( ['post_name' => 'foo-slug'] ); - $I->dontHaveUserInDatabase( 'tester' ); - } -} diff --git a/tests/functional/DocumentCest.php b/tests/functional/DocumentCest.php index 414e5862..6fd9387a 100644 --- a/tests/functional/DocumentCest.php +++ b/tests/functional/DocumentCest.php @@ -7,7 +7,7 @@ public function _before( FunctionalTester $I ) { // clean up and persisted queries terms in the taxonomy $I->dontHavePostInDatabase( [ 'post_type' => 'graphql_document' ] ); - $I->dontHaveTermInDatabase( ['taxonomy' => 'graphql_query_alias'] ); + $I->dontHaveTermInDatabase( [ 'taxonomy' => 'graphql_query_alias'] ); $I->dontHaveOptionInDatabase( 'graphql_persisted_queries_section' ); } diff --git a/tests/wpunit/BatchQueryTest.php b/tests/wpunit/BatchQueryTest.php new file mode 100644 index 00000000..44aed483 --- /dev/null +++ b/tests/wpunit/BatchQueryTest.php @@ -0,0 +1,82 @@ +query_alias = uniqid( "query_posts_" ); + $query_string = sprintf( "query %s { posts { nodes { id title } } }", $this->query_alias ); + + $saved_query = new Document(); + $this->created_post_id = $saved_query->save( $this->query_alias, $query_string ); + codecept_debug( "$this->created_post_id, $this->query_alias, $query_string" ); + } + + public function _after() { + delete_option( 'graphql_cache_section' ); + wp_delete_post( $this->created_post_id ); + } + + public function testBatchQueryIsCached() { + // Enable the local cache transient cache for these tests + add_option( 'graphql_cache_section', [ 'cache_toggle' => 'on' ] ); + + // Create a published post for our queries + self::factory()->post->create( [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'foo', + 'post_content' => 'foo bar. biz bang.', + 'post_name' => 'foo-slug', + ] ); + + // Test saved/persisted query. + $query_string = sprintf( "query %s { posts { nodes { uri id databaseId } } }", $this->query_alias ); + $query = + [ + [ "queryId" => $this->query_alias ], + [ "query" => $query_string ], + [ "queryId" => $this->query_alias ] + ] + ; + + $response_data = graphql( $query ); + codecept_debug( $response_data ); + + $this->assertEquals( 'foo', $response_data[0]['data']['posts']['nodes'][0]['title'] ); + $this->assertEquals( $response_data[0]['data']['posts']['nodes'][0]['id'], $response_data[1]['data']['posts']['nodes'][0]['id'] ); + $this->assertEquals( 'foo', $response_data[2]['data']['posts']['nodes'][0]['title'] ); + + // Verify the smart cache debug return data + $results_cache = new Results(); + $this->assertEmpty( $response_data[0]['extensions']['graphqlSmartCache']['graphqlObjectCache'] ); + $this->assertEmpty( $response_data[1]['extensions']['graphqlSmartCache']['graphqlObjectCache'] ); + $this->assertEquals( $results_cache->the_results_key( $this->query_alias, null ), $response_data[2]['extensions']['graphqlSmartCache']['graphqlObjectCache']['cacheKey'] ); + + // After the batch query, each query should have an entry saved in transient results cache. + $key = $results_cache->the_results_key( $this->query_alias, null ); + $query_alias_cache = $results_cache->get( $key ); + codecept_debug( "Cache for $key $this->query_alias"); + codecept_debug( $query_alias_cache ); + $this->assertEquals( 'foo', $query_alias_cache['data']['posts']['nodes'][0]['title'] ); + + $key = $results_cache->the_results_key( null, $query_string ); + $query_string_cache = $results_cache->get( $key ); + codecept_debug( "Cache for $key $query_string"); + codecept_debug( $query_string_cache ); + $this->assertEquals( $query_alias_cache['data']['posts']['nodes'][0]['id'], $query_string_cache['data']['posts']['nodes'][0]['id'] ); + } + +} diff --git a/tests/wpunit/CacheInvalidationTest.php b/tests/wpunit/CacheInvalidationTest.php index f4ffdaef..79d86110 100644 --- a/tests/wpunit/CacheInvalidationTest.php +++ b/tests/wpunit/CacheInvalidationTest.php @@ -30,7 +30,6 @@ public function tearDown(): void { parent::tearDown(); } - public function testNonNullListOfNonNullPostMapsToListOfPosts() { register_graphql_field( 'RootQuery', 'listOfThing', [ diff --git a/tests/wpunit/CachedQueryTest.php b/tests/wpunit/CachedQueryTest.php index a71b5638..bdf81877 100644 --- a/tests/wpunit/CachedQueryTest.php +++ b/tests/wpunit/CachedQueryTest.php @@ -247,4 +247,36 @@ public function testExpirationTtlIsSetForCachedResults() { $this->assertLessThanOrEqual( $time_after + 30, $transient_timeout_option ); } + public function testPurgeAllCacheAction() { + add_option( 'graphql_cache_section', [ 'cache_toggle' => 'on' ] ); + + $results_object = new Results(); + + // Put something in the cache for the query key that proves it came from cache. + $query = "query GetPosts { + posts { + nodes { + title + } + } + }"; + $key = $results_object->the_results_key( null, $query ); + $expected = [ + 'data' => [ + '__typename' => 'Foo Bar' + ] + ]; + $results_object->save( $key, $expected ); + + $actual = $results_object->get( $key ); + $this->assertEquals( $expected, $actual ); + + // Clear the cache + do_action( 'wpgraphql_cache_purge_all' ); + + // empty when pulled directly from cache + $actual = $results_object->get( $key ); + $this->assertEmpty( $actual ); + } + }