-
-
- Deploy now
-
-
- Read our docs
-
+
+
+ WordPress Webhooks ISR Demo
+
+ This example demonstrates Next.js ISR (Incremental Static Regeneration) with WordPress webhooks.
+ When you update a post in WordPress, the webhook triggers revalidation of the specific page.
+
+
+ Recent Posts
+
+ {posts && posts.length > 0 ? (
+
+ {posts.map((edge) => {
+ const post = edge.node;
+ return (
+
+
+
+ {post.title}
+
+
+
+ By {post.author?.node?.name || 'Unknown'} on {new Date(post.date).toLocaleDateString()}
+
+ {post.excerpt && (
+
+ )}
+
+ Read more →
+
+
+ );
+ })}
+
+ ) : (
+ No posts found. Create some posts in WordPress admin.
+ )}
+
+
-
);
}
+
+const GET_POSTS = gql`
+ query GetPosts {
+ posts(first: 10) {
+ edges {
+ node {
+ id
+ title
+ uri
+ date
+ excerpt
+ author {
+ node {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+`;
+
+export async function getStaticProps() {
+ try {
+ const { data } = await getApolloClient().query({
+ query: GET_POSTS,
+ });
+
+ return {
+ props: {
+ posts: data?.posts?.edges || [],
+ },
+ revalidate: 60, // ISR: revalidate every 60 seconds
+ };
+ } catch (error) {
+ console.error("Error fetching posts:", error);
+ return {
+ props: {
+ posts: [],
+ },
+ revalidate: 60,
+ };
+ }
+}
diff --git a/plugins/wp-graphql-headless-webhooks/.wp-env.json b/plugins/wp-graphql-headless-webhooks/.wp-env.json
new file mode 100644
index 00000000..2b276711
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/.wp-env.json
@@ -0,0 +1,25 @@
+{
+ "core": null,
+ "phpVersion": "8.0",
+ "plugins": [
+ "https://downloads.wordpress.org/plugin/wp-graphql.latest-stable.zip",
+ "https://downloads.wordpress.org/plugin/wpgraphql-smart-cache.latest-stable.zip",
+ "."
+ ],
+ "port": 8889,
+ "testsPort": 8890,
+ "config": {
+ "WP_DEBUG": true,
+ "WP_DEBUG_LOG": true,
+ "WP_DEBUG_DISPLAY": false,
+ "WP_ENVIRONMENT_TYPE": "development"
+ },
+ "mappings": {
+ "wp-content/plugins/wp-graphql-headless-webhooks": "."
+ },
+ "env": {
+ "tests": {
+ "port": 8890
+ }
+ }
+}
diff --git a/plugins/wp-graphql-headless-webhooks/composer.json b/plugins/wp-graphql-headless-webhooks/composer.json
index 69dde89d..e7a4962f 100644
--- a/plugins/wp-graphql-headless-webhooks/composer.json
+++ b/plugins/wp-graphql-headless-webhooks/composer.json
@@ -11,7 +11,7 @@
}
],
"require": {
- "php": "^7.4 || ^8.0",
+ "php": "^7.4 || ^8.0 || ^8.4",
"axepress/wp-graphql-plugin-boilerplate": "^0.1.0"
},
"require-dev": {
diff --git a/plugins/wp-graphql-headless-webhooks/composer.lock b/plugins/wp-graphql-headless-webhooks/composer.lock
index b2fa58da..3d4b6bcb 100644
--- a/plugins/wp-graphql-headless-webhooks/composer.lock
+++ b/plugins/wp-graphql-headless-webhooks/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c95ce760de9d78f5a1b0c1e16bbc33a4",
+ "content-hash": "038a2fe0d6aef203f3bc661d6115e6ed",
"packages": [
{
"name": "axepress/wp-graphql-plugin-boilerplate",
@@ -69,43 +69,36 @@
"packages-dev": [
{
"name": "amphp/amp",
- "version": "v2.6.4",
+ "version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/amphp/amp.git",
- "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d"
+ "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d",
- "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d",
+ "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9",
+ "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2"
},
"require-dev": {
- "amphp/php-cs-fixer-config": "dev-master",
- "amphp/phpunit-util": "^1",
- "ext-json": "*",
- "jetbrains/phpstorm-stubs": "^2019.3",
- "phpunit/phpunit": "^7 | ^8 | ^9",
- "react/promise": "^2",
- "vimeo/psalm": "^3.12"
+ "amphp/php-cs-fixer-config": "^2",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "5.23.1"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.x-dev"
- }
- },
"autoload": {
"files": [
- "lib/functions.php",
- "lib/Internal/functions.php"
+ "src/functions.php",
+ "src/Future/functions.php",
+ "src/Internal/functions.php"
],
"psr-4": {
- "Amp\\": "lib"
+ "Amp\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -113,10 +106,6 @@
"MIT"
],
"authors": [
- {
- "name": "Daniel Lowrey",
- "email": "rdlowrey@php.net"
- },
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
@@ -128,6 +117,10 @@
{
"name": "Niklas Keller",
"email": "me@kelunik.com"
+ },
+ {
+ "name": "Daniel Lowrey",
+ "email": "rdlowrey@php.net"
}
],
"description": "A non-blocking concurrency framework for PHP applications.",
@@ -144,9 +137,8 @@
"promise"
],
"support": {
- "irc": "irc://irc.freenode.org/amphp",
"issues": "https://github.com/amphp/amp/issues",
- "source": "https://github.com/amphp/amp/tree/v2.6.4"
+ "source": "https://github.com/amphp/amp/tree/v3.1.0"
},
"funding": [
{
@@ -154,41 +146,45 @@
"type": "github"
}
],
- "time": "2024-03-21T18:52:26+00:00"
+ "time": "2025-01-26T16:07:39+00:00"
},
{
"name": "amphp/byte-stream",
- "version": "v1.8.2",
+ "version": "v2.1.2",
"source": {
"type": "git",
"url": "https://github.com/amphp/byte-stream.git",
- "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc"
+ "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc",
- "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc",
+ "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46",
+ "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46",
"shasum": ""
},
"require": {
- "amphp/amp": "^2",
- "php": ">=7.1"
+ "amphp/amp": "^3",
+ "amphp/parser": "^1.1",
+ "amphp/pipeline": "^1",
+ "amphp/serialization": "^1",
+ "amphp/sync": "^2",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2.3"
},
"require-dev": {
- "amphp/php-cs-fixer-config": "dev-master",
- "amphp/phpunit-util": "^1.4",
- "friendsofphp/php-cs-fixer": "^2.3",
- "jetbrains/phpstorm-stubs": "^2019.3",
- "phpunit/phpunit": "^6 || ^7 || ^8",
- "psalm/phar": "^3.11.4"
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "5.22.1"
},
"type": "library",
"autoload": {
"files": [
- "lib/functions.php"
+ "src/functions.php",
+ "src/Internal/functions.php"
],
"psr-4": {
- "Amp\\ByteStream\\": "lib"
+ "Amp\\ByteStream\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -217,7 +213,136 @@
],
"support": {
"issues": "https://github.com/amphp/byte-stream/issues",
- "source": "https://github.com/amphp/byte-stream/tree/v1.8.2"
+ "source": "https://github.com/amphp/byte-stream/tree/v2.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2025-03-16T17:10:27+00:00"
+ },
+ {
+ "name": "amphp/parser",
+ "version": "v1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/parser.git",
+ "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7",
+ "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Amp\\Parser\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "A generator parser to make streaming parsers simple.",
+ "homepage": "https://github.com/amphp/parser",
+ "keywords": [
+ "async",
+ "non-blocking",
+ "parser",
+ "stream"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/parser/issues",
+ "source": "https://github.com/amphp/parser/tree/v1.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-21T19:16:53+00:00"
+ },
+ {
+ "name": "amphp/pipeline",
+ "version": "v1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/pipeline.git",
+ "reference": "7b52598c2e9105ebcddf247fc523161581930367"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367",
+ "reference": "7b52598c2e9105ebcddf247fc523161581930367",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "^5.18"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Amp\\Pipeline\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "Asynchronous iterators and operators.",
+ "homepage": "https://amphp.org/pipeline",
+ "keywords": [
+ "amp",
+ "amphp",
+ "async",
+ "io",
+ "iterator",
+ "non-blocking"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/pipeline/issues",
+ "source": "https://github.com/amphp/pipeline/tree/v1.2.3"
},
"funding": [
{
@@ -225,7 +350,140 @@
"type": "github"
}
],
- "time": "2024-04-13T18:00:56+00:00"
+ "time": "2025-03-16T16:33:53+00:00"
+ },
+ {
+ "name": "amphp/serialization",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/serialization.git",
+ "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1",
+ "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "dev-master",
+ "phpunit/phpunit": "^9 || ^8 || ^7"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\Serialization\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "Serialization tools for IPC and data storage in PHP.",
+ "homepage": "https://github.com/amphp/serialization",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "serialization",
+ "serialize"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/serialization/issues",
+ "source": "https://github.com/amphp/serialization/tree/master"
+ },
+ "time": "2020-03-25T21:39:07+00:00"
+ },
+ {
+ "name": "amphp/sync",
+ "version": "v2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/sync.git",
+ "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1",
+ "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "amphp/pipeline": "^1",
+ "amphp/serialization": "^1",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "5.23"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\Sync\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ },
+ {
+ "name": "Stephen Coakley",
+ "email": "me@stephencoakley.com"
+ }
+ ],
+ "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.",
+ "homepage": "https://github.com/amphp/sync",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "mutex",
+ "semaphore",
+ "synchronization"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/sync/issues",
+ "source": "https://github.com/amphp/sync/tree/v2.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-03T19:31:26+00:00"
},
{
"name": "automattic/vipwpcs",
@@ -392,25 +650,31 @@
},
{
"name": "behat/gherkin",
- "version": "v4.10.0",
+ "version": "v4.14.0",
"source": {
"type": "git",
"url": "https://github.com/Behat/Gherkin.git",
- "reference": "cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6"
+ "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Behat/Gherkin/zipball/cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6",
- "reference": "cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6",
+ "url": "https://api.github.com/repos/Behat/Gherkin/zipball/34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4",
+ "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4",
"shasum": ""
},
"require": {
- "php": "~7.2|~8.0"
+ "composer-runtime-api": "^2.2",
+ "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*"
},
"require-dev": {
- "cucumber/cucumber": "dev-gherkin-24.1.0",
- "phpunit/phpunit": "~8|~9",
- "symfony/yaml": "~3|~4|~5|~6|~7"
+ "cucumber/gherkin-monorepo": "dev-gherkin-v32.1.1",
+ "friendsofphp/php-cs-fixer": "^3.65",
+ "mikey179/vfsstream": "^1.6",
+ "phpstan/extension-installer": "^1",
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-phpunit": "^2",
+ "phpunit/phpunit": "^10.5",
+ "symfony/yaml": "^5.4 || ^6.4 || ^7.0"
},
"suggest": {
"symfony/yaml": "If you want to parse features, represented in YAML files"
@@ -422,8 +686,8 @@
}
},
"autoload": {
- "psr-0": {
- "Behat\\Gherkin": "src/"
+ "psr-4": {
+ "Behat\\Gherkin\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -434,11 +698,11 @@
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
- "homepage": "http://everzet.com"
+ "homepage": "https://everzet.com"
}
],
"description": "Gherkin DSL parser for PHP",
- "homepage": "http://behat.org/",
+ "homepage": "https://behat.org/",
"keywords": [
"BDD",
"Behat",
@@ -449,9 +713,9 @@
],
"support": {
"issues": "https://github.com/Behat/Gherkin/issues",
- "source": "https://github.com/Behat/Gherkin/tree/v4.10.0"
+ "source": "https://github.com/Behat/Gherkin/tree/v4.14.0"
},
- "time": "2024-10-19T14:46:06+00:00"
+ "time": "2025-05-23T15:06:40+00:00"
},
{
"name": "codeception/codeception",
@@ -1136,16 +1400,16 @@
},
{
"name": "composer/ca-bundle",
- "version": "1.5.6",
+ "version": "1.5.7",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
- "reference": "f65c239c970e7f072f067ab78646e9f0b2935175"
+ "reference": "d665d22c417056996c59019579f1967dfe5c1e82"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175",
- "reference": "f65c239c970e7f072f067ab78646e9f0b2935175",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d665d22c417056996c59019579f1967dfe5c1e82",
+ "reference": "d665d22c417056996c59019579f1967dfe5c1e82",
"shasum": ""
},
"require": {
@@ -1192,7 +1456,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues",
- "source": "https://github.com/composer/ca-bundle/tree/1.5.6"
+ "source": "https://github.com/composer/ca-bundle/tree/1.5.7"
},
"funding": [
{
@@ -1208,7 +1472,7 @@
"type": "tidelift"
}
],
- "time": "2025-03-06T14:30:56+00:00"
+ "time": "2025-05-26T15:08:54+00:00"
},
{
"name": "composer/class-map-generator",
@@ -1937,30 +2201,30 @@
},
{
"name": "doctrine/instantiator",
- "version": "1.5.0",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
- "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+ "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
- "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
+ "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
"shasum": ""
},
"require": {
- "php": "^7.1 || ^8.0"
+ "php": "^8.1"
},
"require-dev": {
- "doctrine/coding-standard": "^9 || ^11",
+ "doctrine/coding-standard": "^11",
"ext-pdo": "*",
"ext-phar": "*",
- "phpbench/phpbench": "^0.16 || ^1",
- "phpstan/phpstan": "^1.4",
- "phpstan/phpstan-phpunit": "^1",
- "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
- "vimeo/psalm": "^4.30 || ^5.4"
+ "phpbench/phpbench": "^1.2",
+ "phpstan/phpstan": "^1.9.4",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^9.5.27",
+ "vimeo/psalm": "^5.4"
},
"type": "library",
"autoload": {
@@ -1987,7 +2251,7 @@
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
- "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
+ "source": "https://github.com/doctrine/instantiator/tree/2.0.0"
},
"funding": [
{
@@ -2003,7 +2267,7 @@
"type": "tidelift"
}
],
- "time": "2022-12-30T00:15:36+00:00"
+ "time": "2022-12-30T00:23:10+00:00"
},
{
"name": "eftec/bladeone",
@@ -3178,25 +3442,27 @@
},
{
"name": "nikic/php-parser",
- "version": "v4.19.4",
+ "version": "v5.4.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2"
+ "reference": "447a020a1f875a434d62f2a401f53b82a396e494"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2",
- "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
+ "reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"shasum": ""
},
"require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
"ext-tokenizer": "*",
- "php": ">=7.1"
+ "php": ">=7.4"
},
"require-dev": {
"ircmaxell/php-yacc": "^0.0.7",
- "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+ "phpunit/phpunit": "^9.0"
},
"bin": [
"bin/php-parse"
@@ -3204,7 +3470,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.9-dev"
+ "dev-master": "5.0-dev"
}
},
"autoload": {
@@ -3228,9 +3494,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
},
- "time": "2024-09-29T15:01:53+00:00"
+ "time": "2024-12-30T11:07:19+00:00"
},
{
"name": "phar-io/manifest",
@@ -4377,16 +4643,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.16",
+ "version": "2.1.17",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9"
+ "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9",
- "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
+ "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
"shasum": ""
},
"require": {
@@ -4431,7 +4697,7 @@
"type": "github"
}
],
- "time": "2025-05-16T09:40:10+00:00"
+ "time": "2025-05-21T20:55:28+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -4960,22 +5226,27 @@
},
{
"name": "psr/container",
- "version": "1.1.2",
+ "version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
- "reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
- "reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
@@ -5002,9 +5273,9 @@
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
- "source": "https://github.com/php-fig/container/tree/1.1.2"
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
},
- "time": "2021-11-05T16:50:12+00:00"
+ "time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/event-dispatcher",
@@ -5218,30 +5489,30 @@
},
{
"name": "psr/log",
- "version": "1.1.4",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+ "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
- "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376",
+ "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376",
"shasum": ""
},
"require": {
- "php": ">=5.3.0"
+ "php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.1.x-dev"
+ "dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
- "Psr\\Log\\": "Psr/Log/"
+ "Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -5262,9 +5533,9 @@
"psr-3"
],
"support": {
- "source": "https://github.com/php-fig/log/tree/1.1.4"
+ "source": "https://github.com/php-fig/log/tree/2.0.0"
},
- "time": "2021-05-03T11:20:27+00:00"
+ "time": "2021-07-14T16:41:46+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -5383,6 +5654,78 @@
],
"time": "2024-05-24T10:39:05+00:00"
},
+ {
+ "name": "revolt/event-loop",
+ "version": "v1.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/revoltphp/event-loop.git",
+ "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3",
+ "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "ext-json": "*",
+ "jetbrains/phpstorm-stubs": "^2019.3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "^5.15"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Revolt\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "ceesjank@gmail.com"
+ },
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "Rock-solid event loop for concurrent PHP applications.",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "concurrency",
+ "event",
+ "event-loop",
+ "non-blocking",
+ "scheduler"
+ ],
+ "support": {
+ "issues": "https://github.com/revoltphp/event-loop/issues",
+ "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7"
+ },
+ "time": "2025-01-25T19:27:39+00:00"
+ },
{
"name": "sebastian/cli-parser",
"version": "1.0.2",
@@ -6578,32 +6921,32 @@
},
{
"name": "slevomat/coding-standard",
- "version": "8.18.0",
+ "version": "8.18.1",
"source": {
"type": "git",
"url": "https://github.com/slevomat/coding-standard.git",
- "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593"
+ "reference": "06b18b3f64979ab31d27c37021838439f3ed5919"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/f3b23cb9b26301b8c3c7bb03035a1bee23974593",
- "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593",
+ "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/06b18b3f64979ab31d27c37021838439f3ed5919",
+ "reference": "06b18b3f64979ab31d27c37021838439f3ed5919",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0",
"php": "^7.4 || ^8.0",
"phpstan/phpdoc-parser": "^2.1.0",
- "squizlabs/php_codesniffer": "^3.12.2"
+ "squizlabs/php_codesniffer": "^3.13.0"
},
"require-dev": {
"phing/phing": "3.0.1",
"php-parallel-lint/php-parallel-lint": "1.4.0",
- "phpstan/phpstan": "2.1.13",
- "phpstan/phpstan-deprecation-rules": "2.0.2",
+ "phpstan/phpstan": "2.1.17",
+ "phpstan/phpstan-deprecation-rules": "2.0.3",
"phpstan/phpstan-phpunit": "2.0.6",
"phpstan/phpstan-strict-rules": "2.0.4",
- "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.17|12.1.3"
+ "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3"
},
"type": "phpcodesniffer-standard",
"extra": {
@@ -6627,7 +6970,7 @@
],
"support": {
"issues": "https://github.com/slevomat/coding-standard/issues",
- "source": "https://github.com/slevomat/coding-standard/tree/8.18.0"
+ "source": "https://github.com/slevomat/coding-standard/tree/8.18.1"
},
"funding": [
{
@@ -6639,33 +6982,32 @@
"type": "tidelift"
}
],
- "time": "2025-05-01T09:40:50+00:00"
+ "time": "2025-05-22T14:32:30+00:00"
},
{
"name": "softcreatr/jsonpath",
- "version": "0.7.6",
+ "version": "0.8.3",
"source": {
"type": "git",
"url": "https://github.com/SoftCreatR/JSONPath.git",
- "reference": "e04c02cb78bcc242c69d17dac5b29436bf3e1076"
+ "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/e04c02cb78bcc242c69d17dac5b29436bf3e1076",
- "reference": "e04c02cb78bcc242c69d17dac5b29436bf3e1076",
+ "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/fc12dee0b46f3fa3a175c4051dbab60984acef4b",
+ "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b",
"shasum": ""
},
"require": {
"ext-json": "*",
- "php": ">=7.1,<8.0"
+ "php": ">=8.0"
},
"replace": {
"flow/jsonpath": "*"
},
"require-dev": {
- "phpunit/phpunit": ">=7.0",
- "roave/security-advisories": "dev-latest",
- "squizlabs/php_codesniffer": "^3.5"
+ "phpunit/phpunit": "^9.6",
+ "roave/security-advisories": "dev-latest"
},
"type": "library",
"autoload": {
@@ -6708,33 +7050,37 @@
"type": "github"
}
],
- "time": "2022-09-27T09:27:12+00:00"
+ "time": "2023-08-17T20:14:00+00:00"
},
{
"name": "spatie/array-to-xml",
- "version": "2.17.1",
+ "version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/array-to-xml.git",
- "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46"
+ "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/5cbec9c6ab17e320c58a259f0cebe88bde4a7c46",
- "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46",
+ "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67",
+ "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67",
"shasum": ""
},
"require": {
"ext-dom": "*",
- "php": "^7.4|^8.0"
+ "php": "^8.0"
},
"require-dev": {
"mockery/mockery": "^1.2",
"pestphp/pest": "^1.21",
- "phpunit/phpunit": "^9.0",
"spatie/pest-plugin-snapshots": "^1.1"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
"Spatie\\ArrayToXml\\": "src"
@@ -6760,7 +7106,7 @@
"xml"
],
"support": {
- "source": "https://github.com/spatie/array-to-xml/tree/2.17.1"
+ "source": "https://github.com/spatie/array-to-xml/tree/3.4.0"
},
"funding": [
{
@@ -6772,7 +7118,7 @@
"type": "github"
}
],
- "time": "2022-12-26T08:22:07+00:00"
+ "time": "2024-12-16T12:45:15+00:00"
},
{
"name": "squizlabs/php_codesniffer",
@@ -6932,38 +7278,34 @@
},
{
"name": "symfony/config",
- "version": "v5.4.46",
+ "version": "v6.4.22",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "977c88a02d7d3f16904a81907531b19666a08e78"
+ "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/977c88a02d7d3f16904a81907531b19666a08e78",
- "reference": "977c88a02d7d3f16904a81907531b19666a08e78",
+ "url": "https://api.github.com/repos/symfony/config/zipball/af5917a3b1571f54689e56677a3f06440d2fe4c7",
+ "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7",
"shasum": ""
},
"require": {
- "php": ">=7.2.5",
- "symfony/deprecation-contracts": "^2.1|^3",
- "symfony/filesystem": "^4.4|^5.0|^6.0",
- "symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-php80": "^1.16",
- "symfony/polyfill-php81": "^1.22"
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/filesystem": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-ctype": "~1.8"
},
"conflict": {
- "symfony/finder": "<4.4"
+ "symfony/finder": "<5.4",
+ "symfony/service-contracts": "<2.5"
},
"require-dev": {
- "symfony/event-dispatcher": "^4.4|^5.0|^6.0",
- "symfony/finder": "^4.4|^5.0|^6.0",
- "symfony/messenger": "^4.4|^5.0|^6.0",
- "symfony/service-contracts": "^1.1|^2|^3",
- "symfony/yaml": "^4.4|^5.0|^6.0"
- },
- "suggest": {
- "symfony/yaml": "To use the yaml reference dumper"
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
@@ -6991,7 +7333,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/config/tree/v5.4.46"
+ "source": "https://github.com/symfony/config/tree/v6.4.22"
},
"funding": [
{
@@ -7007,7 +7349,7 @@
"type": "tidelift"
}
],
- "time": "2024-10-30T07:58:02+00:00"
+ "time": "2025-05-14T06:00:01+00:00"
},
{
"name": "symfony/console",
@@ -7176,20 +7518,20 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v2.5.4",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918"
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918",
- "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=8.1"
},
"type": "library",
"extra": {
@@ -7198,7 +7540,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "2.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -7223,7 +7565,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -7239,7 +7581,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:11:13+00:00"
+ "time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/dom-crawler",
@@ -7403,25 +7745,22 @@
},
{
"name": "symfony/event-dispatcher-contracts",
- "version": "v2.5.4",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
- "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f"
+ "reference": "59eb412e93815df44f05f342958efa9f46b1e586"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f",
- "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586",
+ "reference": "59eb412e93815df44f05f342958efa9f46b1e586",
"shasum": ""
},
"require": {
- "php": ">=7.2.5",
+ "php": ">=8.1",
"psr/event-dispatcher": "^1"
},
- "suggest": {
- "symfony/event-dispatcher-implementation": ""
- },
"type": "library",
"extra": {
"thanks": {
@@ -7429,7 +7768,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "2.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -7462,7 +7801,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.4"
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -7478,30 +7817,29 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:11:13+00:00"
+ "time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v5.4.45",
+ "version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "57c8294ed37d4a055b77057827c67f9558c95c54"
+ "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54",
- "reference": "57c8294ed37d4a055b77057827c67f9558c95c54",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
+ "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
"shasum": ""
},
"require": {
- "php": ">=7.2.5",
+ "php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-mbstring": "~1.8",
- "symfony/polyfill-php80": "^1.16"
+ "symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
- "symfony/process": "^5.4|^6.4"
+ "symfony/process": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -7529,7 +7867,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v5.4.45"
+ "source": "https://github.com/symfony/filesystem/tree/v7.3.0"
},
"funding": [
{
@@ -7545,7 +7883,7 @@
"type": "tidelift"
}
],
- "time": "2024-10-22T13:05:35+00:00"
+ "time": "2024-10-25T15:15:23+00:00"
},
{
"name": "symfony/finder",
@@ -8163,29 +8501,26 @@
},
{
"name": "symfony/service-contracts",
- "version": "v2.5.4",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "f37b419f7aea2e9abf10abd261832cace12e3300"
+ "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300",
- "reference": "f37b419f7aea2e9abf10abd261832cace12e3300",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
"shasum": ""
},
"require": {
- "php": ">=7.2.5",
- "psr/container": "^1.1",
- "symfony/deprecation-contracts": "^2.1|^3"
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
- "suggest": {
- "symfony/service-implementation": ""
- },
"type": "library",
"extra": {
"thanks": {
@@ -8193,13 +8528,16 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "2.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
- }
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -8226,7 +8564,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v2.5.4"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -8242,25 +8580,25 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:11:13+00:00"
+ "time": "2025-04-25T09:37:31+00:00"
},
{
"name": "symfony/stopwatch",
- "version": "v5.4.45",
+ "version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
- "reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004"
+ "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/stopwatch/zipball/fb2c199cf302eb207f8c23e7ee174c1c31a5c004",
- "reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004",
+ "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd",
+ "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd",
"shasum": ""
},
"require": {
- "php": ">=7.2.5",
- "symfony/service-contracts": "^1|^2|^3"
+ "php": ">=8.2",
+ "symfony/service-contracts": "^2.5|^3"
},
"type": "library",
"autoload": {
@@ -8288,7 +8626,7 @@
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/stopwatch/tree/v5.4.45"
+ "source": "https://github.com/symfony/stopwatch/tree/v7.3.0"
},
"funding": [
{
@@ -8304,38 +8642,38 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:11:13+00:00"
+ "time": "2025-02-24T10:49:57+00:00"
},
{
"name": "symfony/string",
- "version": "v5.4.47",
+ "version": "v6.4.21",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "136ca7d72f72b599f2631aca474a4f8e26719799"
+ "reference": "73e2c6966a5aef1d4892873ed5322245295370c6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799",
- "reference": "136ca7d72f72b599f2631aca474a4f8e26719799",
+ "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6",
+ "reference": "73e2c6966a5aef1d4892873ed5322245295370c6",
"shasum": ""
},
"require": {
- "php": ">=7.2.5",
+ "php": ">=8.1",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
- "symfony/polyfill-mbstring": "~1.0",
- "symfony/polyfill-php80": "~1.15"
+ "symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
- "symfony/translation-contracts": ">=3.0"
+ "symfony/translation-contracts": "<2.5"
},
"require-dev": {
- "symfony/error-handler": "^4.4|^5.0|^6.0",
- "symfony/http-client": "^4.4|^5.0|^6.0",
- "symfony/translation-contracts": "^1.1|^2",
- "symfony/var-exporter": "^4.4|^5.0|^6.0"
+ "symfony/error-handler": "^5.4|^6.0|^7.0",
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^6.2|^7.0",
+ "symfony/translation-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
@@ -8374,7 +8712,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v5.4.47"
+ "source": "https://github.com/symfony/string/tree/v6.4.21"
},
"funding": [
{
@@ -8390,7 +8728,7 @@
"type": "tidelift"
}
],
- "time": "2024-11-10T20:33:58+00:00"
+ "time": "2025-04-18T15:23:29+00:00"
},
{
"name": "symfony/yaml",
@@ -8581,21 +8919,21 @@
},
{
"name": "vimeo/psalm",
- "version": "5.26.1",
+ "version": "6.0.0",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
- "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0"
+ "reference": "b8e96bb617bf59382113b1b56cef751f648a7dc9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0",
- "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0",
+ "url": "https://api.github.com/repos/vimeo/psalm/zipball/b8e96bb617bf59382113b1b56cef751f648a7dc9",
+ "reference": "b8e96bb617bf59382113b1b56cef751f648a7dc9",
"shasum": ""
},
"require": {
- "amphp/amp": "^2.4.2",
- "amphp/byte-stream": "^1.5",
+ "amphp/amp": "^3",
+ "amphp/byte-stream": "^2",
"composer-runtime-api": "^2",
"composer/semver": "^1.4 || ^2.0 || ^3.0",
"composer/xdebug-handler": "^2.0 || ^3.0",
@@ -8608,26 +8946,24 @@
"ext-simplexml": "*",
"ext-tokenizer": "*",
"felixfbecker/advanced-json-rpc": "^3.1",
- "felixfbecker/language-server-protocol": "^1.5.2",
+ "felixfbecker/language-server-protocol": "^1.5.3",
"fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
- "nikic/php-parser": "^4.17",
- "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
+ "nikic/php-parser": "^5.0.0",
+ "php": "~8.1.17 || ~8.2.4 || ~8.3.0 || ~8.4.0",
"sebastian/diff": "^4.0 || ^5.0 || ^6.0",
"spatie/array-to-xml": "^2.17.0 || ^3.0",
"symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0",
"symfony/filesystem": "^5.4 || ^6.0 || ^7.0"
},
- "conflict": {
- "nikic/php-parser": "4.17.0"
- },
"provide": {
"psalm/psalm": "self.version"
},
"require-dev": {
- "amphp/phpunit-util": "^2.0",
+ "amphp/phpunit-util": "^3",
"bamarni/composer-bin-plugin": "^1.4",
"brianium/paratest": "^6.9",
+ "dg/bypass-finals": "^1.5",
"ext-curl": "*",
"mockery/mockery": "^1.5",
"nunomaduro/mock-final-classes": "^1.1",
@@ -8635,7 +8971,7 @@
"phpstan/phpdoc-parser": "^1.6",
"phpunit/phpunit": "^9.6",
"psalm/plugin-mockery": "^1.1",
- "psalm/plugin-phpunit": "^0.18",
+ "psalm/plugin-phpunit": "^0.19",
"slevomat/coding-standard": "^8.4",
"squizlabs/php_codesniffer": "^3.6",
"symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0"
@@ -8658,7 +8994,9 @@
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
- "dev-master": "5.x-dev"
+ "dev-5.x": "5.x-dev",
+ "dev-6.x": "6.x-dev",
+ "dev-master": "7.x-dev"
}
},
"autoload": {
@@ -8687,7 +9025,7 @@
"issues": "https://github.com/vimeo/psalm/issues",
"source": "https://github.com/vimeo/psalm"
},
- "time": "2024-09-08T18:53:08+00:00"
+ "time": "2025-01-26T12:03:19+00:00"
},
{
"name": "webmozart/assert",
@@ -11230,8 +11568,8 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": "^7.4 || ^8.0"
+ "php": "^7.4 || ^8.0 || ^8.4"
},
- "platform-dev": [],
+ "platform-dev": {},
"plugin-api-version": "2.6.0"
}
diff --git a/plugins/wp-graphql-headless-webhooks/package.json b/plugins/wp-graphql-headless-webhooks/package.json
new file mode 100644
index 00000000..025a6ed2
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@placeholder/wp-graphql-headless-webhooks",
+ "version": "0.0.1",
+ "private": true,
+ "description": "WPGraphQL Headless Webhooks plugin",
+ "author": "WPEngine Headless OSS Team",
+ "license": "BSD-3-Clause",
+ "scripts": {
+ "dev": "wp-env start && wp-env run cli wp plugin activate wp-graphql wp-graphql-headless-webhooks",
+ "start": "wp-env start",
+ "stop": "wp-env stop",
+ "clean": "wp-env destroy",
+ "cli": "wp-env run cli wp"
+ },
+ "devDependencies": {
+ "@wordpress/env": "^8.0.0"
+ }
+}
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php
new file mode 100644
index 00000000..97daeb31
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php
@@ -0,0 +1,608 @@
+repository = $repository;
+ }
+
+ /**
+ * Initialize admin hooks
+ */
+ public function init(): void {
+ add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
+ add_action( 'admin_init', array( $this, 'handle_actions' ) );
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
+
+ // Register admin-post.php handlers
+ add_action( 'admin_post_graphql_webhook_save', array( $this, 'handle_webhook_save' ) );
+ add_action( 'admin_post_graphql_webhook_bulk_delete', array( $this, 'handle_bulk_delete' ) );
+
+ // Register AJAX handlers
+ add_action( 'wp_ajax_test_webhook', array( $this, 'handle_test_webhook' ) );
+ }
+
+ /**
+ * Add admin menu
+ */
+ public function add_admin_menu(): void {
+ add_submenu_page(
+ 'graphiql-ide',
+ __( 'Webhooks', 'wp-graphql-headless-webhooks' ),
+ __( 'Webhooks', 'wp-graphql-headless-webhooks' ),
+ 'manage_options',
+ self::ADMIN_PAGE_SLUG,
+ array( $this, 'render_admin_page' )
+ );
+ }
+
+ /**
+ * Get admin URL helper
+ *
+ * @param array $args Query arguments.
+ * @return string
+ */
+ public function get_admin_url( array $args = array() ): string {
+ $defaults = array( 'page' => self::ADMIN_PAGE_SLUG );
+ $args = wp_parse_args( $args, $defaults );
+ return admin_url( 'admin.php?' . http_build_query( $args ) );
+ }
+
+ /**
+ * Enqueue admin assets
+ *
+ * @param string $hook_suffix Current admin page.
+ */
+ public function enqueue_assets( string $hook_suffix ): void {
+ // Only load on our admin page - check if we're on the webhooks page
+ if ( ! isset( $_GET['page'] ) || $_GET['page'] !== self::ADMIN_PAGE_SLUG ) {
+ return;
+ }
+
+ $plugin_url = plugin_dir_url( dirname( __DIR__ ) );
+
+ wp_enqueue_style(
+ 'wp-graphql-webhooks-admin',
+ $plugin_url . 'src/Admin/assets/admin.css',
+ array(),
+ '1.0.0'
+ );
+
+ wp_enqueue_script(
+ 'wp-graphql-webhooks-admin',
+ $plugin_url . 'src/Admin/assets/admin.js',
+ array( 'jquery' ),
+ '1.0.0',
+ true
+ );
+
+ wp_localize_script(
+ 'wp-graphql-webhooks-admin',
+ 'wpGraphQLWebhooks',
+ array(
+ 'restUrl' => rest_url( 'graphql-webhooks/v1/' ),
+ 'nonce' => wp_create_nonce( 'wp_rest' ),
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'headerTemplate' => $this->get_header_row_template(),
+ 'confirmDelete' => __( 'Are you sure you want to delete this webhook?', 'wp-graphql-headless-webhooks' ),
+ )
+ );
+ }
+
+ /**
+ * Get header row template for JavaScript
+ *
+ * @return string
+ */
+ private function get_header_row_template(): string {
+ ob_start();
+ include __DIR__ . '/views/partials/webhook-header-row.php';
+ return ob_get_clean();
+ }
+
+ /**
+ * Handle admin actions
+ */
+ public function handle_actions(): void {
+ if ( ! isset( $_GET['page'] ) || self::ADMIN_PAGE_SLUG !== $_GET['page'] ) {
+ return;
+ }
+
+ // Only handle delete action here since save is handled by admin-post.php
+ if ( isset( $_GET['action'] ) && 'delete' === $_GET['action'] ) {
+ $this->handle_webhook_delete();
+ }
+ }
+
+ /**
+ * Verify admin permission
+ *
+ * @return bool
+ */
+ private function verify_admin_permission(): bool {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-graphql-headless-webhooks' ) );
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Verify nonce
+ *
+ * @param string $action Nonce action.
+ * @param string $nonce_field Nonce field name.
+ * @return bool
+ */
+ private function verify_nonce( string $action, string $nonce_field = '_wpnonce' ): bool {
+ if ( ! isset( $_REQUEST[ $nonce_field ] ) || ! wp_verify_nonce( $_REQUEST[ $nonce_field ], $action ) ) {
+ wp_die( esc_html__( 'Security check failed.', 'wp-graphql-headless-webhooks' ) );
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle webhook save
+ */
+ public function handle_webhook_save(): void {
+ if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'graphql_webhook_save', 'graphql_webhook_nonce' ) ) {
+ return;
+ }
+
+ $webhook_id = isset( $_POST['webhook_id'] ) ? intval( $_POST['webhook_id'] ) : 0;
+ $name = sanitize_text_field( $_POST['webhook_name'] ?? '' );
+ $event = sanitize_text_field( $_POST['webhook_event'] ?? '' );
+ $url = esc_url_raw( $_POST['webhook_url'] ?? '' );
+ $method = sanitize_text_field( $_POST['webhook_method'] ?? 'POST' );
+
+ // Process headers
+ $headers = array();
+ if ( ! empty( $_POST['webhook_headers']['name'] ) && is_array( $_POST['webhook_headers']['name'] ) ) {
+ foreach ( $_POST['webhook_headers']['name'] as $index => $header_name ) {
+ $header_name = sanitize_text_field( $header_name );
+ $header_value = sanitize_text_field( $_POST['webhook_headers']['value'][ $index ] ?? '' );
+ if ( ! empty( $header_name ) && ! empty( $header_value ) ) {
+ $headers[ $header_name ] = $header_value;
+ }
+ }
+ }
+
+ if ( $webhook_id > 0 ) {
+ $result = $this->repository->update( $webhook_id, $name, $event, $url, $method, $headers );
+ } else {
+ $result = $this->repository->create( $name, $event, $url, $method, $headers );
+ }
+
+ if ( is_wp_error( $result ) ) {
+ $redirect_url = $this->get_admin_url( array( 'error' => urlencode( $result->get_error_message() ) ) );
+ } else {
+ $redirect_url = $this->get_admin_url( array( 'updated' => 'true' ) );
+ }
+
+ wp_safe_redirect( $redirect_url );
+ exit;
+ }
+
+ /**
+ * Handle webhook delete
+ */
+ public function handle_webhook_delete(): void {
+ $webhook_id = isset( $_GET['webhook_id'] ) ? intval( $_GET['webhook_id'] ) : 0;
+
+ if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'delete_webhook_' . $webhook_id ) ) {
+ return;
+ }
+
+ if ( $webhook_id > 0 ) {
+ $this->repository->delete( $webhook_id );
+ }
+
+ wp_safe_redirect( $this->get_admin_url( array( 'deleted' => 'true' ) ) );
+ exit;
+ }
+
+ /**
+ * Handle bulk delete action
+ */
+ public function handle_bulk_delete(): void {
+ if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'bulk_delete_webhooks' ) ) {
+ return;
+ }
+
+ $bulk_action = $_POST['bulk_action'] ?? $_POST['bulk_action2'] ?? '';
+ $webhook_ids = $_POST['webhook_ids'] ?? array();
+
+ if ( 'delete' === $bulk_action && ! empty( $webhook_ids ) ) {
+ $deleted_count = 0;
+ foreach ( $webhook_ids as $webhook_id ) {
+ $webhook_id = intval( $webhook_id );
+ if ( $webhook_id > 0 && $this->repository->delete( $webhook_id ) ) {
+ $deleted_count++;
+ }
+ }
+
+ $redirect_args = array();
+ if ( $deleted_count > 0 ) {
+ $redirect_args['deleted'] = 'true';
+ $redirect_args['count'] = $deleted_count;
+ } else {
+ $redirect_args['error'] = __( 'Failed to delete webhooks.', 'wp-graphql-headless-webhooks' );
+ }
+
+ wp_safe_redirect( $this->get_admin_url( $redirect_args ) );
+ exit;
+ }
+
+ // If no valid action, redirect back
+ wp_safe_redirect( $this->get_admin_url() );
+ exit;
+ }
+
+ /**
+ * Handle webhook test via AJAX
+ */
+ public function handle_test_webhook(): void {
+ // Verify nonce
+ if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'wp_rest' ) ) {
+ wp_send_json_error( __( 'Security check failed.', 'wp-graphql-headless-webhooks' ) );
+ }
+
+ // Verify permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( __( 'Insufficient permissions.', 'wp-graphql-headless-webhooks' ) );
+ }
+
+ $webhook_id = isset( $_POST['webhook_id'] ) ? intval( $_POST['webhook_id'] ) : 0;
+ if ( ! $webhook_id ) {
+ wp_send_json_error( __( 'Invalid webhook ID.', 'wp-graphql-headless-webhooks' ) );
+ }
+
+ // Get the webhook
+ $webhook = $this->repository->get( $webhook_id );
+ if ( ! $webhook ) {
+ wp_send_json_error( __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ) );
+ }
+
+ // Create test payload based on the event type
+ $test_payload = $this->get_test_payload_for_event( $webhook->event );
+
+ // Send the webhook using a synchronous request for testing
+ $args = [
+ 'headers' => $webhook->headers ?: [ 'Content-Type' => 'application/json' ],
+ 'timeout' => 10,
+ 'blocking' => true, // We need blocking for test to get response
+ ];
+
+ $payload = apply_filters( 'graphql_webhooks_payload', $test_payload, $webhook );
+
+ if ( strtoupper( $webhook->method ) === 'GET' ) {
+ $url = add_query_arg( $payload, $webhook->url );
+ $args['method'] = 'GET';
+ } else {
+ $url = $webhook->url;
+ $args['method'] = strtoupper( $webhook->method );
+ $args['body'] = wp_json_encode( $payload );
+ if ( empty( $args['headers']['Content-Type'] ) ) {
+ $args['headers']['Content-Type'] = 'application/json';
+ }
+ }
+
+ // Log the test request for debugging
+ error_log( sprintf(
+ '[Webhook Test] Sending %s request to %s with payload: %s',
+ $args['method'],
+ $url,
+ wp_json_encode( $payload )
+ ) );
+
+ $response = wp_remote_request( $url, $args );
+
+ if ( is_wp_error( $response ) ) {
+ wp_send_json_error( array(
+ 'message' => sprintf(
+ __( 'Connection failed: %s', 'wp-graphql-headless-webhooks' ),
+ $response->get_error_message()
+ ),
+ 'error_code' => $response->get_error_code(),
+ 'error_data' => $response->get_error_data(),
+ ) );
+ }
+
+ // Get response details
+ $response_code = wp_remote_retrieve_response_code( $response );
+ $response_body = wp_remote_retrieve_body( $response );
+ $response_headers = wp_remote_retrieve_headers( $response );
+
+ // Try to parse JSON response
+ $parsed_body = json_decode( $response_body, true );
+ if ( json_last_error() === JSON_ERROR_NONE ) {
+ $response_body_display = wp_json_encode( $parsed_body, JSON_PRETTY_PRINT );
+ } else {
+ // Strip HTML tags and decode entities from response body
+ $response_body = wp_strip_all_tags( $response_body );
+ $response_body = html_entity_decode( $response_body, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
+ $response_body_display = $response_body;
+ }
+
+ // Determine success based on response code
+ $is_success = $response_code >= 200 && $response_code < 300;
+
+ // Build detailed response message
+ $message = $is_success
+ ? __( 'Webhook test completed successfully!', 'wp-graphql-headless-webhooks' )
+ : sprintf( __( 'Webhook test failed with status %d', 'wp-graphql-headless-webhooks' ), $response_code );
+
+ // Send structured response data
+ wp_send_json_success( array(
+ 'message' => $message,
+ 'success' => $is_success,
+ 'response_code' => $response_code,
+ 'response_body' => substr( $response_body_display, 0, 500 ), // Limit response body to 500 chars
+ 'response_headers' => $response_headers->getAll(),
+ 'test_payload' => $payload, // Include what was sent for debugging
+ ) );
+ }
+
+ /**
+ * Get test payload for a specific event
+ *
+ * @param string $event Event type.
+ * @return array
+ */
+ private function get_test_payload_for_event( string $event ): array {
+ $base_payload = array(
+ 'event' => $event,
+ 'timestamp' => current_time( 'mysql' ),
+ 'test' => true,
+ 'test_mode' => true, // Additional flag to clearly indicate test mode
+ 'message' => 'This is a TEST webhook payload - no production data was affected',
+ );
+
+ // Add event-specific test data
+ switch ( $event ) {
+ case 'smart_cache_created':
+ case 'smart_cache_updated':
+ case 'smart_cache_deleted':
+ $base_payload['data'] = array(
+ 'key' => 'test:post:999999',
+ 'action' => str_replace( 'smart_cache_', '', $event ),
+ 'purge_url' => home_url( '/test-graphql-endpoint' ),
+ 'test_note' => 'This is test data - no actual cache was purged',
+ );
+ break;
+
+ case 'smart_cache_nodes_purged':
+ $base_payload['data'] = array(
+ 'key' => 'test:list:post',
+ 'nodes' => array(
+ array( 'id' => 'test_node_1', 'type' => 'post' ),
+ array( 'id' => 'test_node_2', 'type' => 'term' ),
+ ),
+ 'test_note' => 'This is test data - no actual nodes were purged',
+ );
+ break;
+
+ case 'post_published':
+ case 'post_updated':
+ case 'post_deleted':
+ // Match the actual webhook payload structure
+ $test_post_id = 999999;
+ $base_payload = array(
+ 'post_id' => $test_post_id,
+ 'post' => array(
+ 'id' => $test_post_id,
+ 'title' => 'Test Post - Hello World',
+ 'slug' => 'test-post-hello-world',
+ 'uri' => '/test-post-hello-world/',
+ 'status' => 'publish',
+ 'type' => 'post',
+ 'date' => current_time( 'mysql' ),
+ 'modified' => current_time( 'mysql' ),
+ ),
+ 'path' => '/test-post-hello-world/',
+ 'test' => true,
+ 'test_mode' => true,
+ 'message' => 'This is a TEST webhook payload - no actual post was affected',
+ );
+ break;
+
+ case 'post_meta_change':
+ $base_payload['post_id'] = 999999;
+ $base_payload['meta_key'] = 'test_meta_key';
+ $base_payload['test_note'] = 'This is test data - no actual meta was changed';
+ break;
+
+ case 'term_created':
+ case 'term_assigned':
+ case 'term_unassigned':
+ case 'term_deleted':
+ $base_payload['term_id'] = 999999;
+ $base_payload['taxonomy'] = 'category';
+ if ( $event === 'term_assigned' || $event === 'term_unassigned' ) {
+ $base_payload['object_id'] = 888888;
+ }
+ $base_payload['test_note'] = 'This is test data - no actual term was affected';
+ break;
+
+ case 'user_created':
+ case 'user_deleted':
+ $base_payload['user_id'] = 999999;
+ $base_payload['data'] = array(
+ 'username' => 'test_webhook_user',
+ 'email' => 'test@webhook.local',
+ 'role' => 'subscriber',
+ );
+ $base_payload['test_note'] = 'This is test data - no actual user was affected';
+ break;
+
+ case 'user_assigned':
+ case 'user_reassigned':
+ $base_payload['post_id'] = 999999;
+ $base_payload['author_id'] = 888888;
+ if ( $event === 'user_reassigned' ) {
+ $base_payload['old_author_id'] = 777777;
+ $base_payload['new_author_id'] = 888888;
+ }
+ $base_payload['test_note'] = 'This is test data - no actual assignment was made';
+ break;
+
+ case 'media_uploaded':
+ case 'media_updated':
+ case 'media_deleted':
+ $base_payload['post_id'] = 999999;
+ $base_payload['post'] = array(
+ 'id' => 999999,
+ 'title' => 'Test Media File',
+ 'slug' => 'test-media-file',
+ 'uri' => '/test-media-file/',
+ 'status' => 'inherit',
+ 'type' => 'attachment',
+ 'date' => current_time( 'mysql' ),
+ 'modified' => current_time( 'mysql' ),
+ );
+ $base_payload['path'] = '/test-media-file/';
+ $base_payload['test_note'] = 'This is test data - no actual media was affected';
+ break;
+
+ case 'comment_inserted':
+ case 'comment_status':
+ $base_payload['comment_id'] = 999999;
+ if ( $event === 'comment_status' ) {
+ $base_payload['new_status'] = 'approved';
+ }
+ $base_payload['test_note'] = 'This is test data - no actual comment was affected';
+ break;
+
+ default:
+ $base_payload['data'] = array(
+ 'message' => 'This is a test webhook payload',
+ 'test_note' => 'This is test data for event: ' . $event,
+ );
+ break;
+ }
+
+ /**
+ * Filter the test payload for webhook testing
+ *
+ * @param array $base_payload The test payload data
+ * @param string $event The event type being tested
+ */
+ return apply_filters( 'graphql_webhooks_test_payload', $base_payload, $event );
+ }
+
+ /**
+ * Render admin page
+ */
+ public function render_admin_page(): void {
+ $action = $_GET['action'] ?? '';
+ $admin = $this;
+
+ // Display admin notices
+ if ( isset( $_GET['updated'] ) ) {
+ $message = __( 'Webhook saved successfully.', 'wp-graphql-headless-webhooks' );
+ $type = 'success';
+ include __DIR__ . '/views/admin-notice.php';
+ }
+
+ if ( isset( $_GET['deleted'] ) ) {
+ $count = isset( $_GET['count'] ) ? intval( $_GET['count'] ) : 1;
+ if ( $count > 1 ) {
+ $message = sprintf(
+ _n(
+ '%d webhook deleted successfully.',
+ '%d webhooks deleted successfully.',
+ $count,
+ 'wp-graphql-headless-webhooks'
+ ),
+ $count
+ );
+ } else {
+ $message = __( 'Webhook deleted successfully.', 'wp-graphql-headless-webhooks' );
+ }
+ $type = 'success';
+ include __DIR__ . '/views/admin-notice.php';
+ }
+
+ if ( isset( $_GET['error'] ) ) {
+ $message = sanitize_text_field( $_GET['error'] );
+ $type = 'error';
+ include __DIR__ . '/views/admin-notice.php';
+ }
+
+ // Render appropriate view
+ if ( 'add' === $action || 'edit' === $action ) {
+ $webhook_id = isset( $_GET['webhook_id'] ) ? intval( $_GET['webhook_id'] ) : 0;
+ $webhook = null;
+
+ if ( 'edit' === $action && $webhook_id > 0 ) {
+ $webhook = $this->repository->get( $webhook_id );
+ if ( ! $webhook ) {
+ wp_die( esc_html__( 'Webhook not found.', 'wp-graphql-headless-webhooks' ) );
+ }
+ }
+
+ $events = $this->repository->get_allowed_events();
+ $methods = $this->repository->get_allowed_methods();
+
+ // Convert simple array to associative array for the form
+ $methods = array_combine($methods, $methods);
+
+ // Set form variables
+ $form_title = 'edit' === $action ? __( 'Edit Webhook', 'wp-graphql-headless-webhooks' ) : __( 'Add New Webhook', 'wp-graphql-headless-webhooks' );
+ $submit_text = 'edit' === $action ? __( 'Update Webhook', 'wp-graphql-headless-webhooks' ) : __( 'Add Webhook', 'wp-graphql-headless-webhooks' );
+
+ // Set default values for new webhook
+ if ( 'add' === $action ) {
+ $webhook_id = 0;
+ $name = '';
+ $event = '';
+ $url = '';
+ $method = 'POST';
+ $headers = array();
+ } else {
+ // Extract values from webhook entity
+ $webhook_id = $webhook->id;
+ $name = $webhook->name;
+ $event = $webhook->event;
+ $url = $webhook->url;
+ $method = $webhook->method;
+ $headers = $webhook->headers;
+ }
+
+ include __DIR__ . '/views/webhook-form.php';
+ } else {
+ $webhooks = $this->repository->get_all();
+ include __DIR__ . '/views/webhooks-list.php';
+ }
+ }
+}
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css
new file mode 100644
index 00000000..392f4f96
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css
@@ -0,0 +1,294 @@
+/**
+ * Admin styles for WPGraphQL Webhooks
+ */
+
+/* Webhook header rows */
+.webhook-header-row {
+ margin-bottom: 10px;
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.webhook-header-row input[type="text"] {
+ flex: 1;
+ min-width: 150px;
+}
+
+.webhook-header-row .remove-header {
+ flex-shrink: 0;
+}
+
+.webhook-header-row button.remove-header:hover {
+ background: #d63638;
+ color: #fff;
+}
+
+/* Add some spacing */
+#webhook-headers {
+ margin-bottom: 10px;
+}
+
+/* Form table adjustments */
+.form-table th {
+ width: 200px;
+}
+
+/* Webhooks table column widths */
+.wp-list-table.webhooks .column-cb {
+ width: 2.2em;
+}
+
+.wp-list-table.webhooks .column-name {
+ width: 25%;
+}
+
+.wp-list-table.webhooks .column-event {
+ width: 15%;
+}
+
+.wp-list-table.webhooks .column-method {
+ width: 10%;
+}
+
+.wp-list-table.webhooks .column-url {
+ width: auto;
+}
+
+.wp-list-table.webhooks .column-headers {
+ width: 15%;
+}
+
+/* Improve table styling */
+.wp-list-table.webhooks {
+ margin-top: 0.5em;
+}
+
+.wp-list-table.webhooks th:not(.check-column),
+.wp-list-table.webhooks td:not(.check-column) {
+ vertical-align: top;
+}
+
+/* Checkbox column specific styling */
+.wp-list-table.webhooks .check-column {
+ vertical-align: top;
+ padding: 8px 0 0 3px;
+}
+
+/* Style webhook name as primary column */
+.wp-list-table.webhooks .column-name strong {
+ display: block;
+ margin-bottom: 0.2em;
+ font-size: 14px;
+}
+
+.wp-list-table.webhooks .row-actions {
+ font-size: 13px;
+}
+
+/* Method column styling */
+.wp-list-table.webhooks .column-method {
+ text-align: center;
+}
+
+.wp-list-table.webhooks .column-method strong {
+ display: inline-block;
+ padding: 3px 8px;
+ background: #f0f0f1;
+ border-radius: 3px;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+/* URL column styling */
+.wp-list-table.webhooks .column-url code {
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ font-size: 12px;
+ background: transparent;
+ padding: 0;
+}
+
+/* Headers column styling */
+.wp-list-table.webhooks .column-headers {
+ color: #50575e;
+ font-size: 12px;
+}
+
+.wp-list-table.webhooks .column-headers code {
+ background: #f0f0f1;
+ padding: 2px 4px;
+ border-radius: 2px;
+ font-size: 11px;
+}
+
+/* Bulk actions bar */
+.tablenav .actions {
+ padding: 8px 0;
+}
+
+/* Empty state improvements */
+.webhooks-empty-state {
+ text-align: center;
+ padding: 60px 20px;
+ background: #fff;
+ border: 1px solid #c3c4c7;
+ border-radius: 4px;
+ margin-top: 20px;
+}
+
+.webhooks-empty-state h2 {
+ font-size: 21px;
+ font-weight: 400;
+ margin: 0 0 0.5em;
+ line-height: 1.3;
+}
+
+.webhooks-empty-state p {
+ font-size: 14px;
+ color: #50575e;
+ margin: 0 0 1.5em;
+ line-height: 1.5;
+}
+
+.webhooks-empty-state .button-primary {
+ font-size: 14px;
+ padding: 6px 14px;
+ height: auto;
+}
+
+/* Test webhook link */
+.test-webhook {
+ cursor: pointer;
+ text-decoration: none;
+}
+
+/* Test webhook button states */
+.test-webhook.testing {
+ color: #666;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.test-webhook.success {
+ color: #46b450;
+}
+
+.test-webhook.error {
+ color: #dc3232;
+}
+
+/* Test webhook result display */
+.webhook-test-result td {
+ padding: 0 !important;
+ background: transparent !important;
+}
+
+.webhook-test-result .notice {
+ margin: 10px 0;
+ border-left-width: 4px;
+}
+
+.webhook-test-details {
+ padding: 5px 0;
+ margin: 0;
+}
+
+.webhook-test-details p {
+ margin: 5px 0;
+}
+
+.webhook-test-details .status-success {
+ color: #46b450;
+ font-weight: bold;
+}
+
+.webhook-test-details .status-error {
+ color: #dc3232;
+ font-weight: bold;
+}
+
+.webhook-test-details details {
+ margin: 10px 0;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ padding: 5px;
+ background: #f9f9f9;
+}
+
+.webhook-test-details summary {
+ cursor: pointer;
+ font-weight: 600;
+ padding: 5px;
+ user-select: none;
+}
+
+.webhook-test-details summary:hover {
+ background: #f1f1f1;
+}
+
+.webhook-test-details pre {
+ margin: 10px 0 5px;
+ padding: 10px;
+ background: #fff;
+ border: 1px solid #e1e1e1;
+ border-radius: 3px;
+ overflow-x: auto;
+ font-size: 12px;
+ line-height: 1.4;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.webhook-test-details pre.webhook-test-payload {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.webhook-test-details pre.webhook-response-body {
+ background: #f7f7f7;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+.webhook-test-details .notice-message-body {
+ margin-top: 1em;
+}
+
+.webhook-test-details pre.webhook-response-body {
+ background: #f6f7f7;
+ border: 1px solid #c3c4c7;
+ border-radius: 4px;
+ padding: 12px;
+ margin: 0.5em 0 0;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ font-family: Consolas, Monaco, monospace;
+ font-size: 13px;
+ line-height: 1.5;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+/* Responsive adjustments */
+@media screen and (max-width: 782px) {
+ .webhook-header-row {
+ flex-wrap: wrap;
+ }
+
+ .webhook-header-row input[type="text"] {
+ width: 100%;
+ margin-bottom: 5px;
+ }
+
+ .wp-list-table.webhooks-table {
+ display: block;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js
new file mode 100644
index 00000000..d453635e
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js
@@ -0,0 +1,195 @@
+/**
+ * Admin JavaScript for WPGraphQL Webhooks
+ */
+
+(function ($) {
+ 'use strict';
+
+ $( document ).ready(
+ function () {
+ // Handle adding new header fields
+ $( '#add-header' ).on(
+ 'click',
+ function () {
+ var headerRow = $( wpGraphQLWebhooks.headerTemplate );
+ $( '#webhook-headers' ).append( headerRow );
+ }
+ );
+
+ // Handle removing header fields
+ $( document ).on(
+ 'click',
+ '.remove-header',
+ function () {
+ var headerRows = $( '.webhook-header-row' );
+
+ // Keep at least one header row
+ if (headerRows.length > 1) {
+ $( this ).closest( '.webhook-header-row' ).remove();
+ } else {
+ // Clear the values instead of removing the last row
+ $( this ).closest( '.webhook-header-row' ).find( 'input' ).val( '' );
+ }
+ }
+ );
+
+ // Confirm webhook deletion
+ $( '.delete-webhook' ).on(
+ 'click',
+ function (e) {
+ if ( ! confirm( wpGraphQLWebhooks.confirmDelete )) {
+ e.preventDefault();
+ }
+ }
+ );
+
+ // Handle test webhook clicks using event delegation
+ $( document ).on(
+ 'click',
+ '.test-webhook',
+ function (e) {
+ e.preventDefault();
+
+ var $link = $( this );
+ var webhookId = $link.data( 'webhook-id' );
+ var originalText = $link.text();
+
+ // Prevent multiple clicks
+ if ($link.hasClass( 'testing' )) {
+ return false;
+ }
+
+ // Generate unique ID for this test result
+ var resultId = 'webhook-test-result-' + webhookId + '-' + Date.now();
+
+ // Update UI to show testing
+ $link.text( 'Testing...' ).addClass( 'testing' ).css( 'pointer-events', 'none' );
+
+ // Send test request
+ $.ajax({
+ url: wpGraphQLWebhooks.ajaxUrl,
+ type: 'POST',
+ data: {
+ action: 'test_webhook',
+ webhook_id: webhookId,
+ nonce: wpGraphQLWebhooks.nonce
+ },
+ success: function(response) {
+ if (response.success) {
+ $link.text( 'Success' ).removeClass( 'testing' ).addClass( 'success' );
+ if (response.data) {
+ var $row = $link.closest( 'tr' );
+ var colspan = $row.find( 'td' ).length;
+
+ // Build detailed result HTML
+ var resultHtml = '
';
+ resultHtml += '
Dismiss this notice. ';
+ resultHtml += '
' + response.data.message + '
';
+
+ // Response details
+ if (response.data.response_code) {
+ var statusClass = response.data.success ? 'success' : 'error';
+ resultHtml += '
Response Status: ' + response.data.response_code + '
';
+ }
+
+ // Show payload sent
+ if (response.data.test_payload) {
+ resultHtml += '
';
+ resultHtml += 'Test Payload Sent ';
+ resultHtml += '' + JSON.stringify(response.data.test_payload, null, 2) + ' ';
+ resultHtml += ' ';
+ }
+
+ // Show response body if available
+ if (response.data.response_body) {
+ resultHtml += '
';
+ resultHtml += 'Response Body ';
+ resultHtml += '' + response.data.response_body + ' ';
+ resultHtml += ' ';
+ }
+
+ resultHtml += '
';
+
+ var noticeClass = response.data.success ? 'notice-success' : 'notice-warning';
+ var $resultRow = $( '
' + resultHtml + '
' );
+ $row.after( $resultRow );
+
+ // Handle dismiss button
+ $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() {
+ $( '#' + resultId ).fadeOut(function() {
+ $( this ).remove();
+ });
+ });
+ }
+ } else {
+ $link.text( 'Failed' ).removeClass( 'testing' ).addClass( 'error' );
+ var errorData = response.data || {};
+ var $row = $link.closest( 'tr' );
+ var colspan = $row.find( 'td' ).length;
+
+ // Build error HTML
+ var errorHtml = '
';
+ errorHtml += '
Dismiss this notice. ';
+ errorHtml += '
Test failed: ' + (errorData.message || 'Unknown error') + '
';
+
+ if (errorData.error_code) {
+ errorHtml += '
Error Code: ' + errorData.error_code + '
';
+ }
+
+ if (errorData.error_data) {
+ errorHtml += '
';
+ errorHtml += 'Error Details ';
+ errorHtml += '' + JSON.stringify(errorData.error_data, null, 2) + ' ';
+ errorHtml += ' ';
+ }
+
+ errorHtml += '
';
+
+ var $resultRow = $( '
' + errorHtml + '
' );
+ $row.after( $resultRow );
+
+ // Handle dismiss button
+ $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() {
+ $( '#' + resultId ).fadeOut(function() {
+ $( this ).remove();
+ });
+ });
+ }
+
+ // Reset button after 3 seconds
+ setTimeout(function() {
+ $link.text( originalText ).removeClass( 'testing success error' ).css( 'pointer-events', 'auto' );
+ }, 3000);
+ },
+ error: function(xhr, status, error) {
+ $link.text( 'Error' ).removeClass( 'testing' ).addClass( 'error' );
+ var $row = $link.closest( 'tr' );
+ var colspan = $row.find( 'td' ).length;
+
+ var errorHtml = '
';
+ errorHtml += '
Dismiss this notice. ';
+ errorHtml += '
Test error: ' + error + '
';
+ errorHtml += '
';
+
+ var $resultRow = $( '
' + errorHtml + '
' );
+ $row.after( $resultRow );
+
+ // Handle dismiss button
+ $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() {
+ $( '#' + resultId ).fadeOut(function() {
+ $( this ).remove();
+ });
+ });
+
+ // Reset button after 3 seconds
+ setTimeout(function() {
+ $link.text( originalText ).removeClass( 'testing error' ).css( 'pointer-events', 'auto' );
+ }, 3000);
+ }
+ });
+ }
+ );
+ }
+ );
+
+})( jQuery );
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/admin-notice.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/admin-notice.php
new file mode 100644
index 00000000..4f0de032
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/admin-notice.php
@@ -0,0 +1,20 @@
+
+
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php
new file mode 100644
index 00000000..da7e0b6a
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php
@@ -0,0 +1,23 @@
+
+
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php
new file mode 100644
index 00000000..9099e927
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php
@@ -0,0 +1,113 @@
+
+
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php
new file mode 100644
index 00000000..95e9273e
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php
new file mode 100644
index 00000000..29f5f4b3
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php
@@ -0,0 +1,185 @@
+ 'smart_cache_created',
+ 'update' => 'smart_cache_updated',
+ 'delete' => 'smart_cache_deleted',
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param $webhook_trigger_callback Callback to trigger webhooks
+ */
+ public function __construct( $webhook_trigger_callback ) {
+ $this->webhook_trigger_callback = $webhook_trigger_callback;
+ }
+
+ /**
+ * Initialize hooks
+ */
+ public function init() {
+ add_action( 'graphql_purge', [ $this, 'handle_graphql_purge' ], 10, 3 );
+ add_action( 'wpgraphql_cache_purge_nodes', [ $this, 'handle_cache_purge_nodes' ], 10, 2 );
+ add_action( 'shutdown', [ $this, 'process_buffer' ] );
+ }
+
+ /**
+ * Handle graphql_purge event
+ *
+ * @param string $key Cache key being purged
+ * @param string $event Event type (e.g., post_UPDATE)
+ * @param string $graphql_endpoint GraphQL endpoint URL
+ */
+ public function handle_graphql_purge( $key, $event, $graphql_endpoint ) {
+ $parts = explode( '_', $event );
+ if ( count( $parts ) !== 2 ) {
+ return;
+ }
+
+ $post_type = $parts[0];
+ $action = strtolower( $parts[1] );
+ $buffer_key = "{$post_type}_{$action}";
+
+ if ( ! isset( $this->buffer[ $buffer_key ] ) ) {
+ $this->buffer[ $buffer_key ] = [
+ 'post_type' => $post_type,
+ 'action' => $action,
+ 'graphql_endpoint' => $graphql_endpoint,
+ 'keys' => [],
+ ];
+ }
+
+ // Just store the key - let webhook consumers decode if needed
+ $this->buffer[ $buffer_key ]['keys'][] = $key;
+
+ // Schedule processing if not already scheduled
+ if ( $this->timer === false ) {
+ $this->timer = wp_schedule_single_event( time() + 1, 'wpgraphql_webhooks_process_smart_cache' );
+ add_action( 'wpgraphql_webhooks_process_smart_cache', [ $this, 'process_buffer' ] );
+ }
+ }
+
+ /**
+ * Handle cache purge nodes event - this fires immediately
+ *
+ * @param string $key Cache key
+ * @param array $nodes Nodes being purged
+ */
+ public function handle_cache_purge_nodes( $key, $nodes ) {
+ // This event provides the actual nodes being purged, so we can fire immediately
+ $payload = [
+ 'cache_key' => $key,
+ 'nodes' => $nodes,
+ 'timestamp' => current_time( 'c' ),
+ ];
+
+ call_user_func( $this->webhook_trigger_callback, 'smart_cache_nodes_purged', $payload );
+ }
+
+ /**
+ * Process the buffered events
+ */
+ public function process_buffer() {
+ if ( empty( $this->buffer ) ) {
+ return;
+ }
+
+ foreach ( $this->buffer as $data ) {
+ $webhook_event = self::EVENT_MAP[ $data['action'] ] ?? null;
+ if ( ! $webhook_event ) {
+ continue;
+ }
+
+ // Decode cache keys to get actual post IDs
+ $decoded_items = [];
+ $paths = [];
+
+ foreach ( $data['keys'] as $key ) {
+ // WPGraphQL cache keys are base64 encoded global IDs
+ // Format is typically: base64("post:123") or base64("page:456")
+ $decoded = base64_decode( $key );
+ if ( $decoded && strpos( $decoded, ':' ) !== false ) {
+ list( $type, $id ) = explode( ':', $decoded, 2 );
+
+ // Get the post data
+ if ( $type === 'post' || $type === 'page' ) {
+ $post = get_post( $id );
+ if ( $post ) {
+ $uri = str_replace( home_url(), '', get_permalink( $post ) );
+ $path = '/' . trim( $uri, '/' ) . '/';
+
+ $decoded_items[] = [
+ 'id' => $post->ID,
+ 'title' => $post->post_title,
+ 'slug' => $post->post_name,
+ 'uri' => $uri,
+ 'path' => $path,
+ 'type' => $post->post_type,
+ 'status' => $post->post_status,
+ ];
+
+ $paths[] = $path;
+ }
+ }
+ }
+ }
+
+ // Enhanced payload with decoded post data
+ $payload = [
+ 'post_type' => $data['post_type'],
+ 'action' => $data['action'],
+ 'graphql_endpoint' => $data['graphql_endpoint'],
+ 'cache_keys' => array_unique( $data['keys'] ), // Original cache keys
+ 'cache_keys_count' => count( array_unique( $data['keys'] ) ),
+ 'posts' => $decoded_items, // Decoded post data
+ 'paths' => array_unique( $paths ), // Paths for ISR revalidation
+ 'timestamp' => current_time( 'c' ),
+ ];
+
+ // If there's only one post, add it as the primary post/path for compatibility
+ if ( count( $decoded_items ) === 1 ) {
+ $payload['post'] = $decoded_items[0];
+ $payload['path'] = $decoded_items[0]['path'];
+ }
+
+ call_user_func( $this->webhook_trigger_callback, $webhook_event, $payload );
+ }
+
+ $this->buffer = [];
+ $this->timer = false;
+ }
+}
diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php
index 3776fae7..a95d80ff 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php
@@ -5,6 +5,7 @@
use WPGraphQL\Webhooks\Events\Interfaces\EventManager;
use WPGraphQL\Webhooks\Repository\Interfaces\WebhookRepositoryInterface;
use WPGraphQL\Webhooks\Handlers\Interfaces\Handler;
+use WPGraphQL\Webhooks\Events\SmartCacheEventHandler;
/**
* Webhook Event Manager
@@ -16,15 +17,24 @@ class WebhookEventManager implements EventManager {
private WebhookRepositoryInterface $repository;
private Handler $handler;
+ /**
+ * Smart Cache event handler
+ * @var SmartCacheEventHandler
+ */
+ private SmartCacheEventHandler $smart_cache_handler;
+
/**
* Constructor
*
* @param WebhookRepositoryInterface $repository
- * @param Handler $sender
+ * @param Handler $handler
*/
- public function __construct( WebhookRepositoryInterface $repository, $handler ) {
+ public function __construct( WebhookRepositoryInterface $repository, Handler $handler ) {
$this->repository = $repository;
$this->handler = $handler;
+
+ // Initialize Smart Cache handler
+ $this->smart_cache_handler = new SmartCacheEventHandler( [ $this, 'trigger_webhooks' ] );
}
/**
@@ -47,6 +57,9 @@ public function register_hooks(): void {
add_action( 'delete_attachment', [ $this, 'on_media_deleted' ], 10, 1 );
add_action( 'wp_insert_comment', [ $this, 'on_comment_inserted' ], 10, 2 );
add_action( 'transition_comment_status', [ $this, 'on_comment_status' ], 10, 3 );
+
+ // Smart Cache integration
+ $this->smart_cache_handler->init();
}
/**
@@ -57,12 +70,35 @@ public function register_hooks(): void {
*/
private function trigger_webhooks( string $event, array $payload ): void {
$allowed_events = $this->repository->get_allowed_events();
+
if ( ! array_key_exists( $event, $allowed_events ) ) {
error_log( 'Event ' . $event . ' is not allowed. Allowed events: ' . implode( ', ', $allowed_events ) );
return;
}
+ // Enrich payload with post data if post_id is present
+ if ( isset( $payload['post_id'] ) ) {
+ $post = get_post( $payload['post_id'] );
+ if ( $post ) {
+ // Add post data to payload
+ $payload['post'] = [
+ 'id' => $post->ID,
+ 'title' => $post->post_title,
+ 'slug' => $post->post_name,
+ 'uri' => str_replace( home_url(), '', get_permalink( $post ) ),
+ 'status' => $post->post_status,
+ 'type' => $post->post_type,
+ 'date' => $post->post_date,
+ 'modified' => $post->post_modified,
+ ];
+
+ // Add the path for Next.js revalidation
+ $payload['path'] = '/' . trim( $payload['post']['uri'], '/' ) . '/';
+ }
+ }
+
do_action( 'graphql_webhooks_before_trigger', $event, $payload );
+
foreach ( $this->repository->get_all() as $webhook ) {
if ( $webhook->event === $event ) {
$this->handler->handle( $webhook, $payload );
diff --git a/plugins/wp-graphql-headless-webhooks/src/Plugin.php b/plugins/wp-graphql-headless-webhooks/src/Plugin.php
index 4eba694a..8dfdc0f2 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Plugin.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Plugin.php
@@ -10,10 +10,14 @@
namespace WPGraphQL\Webhooks;
use AxeWP\GraphQL\Helper\Helper;
+use WPGraphQL\Webhooks\Admin\WebhooksAdmin;
use WPGraphQL\Webhooks\Handlers\WebhookHandler;
use WPGraphQL\Webhooks\PostTypes\WebhookPostType;
use WPGraphQL\Webhooks\Repository\WebhookRepository;
use WPGraphQL\Webhooks\Events\WebhookEventManager;
+use WPGraphQL\Webhooks\Rest\WebhookEventsEndpoint;
+use WPGraphQL\Webhooks\Rest\WebhookTestEndpoint;
+use WPGraphQL\Webhooks\Mutation\CreateWebhook;
/**
* Plugin singleton class.
@@ -50,6 +54,13 @@ final class Plugin {
*/
private WebhookEventManager $event_manager;
+ /**
+ * Webhooks admin.
+ *
+ * @var WebhooksAdmin
+ */
+ private WebhooksAdmin $admin;
+
/**
* Get singleton instance.
*
@@ -79,10 +90,60 @@ private function setup(): void {
Helper::set_hook_prefix( 'graphql_webhooks' );
WebhookPostType::init();
- $this->repository = new WebhookRepository();
- $this->handler = new WebhookHandler();
+ $this->repository = new WebhookRepository();
+ $this->handler = new WebhookHandler();
$this->event_manager = new WebhookEventManager( $this->repository, $this->handler );
$this->event_manager->register_hooks();
+
+ // Add standard WordPress events to the allowed events list
+ add_filter( 'graphql_webhooks_allowed_events', function( $events ) {
+ // Add standard WordPress post events
+ $events['post_published'] = 'Post Published';
+ $events['post_updated'] = 'Post Updated';
+ $events['post_deleted'] = 'Post Deleted';
+ $events['post_meta_change'] = 'Post Meta Changed';
+
+ // Add term events
+ $events['term_created'] = 'Term Created';
+ $events['term_assigned'] = 'Term Assigned';
+ $events['term_unassigned'] = 'Term Unassigned';
+ $events['term_deleted'] = 'Term Deleted';
+
+ // Add user events
+ $events['user_created'] = 'User Created';
+ $events['user_deleted'] = 'User Deleted';
+ $events['user_assigned'] = 'User Assigned to Post';
+ $events['user_reassigned'] = 'User Reassigned on Post';
+
+ // Add media events
+ $events['media_uploaded'] = 'Media Uploaded';
+ $events['media_updated'] = 'Media Updated';
+ $events['media_deleted'] = 'Media Deleted';
+
+ // Add comment events
+ $events['comment_inserted'] = 'Comment Added';
+ $events['comment_status'] = 'Comment Status Changed';
+
+ return $events;
+ } );
+
+ // Register REST endpoints
+ if ( class_exists( WebhookEventsEndpoint::class ) ) {
+ $events_endpoint = new WebhookEventsEndpoint( $this->repository );
+ add_action( 'rest_api_init', array( $events_endpoint, 'register' ) );
+ }
+
+ // Register test endpoint
+ if ( class_exists( WebhookTestEndpoint::class ) ) {
+ $test_endpoint = new WebhookTestEndpoint( $this->repository );
+ add_action( 'rest_api_init', array( $test_endpoint, 'register' ) );
+ }
+
+ // Initialize admin UI
+ if ( is_admin() ) {
+ $this->admin = new WebhooksAdmin( $this->repository );
+ $this->admin->init();
+ }
}
/**
@@ -124,4 +185,4 @@ public function __wakeup(): void {
}
}
-endif;
\ No newline at end of file
+endif;
diff --git a/plugins/wp-graphql-headless-webhooks/src/PostTypes/WebhookPostType.php b/plugins/wp-graphql-headless-webhooks/src/PostTypes/WebhookPostType.php
index ae78d8ca..23f0c7b3 100644
--- a/plugins/wp-graphql-headless-webhooks/src/PostTypes/WebhookPostType.php
+++ b/plugins/wp-graphql-headless-webhooks/src/PostTypes/WebhookPostType.php
@@ -44,8 +44,8 @@ public static function register_webhook_cpt(): void {
'description' => 'Manages GraphQL Webhooks',
'taxonomies' => [],
'public' => false,
- 'show_ui' => true,
- 'show_in_menu' => true,
+ 'show_ui' => false,
+ 'show_in_menu' => false,
'show_in_admin_bar' => false,
'menu_icon' => 'dashicons-share-alt',
'show_in_nav_menus' => false,
diff --git a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php
index 05cfe9dc..b391e17b 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php
@@ -24,24 +24,11 @@ class WebhookRepository implements WebhookRepositoryInterface {
* @var array
*/
private $default_events = [
- 'post_published' => 'Post Published',
- 'post_updated' => 'Post Updated',
- 'post_deleted' => 'Post Deleted',
- 'post_meta_change' => 'Post Meta Changed',
- 'term_created' => 'Term Created',
- 'term_assigned' => 'Term Assigned to Post',
- 'term_unassigned' => 'Term Unassigned from Post',
- 'term_deleted' => 'Term Deleted',
- 'term_meta_change' => 'Term Meta Changed',
- 'user_created' => 'User Created',
- 'user_assigned' => 'User Assigned as Author',
- 'user_deleted' => 'User Deleted',
- 'user_reassigned' => 'User Author Reassigned',
- 'media_uploaded' => 'Media Uploaded',
- 'media_updated' => 'Media Updated',
- 'media_deleted' => 'Media Deleted',
- 'comment_inserted' => 'Comment Inserted',
- 'comment_status' => 'Comment Status Changed',
+ // Smart Cache events only by default
+ 'smart_cache_created' => 'Smart Cache - Content Created',
+ 'smart_cache_updated' => 'Smart Cache - Content Updated',
+ 'smart_cache_deleted' => 'Smart Cache - Content Deleted',
+ 'smart_cache_nodes_purged' => 'Smart Cache - Nodes Purged',
];
/**
@@ -53,6 +40,16 @@ public function get_allowed_events(): array {
return apply_filters('graphql_webhooks_allowed_events', $this->default_events);
}
+ /**
+ * Get the list of allowed HTTP methods.
+ *
+ * @return array Array of allowed HTTP methods.
+ */
+ public function get_allowed_methods(): array {
+ $default_methods = ['POST', 'GET'];
+ return apply_filters('graphql_webhooks_allowed_methods', $default_methods);
+ }
+
/**
* Retrieve all published webhook entities.
*
@@ -197,7 +194,8 @@ public function validate_data($event, $url, $method) {
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return new WP_Error('invalid_url', 'Invalid URL.');
}
- if (!in_array(strtoupper($method), ['GET', 'POST'], true)) {
+ $allowed_methods = $this->get_allowed_methods();
+ if (!in_array(strtoupper($method), array_map('strtoupper', $allowed_methods), true)) {
return new WP_Error('invalid_method', 'Invalid HTTP method.');
}
return apply_filters('graphql_webhooks_validate_data', true, $event, $url, $method);
diff --git a/plugins/wp-graphql-headless-webhooks/tests/wp-graphql-headless-webhooks-test.xml b/plugins/wp-graphql-headless-webhooks/tests/wp-graphql-headless-webhooks-test.xml
new file mode 100644
index 00000000..bbe5a3c2
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/tests/wp-graphql-headless-webhooks-test.xml
@@ -0,0 +1,180 @@
+
+
+
+
+ wp-graphql-headless-webhooks
+ http://localhost:8889
+
+ Thu, 12 Jun 2025 14:45:36 +0000
+ en-US
+ 1.2
+
+
+
+ 1 admin wordpress@example.com admin
+
+
+ https://wordpress.org/?v=6.8.1
+
+ -
+
Smart Cache - Post Created
+
+ Wed, 11 Jun 2025 20:12:11 +0000
+ admin
+
+
+
+
+
+ 2025-06-11 20:12:11
+ 2025-06-11 20:12:11
+ 2025-06-11 20:34:52
+ 2025-06-11 20:34:52
+ closed
+ closed
+ smart-cache-post-created
+ publish
+ 0
+ 0
+ graphql_webhook
+
+ 0
+
+ _webhook_event
+ smart_cache_created
+
+
+ _webhook_url
+ https://webhook.site/649f6fda-2f6b-48c1-b85e-f016285083dc
+
+
+ _webhook_method
+ POST
+
+
+ _webhook_headers
+ {"X-Webhook-Event":"smart_cache_created","X-Another-Webhook-Event":"main_event"}
+
+
+ -
+
Smart Cache Updated Webhook
+
+ Wed, 11 Jun 2025 20:37:47 +0000
+ admin
+
+
+
+
+
+ 2025-06-11 20:37:47
+ 2025-06-11 20:37:47
+ 2025-06-11 20:37:47
+ 2025-06-11 20:37:47
+ closed
+ closed
+ smart-cache-updated-webhook
+ publish
+ 0
+ 0
+ graphql_webhook
+
+ 0
+
+ _webhook_event
+ smart_cache_updated
+
+
+ _webhook_url
+ https://webhook.site/649f6fda-2f6b-48c1-b85e-f016285083dc
+
+
+ _webhook_method
+ POST
+
+
+ _webhook_headers
+ {"X-Webhook-Event":"smart_cache_updated"}
+
+
+ -
+
Smart Cache - Post Deleted
+
+ Wed, 11 Jun 2025 20:38:23 +0000
+ admin
+
+
+
+
+
+ 2025-06-11 20:38:23
+ 2025-06-11 20:38:23
+ 2025-06-12 13:54:33
+ 2025-06-12 13:54:33
+ closed
+ closed
+ smart-cache-post-deleted
+ publish
+ 0
+ 0
+ graphql_webhook
+
+ 0
+
+ _webhook_event
+ smart_cache_deleted
+
+
+ _webhook_url
+ https://webhook.site/649f6fda-2f6b-48c1-b85e-f016285083dc
+
+
+ _webhook_method
+ POST
+
+
+ _webhook_headers
+ {"X-Webhook-Event":"smart_cache_deleted"}
+
+
+ -
+
Smart Cache Nodes Purged Webhook
+
+ Wed, 11 Jun 2025 20:39:46 +0000
+ admin
+
+
+
+
+
+ 2025-06-11 20:39:46
+ 2025-06-11 20:39:46
+ 2025-06-11 20:39:46
+ 2025-06-11 20:39:46
+ closed
+ closed
+ smart-cache-nodes-purged-webhook
+ publish
+ 0
+ 0
+ graphql_webhook
+
+ 0
+
+ _webhook_event
+ smart_cache_nodes_purged
+
+
+ _webhook_url
+ https://webhook.site/649f6fda-2f6b-48c1-b85e-f016285083dc
+
+
+ _webhook_method
+ POST
+
+
+ _webhook_headers
+ {"X-Webhook-Event":"smart_cache_nodes_purged"}
+
+
+
+
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5f6d9344..eab0e4c1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -34,6 +34,12 @@ importers:
specifier: ^8.13.0
version: 8.13.0
+ plugins/wp-graphql-headless-webhooks:
+ devDependencies:
+ '@wordpress/env':
+ specifier: ^8.0.0
+ version: 8.13.0
+
packages:
'@isaacs/cliui@8.0.2':
diff --git a/scripts/clean.sh b/scripts/clean.sh
index 365fb2bf..fa6778ca 100755
--- a/scripts/clean.sh
+++ b/scripts/clean.sh
@@ -1,21 +1,10 @@
#!/bin/bash
-echo "Stopping wp-env containers..."
-docker-compose -f ~/.wp-env/*/docker-compose.yml down 2>/dev/null || true
-
-echo "Removing wp-env containers..."
+# Stop and remove all Docker containers with wp-env prefix
docker ps -a -q -f name=wp-env | xargs -r docker rm -f
-echo "Removing wp-env volumes..."
-docker volume ls -q -f name=wp-env | xargs -r docker volume rm -f
-
-echo "Removing WordPress volumes..."
-docker volume ls -q -f name=wordpress | xargs -r docker volume rm -f
+# Remove wp-env state directories
+rm -rf ~/.wp-env
-echo "Removing all test volumes..."
-docker volume ls -q -f name=tests-wordpress | xargs -r docker volume rm -f
-
-echo "Cleaning up networks..."
+# Prune Docker networks
docker network prune -f
-
-echo "Cleanup complete! You may need to wait a few seconds before starting wp-env again."
diff --git a/scripts/cleanup-wp-env.sh b/scripts/cleanup-wp-env.sh
deleted file mode 100755
index bcb0da4b..00000000
--- a/scripts/cleanup-wp-env.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-# Stop and remove all Docker containers with wp-env prefix
-docker ps -a -q -f name=wp-env | xargs -r docker rm -f
-
-# Remove wp-env state directories
-sudo rm -rf ~/.wp-env/*
-
-# Prune Docker networks
-docker network prune -f