diff --git a/examples/next/webhooks-isr/.wp-env.json b/examples/next/webhooks-isr/.wp-env.json
index a15c20dd..b18d87b2 100644
--- a/examples/next/webhooks-isr/.wp-env.json
+++ b/examples/next/webhooks-isr/.wp-env.json
@@ -1,7 +1,8 @@
{
- "phpVersion": "7.4",
+ "phpVersion": "8.0",
"plugins": [
"https://github.com/wp-graphql/wp-graphql/releases/latest/download/wp-graphql.zip",
+ "https://github.com/wp-graphql/wp-graphql-smart-cache/releases/download/v2.0.0/wpgraphql-smart-cache.zip",
"https://downloads.wordpress.org/plugin/code-snippets.3.6.8.zip",
"../../../plugins/wp-graphql-headless-webhooks"
],
diff --git a/plugins/wp-graphql-headless-webhooks/assets/css/admin.css b/plugins/wp-graphql-headless-webhooks/assets/css/admin.css
new file mode 100644
index 00000000..7bfc7661
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/assets/css/admin.css
@@ -0,0 +1,207 @@
+/**
+ * Minimal custom styles for Webhooks admin
+ * Leveraging WordPress core admin classes
+ */
+
+/* Use core WordPress styles for most elements */
+
+/* Webhook method badge styling */
+.webhook-method {
+ font-weight: 600;
+ text-transform: uppercase;
+ font-size: 11px;
+}
+
+/* Header row styling using core form-table classes */
+.webhook-headers .form-table td {
+ padding: 5px 10px 5px 0;
+}
+
+/* Minimal adjustment for header inputs */
+.webhook-header-row {
+ margin-bottom: 10px;
+}
+
+.webhook-header-row input[type="text"] {
+ width: calc(50% - 50px);
+ margin-right: 10px;
+}
+
+/* Use core button styles, just add spacing */
+.test-webhook {
+ margin-right: 5px;
+}
+
+/* Test button states */
+.test-webhook.testing {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.test-webhook.success {
+ color: #46b450;
+}
+
+.test-webhook.error {
+ color: #dc3232;
+}
+
+/* Webhook test result row */
+.webhook-test-result td {
+ padding: 0 !important;
+ background: transparent !important;
+}
+
+.webhook-test-result .notice {
+ margin: 10px;
+ position: relative;
+}
+
+.webhook-test-result .notice.inline {
+ display: block;
+}
+
+/* Test details styling */
+.webhook-test-details {
+ padding: 12px;
+}
+
+.webhook-test-details p {
+ margin: 0.5em 0;
+}
+
+.webhook-test-details strong {
+ font-weight: 600;
+}
+
+/* Status indicators */
+.webhook-test-details .status-success {
+ color: #46b450;
+ font-weight: 600;
+}
+
+.webhook-test-details .status-error {
+ color: #dc3232;
+ font-weight: 600;
+}
+
+/* Expandable details */
+.webhook-test-details details {
+ margin: 10px 0;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ padding: 0;
+}
+
+.webhook-test-details summary {
+ padding: 10px;
+ cursor: pointer;
+ background: #f7f7f7;
+ border-bottom: 1px solid #ddd;
+ font-weight: 600;
+}
+
+.webhook-test-details details[open] summary {
+ border-bottom: 1px solid #ddd;
+}
+
+.webhook-test-details pre {
+ margin: 0;
+ padding: 10px;
+ background: #f9f9f9;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ font-size: 12px;
+ line-height: 1.4;
+}
+
+/* Payload and response styling */
+.webhook-test-payload,
+.webhook-response-body {
+ font-family: Consolas, Monaco, monospace;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+/* Notice dismiss button positioning */
+.webhook-test-result .notice-dismiss {
+ position: absolute;
+ top: 0;
+ right: 1px;
+ padding: 10px 15px;
+ font-size: 13px;
+ line-height: 1.23076923;
+ text-decoration: none;
+}
+
+.webhook-test-result .notice-dismiss:before {
+ content: "\f153";
+ font-family: dashicons;
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 400;
+ height: 20px;
+ width: 20px;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Webhook URL column styling */
+.webhook-url {
+ display: block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: Consolas, Monaco, monospace;
+ font-size: 13px;
+ color: #2271b1;
+}
+
+/* Ensure proper column spacing */
+.wp-list-table .column-url {
+ width: 35%;
+ min-width: 200px;
+}
+
+.wp-list-table .column-name {
+ width: 25%;
+}
+
+.wp-list-table .column-event {
+ width: 20%;
+}
+
+.wp-list-table .column-method {
+ width: 10%;
+}
+
+.wp-list-table .column-headers {
+ width: 10%;
+}
+
+/* Row actions test link styling */
+.row-actions .test-webhook {
+ color: #2271b1;
+ text-decoration: none;
+}
+
+.row-actions .test-webhook:hover {
+ color: #135e96;
+ text-decoration: underline;
+}
+
+.row-actions .test-webhook.testing {
+ color: #666;
+ cursor: default;
+ pointer-events: none;
+}
+
+.row-actions .test-webhook.success {
+ color: #46b450;
+}
+
+.row-actions .test-webhook.error {
+ color: #dc3232;
+}
diff --git a/plugins/wp-graphql-headless-webhooks/assets/js/admin.js b/plugins/wp-graphql-headless-webhooks/assets/js/admin.js
new file mode 100644
index 00000000..26e58e08
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/assets/js/admin.js
@@ -0,0 +1,205 @@
+/**
+ * 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 || wpGraphQLWebhooks.headerRowTemplate );
+ $( '#webhook-headers-container, #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' );
+
+ // 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( originalText ).removeClass( 'testing' ).addClass( 'success' );
+ if (response.data) {
+ var $row = $link.closest( 'tr' );
+ var colspan = $row.find( 'td, th' ).length;
+
+ // Build detailed result HTML
+ var resultHtml = '
';
+ resultHtml += '
';
+ 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( originalText ).removeClass( 'testing' ).addClass( 'error' );
+ var errorData = response.data || {};
+ var $row = $link.closest( 'tr' );
+ var colspan = $row.find( 'td, th' ).length;
+
+ // Build error HTML
+ var errorHtml = '';
+ errorHtml += '
';
+ 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();
+ });
+ });
+ }
+ },
+ error: function(xhr, status, error) {
+ $link.text( originalText ).removeClass( 'testing' ).addClass( 'error' );
+ var $row = $link.closest( 'tr' );
+ var colspan = $row.find( 'td, th' ).length;
+
+ var errorHtml = '';
+ errorHtml += '
';
+ 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();
+ });
+ });
+ }
+ });
+ }
+ );
+
+ // Handle bulk actions if using WP_List_Table
+ $('#doaction, #doaction2').on('click', function(e) {
+ var action = $(this).prev('select').val();
+
+ if (action === 'delete') {
+ var checked = $('input[name="webhook[]"]:checked');
+
+ if (checked.length === 0) {
+ alert('Please select at least one webhook to delete.');
+ e.preventDefault();
+ return false;
+ }
+
+ if (!confirm(wpGraphQLWebhooks.confirmDelete)) {
+ e.preventDefault();
+ return false;
+ }
+ }
+ });
+ }
+ );
+
+})( jQuery );
diff --git a/plugins/wp-graphql-headless-webhooks/composer.lock b/plugins/wp-graphql-headless-webhooks/composer.lock
index b2fa58da..cd233022 100644
--- a/plugins/wp-graphql-headless-webhooks/composer.lock
+++ b/plugins/wp-graphql-headless-webhooks/composer.lock
@@ -8,28 +8,29 @@
"packages": [
{
"name": "axepress/wp-graphql-plugin-boilerplate",
- "version": "0.1.0",
+ "version": "0.1.1",
"source": {
"type": "git",
"url": "https://github.com/AxeWP/wp-graphql-plugin-boilerplate.git",
- "reference": "49096512cd599068edbbcff8e46684e207789001"
+ "reference": "09495b61346453baabdf4c71a38ada3cfc91c3a7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/AxeWP/wp-graphql-plugin-boilerplate/zipball/49096512cd599068edbbcff8e46684e207789001",
- "reference": "49096512cd599068edbbcff8e46684e207789001",
+ "url": "https://api.github.com/repos/AxeWP/wp-graphql-plugin-boilerplate/zipball/09495b61346453baabdf4c71a38ada3cfc91c3a7",
+ "reference": "09495b61346453baabdf4c71a38ada3cfc91c3a7",
"shasum": ""
},
"require": {
"php": ">=7.4"
},
"require-dev": {
- "axepress/wp-graphql-cs": "^2.0.0-beta",
- "axepress/wp-graphql-stubs": "^1.12.2",
+ "axepress/wp-graphql-cs": "^2.0.0",
+ "axepress/wp-graphql-stubs": "^2.3.0",
"phpcompatibility/php-compatibility": "dev-develop as 9.9.9",
"phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "^1.2",
- "szepeviktor/phpstan-wordpress": "^1.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-deprecation-rules": "^2.0.1",
+ "szepeviktor/phpstan-wordpress": "^2.0",
"wp-cli/wp-cli-bundle": "^2.8.1"
},
"type": "library",
@@ -55,7 +56,7 @@
"description": "Boilerplate for creating WPGraphQL extensions",
"support": {
"issues": "https://github.com/AxeWP/wp-graphql-plugin-boilerplate/issues",
- "source": "https://github.com/AxeWP/wp-graphql-plugin-boilerplate/tree/0.1.0"
+ "source": "https://github.com/AxeWP/wp-graphql-plugin-boilerplate/tree/0.1.1"
},
"funding": [
{
@@ -63,49 +64,42 @@
"type": "github"
}
],
- "time": "2024-04-06T17:12:24+00:00"
+ "time": "2025-06-07T02:03:50+00:00"
}
],
"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 +107,6 @@
"MIT"
],
"authors": [
- {
- "name": "Daniel Lowrey",
- "email": "rdlowrey@php.net"
- },
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
@@ -128,6 +118,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 +138,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 +147,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 +214,269 @@
],
"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": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "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": [
{
@@ -225,7 +484,7 @@
"type": "github"
}
],
- "time": "2024-04-13T18:00:56+00:00"
+ "time": "2024-08-03T19:31:26+00:00"
},
{
"name": "automattic/vipwpcs",
@@ -392,25 +651,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 +687,8 @@
}
},
"autoload": {
- "psr-0": {
- "Behat\\Gherkin": "src/"
+ "psr-4": {
+ "Behat\\Gherkin\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -434,11 +699,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 +714,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 +1401,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 +1457,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 +1473,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 +2202,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 +2252,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 +2268,7 @@
"type": "tidelift"
}
],
- "time": "2022-12-30T00:15:36+00:00"
+ "time": "2022-12-30T00:23:10+00:00"
},
{
"name": "eftec/bladeone",
@@ -3178,25 +3443,27 @@
},
{
"name": "nikic/php-parser",
- "version": "v4.19.4",
+ "version": "v5.5.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2"
+ "reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
},
"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/ae59794362fe85e051a58ad36b289443f57be7a9",
+ "reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
"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 +3471,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.9-dev"
+ "dev-master": "5.0-dev"
}
},
"autoload": {
@@ -3228,9 +3495,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.5.0"
},
- "time": "2024-09-29T15:01:53+00:00"
+ "time": "2025-05-31T08:24:38+00:00"
},
{
"name": "phar-io/manifest",
@@ -3586,16 +3853,16 @@
},
{
"name": "php-stubs/wp-cli-stubs",
- "version": "v2.11.0",
+ "version": "v2.12.0",
"source": {
"type": "git",
"url": "https://github.com/php-stubs/wp-cli-stubs.git",
- "reference": "f27ff9e8e29d7962cb070e58de70dfaf63183007"
+ "reference": "af16401e299a3fd2229bd0fa9a037638a4174a9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-stubs/wp-cli-stubs/zipball/f27ff9e8e29d7962cb070e58de70dfaf63183007",
- "reference": "f27ff9e8e29d7962cb070e58de70dfaf63183007",
+ "url": "https://api.github.com/repos/php-stubs/wp-cli-stubs/zipball/af16401e299a3fd2229bd0fa9a037638a4174a9d",
+ "reference": "af16401e299a3fd2229bd0fa9a037638a4174a9d",
"shasum": ""
},
"require": {
@@ -3624,9 +3891,9 @@
],
"support": {
"issues": "https://github.com/php-stubs/wp-cli-stubs/issues",
- "source": "https://github.com/php-stubs/wp-cli-stubs/tree/v2.11.0"
+ "source": "https://github.com/php-stubs/wp-cli-stubs/tree/v2.12.0"
},
- "time": "2024-11-25T10:09:13+00:00"
+ "time": "2025-06-10T09:58:05+00:00"
},
{
"name": "php-webdriver/webdriver",
@@ -3937,29 +4204,29 @@
},
{
"name": "phpcsstandards/phpcsextra",
- "version": "1.3.0",
+ "version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHPCSExtra.git",
- "reference": "46d08eb86eec622b96c466adec3063adfed280dd"
+ "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/46d08eb86eec622b96c466adec3063adfed280dd",
- "reference": "46d08eb86eec622b96c466adec3063adfed280dd",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca",
+ "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca",
"shasum": ""
},
"require": {
"php": ">=5.4",
- "phpcsstandards/phpcsutils": "^1.0.9",
- "squizlabs/php_codesniffer": "^3.12.1"
+ "phpcsstandards/phpcsutils": "^1.1.0",
+ "squizlabs/php_codesniffer": "^3.13.0 || ^4.0"
},
"require-dev": {
"php-parallel-lint/php-console-highlighter": "^1.0",
- "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
"phpcsstandards/phpcsdevcs": "^1.1.6",
"phpcsstandards/phpcsdevtools": "^1.2.1",
- "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
+ "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
},
"type": "phpcodesniffer-standard",
"extra": {
@@ -4015,33 +4282,33 @@
"type": "thanks_dev"
}
],
- "time": "2025-04-20T23:35:32+00:00"
+ "time": "2025-06-14T07:40:39+00:00"
},
{
"name": "phpcsstandards/phpcsutils",
- "version": "1.0.12",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
- "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c"
+ "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c",
- "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad",
+ "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0",
"php": ">=5.4",
- "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev"
+ "squizlabs/php_codesniffer": "^3.13.0 || ^4.0"
},
"require-dev": {
"ext-filter": "*",
"php-parallel-lint/php-console-highlighter": "^1.0",
- "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
"phpcsstandards/phpcsdevcs": "^1.1.6",
- "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0"
+ "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0"
},
"type": "phpcodesniffer-standard",
"extra": {
@@ -4078,6 +4345,7 @@
"phpcodesniffer-standard",
"phpcs",
"phpcs3",
+ "phpcs4",
"standards",
"static analysis",
"tokens",
@@ -4101,9 +4369,13 @@
{
"url": "https://opencollective.com/php_codesniffer",
"type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
}
],
- "time": "2024-05-20T13:34:27+00:00"
+ "time": "2025-06-12T04:32:33+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@@ -4377,16 +4649,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 +4703,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 +5232,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 +5279,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 +5495,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 +5539,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 +5660,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 +6927,32 @@
},
{
"name": "slevomat/coding-standard",
- "version": "8.18.0",
+ "version": "8.19.1",
"source": {
"type": "git",
"url": "https://github.com/slevomat/coding-standard.git",
- "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593"
+ "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220"
},
"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/458d665acd49009efebd7e0cb385d71ae9ac3220",
+ "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220",
"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 +6976,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.19.1"
},
"funding": [
{
@@ -6639,33 +6988,32 @@
"type": "tidelift"
}
],
- "time": "2025-05-01T09:40:50+00:00"
+ "time": "2025-06-09T17:53:57+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 +7056,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 +7112,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,20 +7124,20 @@
"type": "github"
}
],
- "time": "2022-12-26T08:22:07+00:00"
+ "time": "2024-12-16T12:45:15+00:00"
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.13.0",
+ "version": "3.13.1",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "65ff2489553b83b4597e89c3b8b721487011d186"
+ "reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186",
- "reference": "65ff2489553b83b4597e89c3b8b721487011d186",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1b71b4dd7e7ef651ac749cea67e513c0c832f4bd",
+ "reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd",
"shasum": ""
},
"require": {
@@ -6856,7 +7208,7 @@
"type": "thanks_dev"
}
],
- "time": "2025-05-11T03:36:00+00:00"
+ "time": "2025-06-12T15:04:34+00:00"
},
{
"name": "symfony/browser-kit",
@@ -6932,38 +7284,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 +7339,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 +7355,7 @@
"type": "tidelift"
}
],
- "time": "2024-10-30T07:58:02+00:00"
+ "time": "2025-05-14T06:00:01+00:00"
},
{
"name": "symfony/console",
@@ -7176,20 +7524,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 +7546,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "2.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -7223,7 +7571,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 +7587,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 +7751,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 +7774,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "2.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -7462,7 +7807,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 +7823,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 +7873,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 +7889,7 @@
"type": "tidelift"
}
],
- "time": "2024-10-22T13:05:35+00:00"
+ "time": "2024-10-25T15:15:23+00:00"
},
{
"name": "symfony/finder",
@@ -8163,29 +8507,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 +8534,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 +8570,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 +8586,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 +8632,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 +8648,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 +8718,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 +8734,7 @@
"type": "tidelift"
}
],
- "time": "2024-11-10T20:33:58+00:00"
+ "time": "2025-04-18T15:23:29+00:00"
},
{
"name": "symfony/yaml",
@@ -8581,21 +8925,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 +8952,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 +8977,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 +9000,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 +9031,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",
@@ -11232,6 +11576,6 @@
"platform": {
"php": "^7.4 || ^8.0"
},
- "platform-dev": [],
+ "platform-dev": {},
"plugin-api-version": "2.6.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..c2b6d408
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php
@@ -0,0 +1,508 @@
+repository = $repository;
+
+ // Hook into WordPress admin
+ add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
+ add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
+
+ // Handle form submissions via admin-post.php
+ add_action( 'admin_post_graphql_webhook_save', [ $this, 'handle_webhook_save' ] );
+ add_action( 'admin_post_graphql_webhook_delete', [ $this, 'handle_webhook_delete' ] );
+
+ // Handle admin actions
+ add_action( 'admin_init', [ $this, 'handle_admin_actions' ] );
+
+ // Handle AJAX webhook test
+ add_action( 'wp_ajax_test_webhook', [ $this, 'ajax_test_webhook' ] );
+ }
+
+ /**
+ * Initialize admin hooks.
+ */
+ public function init(): void {
+ add_action( 'admin_init', [ $this, 'handle_actions' ] );
+ }
+
+ /**
+ * 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,
+ [ $this, 'render_admin_page' ]
+ );
+ }
+
+ /**
+ * Generate admin URL.
+ *
+ * @param array $args Query arguments.
+ * @return string Admin URL.
+ */
+ public function get_admin_url( array $args = [] ): string {
+ $defaults = [
+ 'page' => self::ADMIN_PAGE_SLUG,
+ ];
+ $args = array_merge( $defaults, $args );
+ return add_query_arg( $args, admin_url( 'admin.php' ) );
+ }
+
+ /**
+ * Enqueue admin assets.
+ *
+ * @param string $hook Current admin page hook.
+ */
+ public function enqueue_assets( string $hook ): void {
+ if ( 'graphql_page_' . self::ADMIN_PAGE_SLUG !== $hook ) {
+ return;
+ }
+
+ wp_enqueue_style(
+ 'graphql-webhooks-admin',
+ WPGRAPHQL_HEADLESS_WEBHOOKS_PLUGIN_URL . 'assets/css/admin.css',
+ [],
+ WPGRAPHQL_HEADLESS_WEBHOOKS_VERSION
+ );
+
+ wp_enqueue_script(
+ 'graphql-webhooks-admin',
+ WPGRAPHQL_HEADLESS_WEBHOOKS_PLUGIN_URL . 'assets/js/admin.js',
+ [ 'jquery' ],
+ WPGRAPHQL_HEADLESS_WEBHOOKS_VERSION,
+ true
+ );
+
+ wp_localize_script(
+ 'graphql-webhooks-admin',
+ 'wpGraphQLWebhooks',
+ [
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'restUrl' => rest_url( 'graphql-webhooks/v1/' ),
+ 'nonce' => wp_create_nonce( 'wp_rest' ),
+ 'confirmDelete' => __( 'Are you sure you want to delete this webhook?', 'wp-graphql-headless-webhooks' ),
+ 'headerTemplate' => $this->get_header_row_template(),
+ ]
+ );
+ }
+
+ /**
+ * Get header row template for JavaScript.
+ *
+ * @return string HTML template.
+ */
+ 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;
+ }
+
+ if ( isset( $_POST['action'] ) && 'save_webhook' === $_POST['action'] ) {
+ $this->handle_webhook_save();
+ }
+
+ if ( isset( $_GET['action'] ) && 'delete' === $_GET['action'] && isset( $_GET['webhook_id'] ) ) {
+ $this->handle_webhook_delete();
+ }
+ }
+
+ /**
+ * Verify admin permission.
+ *
+ * @return bool Whether user has permission.
+ */
+ private function verify_admin_permission(): bool {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( __( 'You do not have sufficient permissions to access this page.', 'wp-graphql-headless-webhooks' ) );
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Verify nonce.
+ *
+ * @param string $nonce_name Nonce name.
+ * @param string $action Nonce action.
+ * @return bool Whether nonce is valid.
+ */
+ private function verify_nonce( string $nonce_name, string $action ): bool {
+ if ( ! isset( $_REQUEST[ $nonce_name ] ) || ! wp_verify_nonce( $_REQUEST[ $nonce_name ], $action ) ) {
+ wp_die( __( 'Security check failed.', 'wp-graphql-headless-webhooks' ) );
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle webhook save
+ */
+ public function handle_webhook_save() {
+ // Verify permissions and nonce
+ if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'webhook_save', 'webhook_nonce' ) ) {
+ wp_die( __( 'Unauthorized', 'wp-graphql-webhooks' ) );
+ }
+
+ $webhook_id = isset( $_POST['webhook_id'] ) ? intval( $_POST['webhook_id'] ) : 0;
+ $data = [
+ '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' ),
+ 'headers' => $this->sanitize_headers( $_POST['webhook_headers'] ?? [] ),
+ ];
+
+ // Validate data
+ $validation = $this->repository->validate_data( $data );
+ if ( is_wp_error( $validation ) ) {
+ wp_die( $validation->get_error_message() );
+ }
+
+ // Save webhook
+ if ( $webhook_id > 0 ) {
+ $result = $this->repository->update( $webhook_id, $data );
+ $redirect_args = $result ? [ 'updated' => 1 ] : [ 'error' => 1 ];
+ } else {
+ $result = $this->repository->create( $data );
+ $redirect_args = $result ? [ 'added' => 1 ] : [ 'error' => 1 ];
+ }
+
+ // Redirect back to list page
+ wp_redirect( add_query_arg( $redirect_args, $this->get_admin_url() ) );
+ exit;
+ }
+
+ /**
+ * Handle webhook delete
+ */
+ public function handle_webhook_delete() {
+ // This method will be called via bulk actions from WP_List_Table
+ // Individual deletes are handled through the list table's handle_row_actions
+ }
+
+ /**
+ * Handle admin actions
+ */
+ public function handle_admin_actions() {
+ // Handle bulk actions from WP_List_Table
+ if ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ||
+ isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) {
+
+ if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'bulk-webhooks', '_wpnonce' ) ) {
+ return;
+ }
+
+ $webhook_ids = isset( $_REQUEST['webhook'] ) ? array_map( 'intval', (array) $_REQUEST['webhook'] ) : [];
+ $deleted = 0;
+
+ foreach ( $webhook_ids as $webhook_id ) {
+ if ( $this->repository->delete( $webhook_id ) ) {
+ $deleted++;
+ }
+ }
+
+ if ( $deleted > 0 ) {
+ wp_redirect( add_query_arg( [ 'deleted' => $deleted ], $this->get_admin_url() ) );
+ exit;
+ }
+ }
+ }
+
+ /**
+ * Render the admin page
+ */
+ public function render_admin_page() {
+ $action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : 'list';
+
+ switch ( $action ) {
+ case 'add':
+ case 'edit':
+ $this->render_form_page( $action );
+ break;
+ default:
+ $this->render_list_page();
+ break;
+ }
+ }
+
+ /**
+ * Render the list page using WP_List_Table
+ */
+ private function render_list_page() {
+ // Include the custom list table class
+ require_once __DIR__ . '/WebhooksListTable.php';
+
+ // Create an instance of our list table
+ $list_table = new WebhooksListTable( $this->repository );
+
+ // Include the list view template
+ include __DIR__ . '/views/webhooks-list.php';
+ }
+
+ /**
+ * Render the form page (add/edit)
+ *
+ * @param string $action The action (add or edit).
+ */
+ private function render_form_page( $action ) {
+ $webhook = null;
+ $form_title = 'add' === $action ? __( 'Add New Webhook', 'wp-graphql-webhooks' ) : __( 'Edit Webhook', 'wp-graphql-webhooks' );
+ $submit_text = 'add' === $action ? __( 'Add Webhook', 'wp-graphql-webhooks' ) : __( 'Update Webhook', 'wp-graphql-webhooks' );
+
+ // Default values for new webhook
+ $name = '';
+ $event = '';
+ $url = '';
+ $method = 'POST';
+ $headers = [];
+
+ if ( 'edit' === $action ) {
+ $webhook_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
+ $webhook = $this->repository->get( $webhook_id );
+
+ if ( ! $webhook ) {
+ wp_die( __( 'Webhook not found.', 'wp-graphql-webhooks' ) );
+ }
+
+ // Extract values from webhook entity
+ $name = $webhook->name;
+ $event = $webhook->event;
+ $url = $webhook->url;
+ $method = $webhook->method;
+ $headers = $webhook->headers;
+ }
+
+ $events = $this->repository->get_allowed_events();
+ $methods = $this->repository->get_allowed_methods();
+ $admin = $this; // Pass admin instance to template
+
+ include __DIR__ . '/views/webhook-form.php';
+ }
+
+ /**
+ * Sanitize headers
+ *
+ * @param array $headers Headers to sanitize.
+ * @return array Sanitized headers.
+ */
+ private function sanitize_headers( array $headers ): array {
+ $sanitized_headers = [];
+
+ // Handle the form data structure where headers come as separate arrays
+ if ( isset( $headers['name'] ) && isset( $headers['value'] ) ) {
+ $names = (array) $headers['name'];
+ $values = (array) $headers['value'];
+
+ foreach ( $names as $index => $name ) {
+ $name = sanitize_text_field( $name );
+ $value = sanitize_text_field( $values[ $index ] ?? '' );
+
+ if ( ! empty( $name ) && ! empty( $value ) ) {
+ $sanitized_headers[ $name ] = $value;
+ }
+ }
+ }
+
+ return $sanitized_headers;
+ }
+
+ /**
+ * Handle AJAX webhook test request.
+ */
+ public function ajax_test_webhook(): void {
+ // Verify nonce
+ if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'wp_rest' ) ) {
+ wp_send_json_error( [
+ 'message' => __( 'Invalid security token.', 'wp-graphql-headless-webhooks' ),
+ 'error_code' => 'invalid_nonce'
+ ] );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( [
+ 'message' => __( 'You do not have permission to test webhooks.', 'wp-graphql-headless-webhooks' ),
+ 'error_code' => 'insufficient_permissions'
+ ] );
+ }
+
+ // Get webhook ID
+ $webhook_id = isset( $_POST['webhook_id'] ) ? intval( $_POST['webhook_id'] ) : 0;
+ if ( ! $webhook_id ) {
+ wp_send_json_error( [
+ 'message' => __( 'Invalid webhook ID.', 'wp-graphql-headless-webhooks' ),
+ 'error_code' => 'invalid_webhook_id'
+ ] );
+ }
+
+ // Get webhook
+ $webhook = $this->repository->get( $webhook_id );
+ if ( ! $webhook ) {
+ wp_send_json_error( [
+ 'message' => __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ),
+ 'error_code' => 'webhook_not_found'
+ ] );
+ }
+
+ // Create test payload
+ $test_payload = [
+ 'event' => 'test_webhook',
+ 'timestamp' => current_time( 'mysql' ),
+ 'webhook' => [
+ 'id' => $webhook->id,
+ 'name' => $webhook->name,
+ 'url' => $webhook->url,
+ ],
+ 'test_data' => [
+ 'message' => 'This is a test webhook dispatch',
+ 'random' => wp_generate_password( 12, false ),
+ ],
+ ];
+
+ // Log the test attempt
+ error_log( sprintf(
+ '[WPGraphQL Webhooks] Testing webhook #%d (%s) to %s',
+ $webhook->id,
+ $webhook->name,
+ $webhook->url
+ ) );
+
+ // Prepare request args
+ $args = [
+ 'method' => $webhook->method,
+ 'timeout' => 15,
+ 'redirection' => 5,
+ 'httpversion' => '1.1',
+ 'blocking' => true, // We want to wait for the response
+ 'headers' => array_merge(
+ [
+ 'Content-Type' => 'application/json',
+ 'User-Agent' => 'WPGraphQL-Webhooks/' . WPGRAPHQL_HEADLESS_WEBHOOKS_VERSION,
+ ],
+ $webhook->headers
+ ),
+ 'body' => wp_json_encode( $test_payload ),
+ 'sslverify' => apply_filters( 'graphql_webhooks_sslverify', true ),
+ ];
+
+ // Add webhook metadata to headers
+ $args['headers']['X-WPGraphQL-Webhook-Event'] = 'test_webhook';
+ $args['headers']['X-WPGraphQL-Webhook-ID'] = (string) $webhook->id;
+
+ // Start timing
+ $start_time = microtime( true );
+
+ // Make the request
+ $response = wp_remote_request( $webhook->url, $args );
+
+ // Calculate duration
+ $duration_ms = round( ( microtime( true ) - $start_time ) * 1000, 2 );
+
+ // Check for errors
+ if ( is_wp_error( $response ) ) {
+ error_log( sprintf(
+ '[WPGraphQL Webhooks] Test failed for webhook #%d: %s',
+ $webhook->id,
+ $response->get_error_message()
+ ) );
+
+ wp_send_json_error( [
+ 'message' => sprintf(
+ __( 'Failed to send test webhook: %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 );
+
+ // Log the response
+ error_log( sprintf(
+ '[WPGraphQL Webhooks] Test response for webhook #%d: HTTP %d in %sms',
+ $webhook->id,
+ $response_code,
+ $duration_ms
+ ) );
+
+ // Determine if successful (2xx status codes)
+ $is_success = $response_code >= 200 && $response_code < 300;
+
+ // Prepare response data
+ $response_data = [
+ 'success' => $is_success,
+ 'message' => $is_success
+ ? sprintf( __( 'Test webhook sent successfully to %s', 'wp-graphql-headless-webhooks' ), $webhook->url )
+ : sprintf( __( 'Webhook returned HTTP %d', 'wp-graphql-headless-webhooks' ), $response_code ),
+ 'webhook_id' => $webhook->id,
+ 'webhook_name' => $webhook->name,
+ 'target_url' => $webhook->url,
+ 'method' => $webhook->method,
+ 'response_code' => $response_code,
+ 'duration_ms' => $duration_ms,
+ 'timestamp' => current_time( 'c' ),
+ 'test_payload' => $test_payload,
+ ];
+
+ // Add response body if available (limit to 1000 chars for UI)
+ if ( ! empty( $response_body ) ) {
+ $response_data['response_body'] = strlen( $response_body ) > 1000
+ ? substr( $response_body, 0, 1000 ) . '...'
+ : $response_body;
+ }
+
+ // Send success response (even if webhook returned non-2xx, the test itself succeeded)
+ wp_send_json_success( $response_data );
+ }
+}
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksListTable.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksListTable.php
new file mode 100644
index 00000000..1e1e9aea
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksListTable.php
@@ -0,0 +1,264 @@
+repository = $repository;
+
+ parent::__construct( [
+ 'singular' => __( 'Webhook', 'wp-graphql-webhooks' ),
+ 'plural' => __( 'Webhooks', 'wp-graphql-webhooks' ),
+ 'ajax' => false,
+ ] );
+ }
+
+ /**
+ * Get columns
+ *
+ * @return array
+ */
+ public function get_columns() {
+ return [
+ 'cb' => '',
+ 'name' => __( 'Name', 'wp-graphql-webhooks' ),
+ 'event' => __( 'Event', 'wp-graphql-webhooks' ),
+ 'method' => __( 'Method', 'wp-graphql-webhooks' ),
+ 'url' => __( 'URL', 'wp-graphql-webhooks' ),
+ 'headers' => __( 'Headers', 'wp-graphql-webhooks' ),
+ ];
+ }
+
+ /**
+ * Get sortable columns
+ *
+ * @return array
+ */
+ public function get_sortable_columns() {
+ return [
+ 'name' => [ 'name', false ],
+ 'event' => [ 'event', false ],
+ 'method' => [ 'method', false ],
+ ];
+ }
+
+ /**
+ * Get bulk actions
+ *
+ * @return array
+ */
+ public function get_bulk_actions() {
+ return [
+ 'bulk-delete' => __( 'Delete', 'wp-graphql-webhooks' ),
+ ];
+ }
+
+ /**
+ * Process bulk actions
+ */
+ public function process_bulk_action() {
+ // Handle bulk delete
+ if ( 'bulk-delete' === $this->current_action() ) {
+ $webhook_ids = isset( $_POST['webhook'] ) ? array_map( 'intval', $_POST['webhook'] ) : [];
+
+ if ( ! empty( $webhook_ids ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'bulk-' . $this->_args['plural'] ) ) {
+ foreach ( $webhook_ids as $id ) {
+ $this->repository->delete( $id );
+ }
+
+ wp_redirect( add_query_arg( 'deleted', count( $webhook_ids ), remove_query_arg( [ 'action', 'webhook', '_wpnonce' ] ) ) );
+ exit;
+ }
+ }
+
+ // Handle single delete
+ if ( 'delete' === $this->current_action() ) {
+ $webhook_id = isset( $_GET['webhook'] ) ? intval( $_GET['webhook'] ) : 0;
+ $nonce = isset( $_GET['_wpnonce'] ) ? $_GET['_wpnonce'] : '';
+
+ if ( $webhook_id && wp_verify_nonce( $nonce, 'delete-webhook-' . $webhook_id ) ) {
+ $this->repository->delete( $webhook_id );
+ wp_redirect( add_query_arg( 'deleted', 1, remove_query_arg( [ 'action', 'webhook', '_wpnonce' ] ) ) );
+ exit;
+ }
+ }
+ }
+
+ /**
+ * Prepare items for display
+ */
+ public function prepare_items() {
+ $this->process_bulk_action();
+
+ $per_page = $this->get_items_per_page( 'webhooks_per_page', 20 );
+ $current_page = $this->get_pagenum();
+
+ // Get all webhooks
+ $webhooks = $this->repository->get_all();
+ $total_items = count( $webhooks );
+
+ // Handle sorting
+ $orderby = ! empty( $_GET['orderby'] ) ? $_GET['orderby'] : 'name';
+ $order = ! empty( $_GET['order'] ) ? $_GET['order'] : 'asc';
+
+ usort( $webhooks, function( $a, $b ) use ( $orderby, $order ) {
+ $result = 0;
+
+ switch ( $orderby ) {
+ case 'name':
+ $result = strcmp( $a->name, $b->name );
+ break;
+ case 'event':
+ $result = strcmp( $a->event, $b->event );
+ break;
+ case 'method':
+ $result = strcmp( $a->method, $b->method );
+ break;
+ }
+
+ return ( 'asc' === $order ) ? $result : -$result;
+ } );
+
+ // Pagination
+ $this->items = array_slice( $webhooks, ( $current_page - 1 ) * $per_page, $per_page );
+
+ $this->set_pagination_args( [
+ 'total_items' => $total_items,
+ 'per_page' => $per_page,
+ 'total_pages' => ceil( $total_items / $per_page ),
+ ] );
+
+ $columns = $this->get_columns();
+ $hidden = [];
+ $sortable = $this->get_sortable_columns();
+
+ $this->_column_headers = [ $columns, $hidden, $sortable ];
+ }
+
+ /**
+ * Default column renderer
+ *
+ * @param object $item Webhook item.
+ * @param string $column_name Column name.
+ * @return string
+ */
+ public function column_default( $item, $column_name ) {
+ switch ( $column_name ) {
+ case 'name':
+ return esc_html( $item->name );
+ case 'event':
+ $events = $this->repository->get_allowed_events();
+ return esc_html( $events[ $item->event ] ?? $item->event );
+ case 'url':
+ return '' . esc_html( $item->url ) . '';
+ case 'method':
+ return '' . esc_html( $item->method ) . '';
+ case 'headers':
+ $count = is_array( $item->headers ) ? count( $item->headers ) : 0;
+ return $count > 0 ? sprintf( __( '%d headers', 'wp-graphql-webhooks' ), $count ) : '—';
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Checkbox column
+ *
+ * @param object $item Webhook item.
+ * @return string
+ */
+ public function column_cb( $item ) {
+ return sprintf(
+ '',
+ $item->id
+ );
+ }
+
+ /**
+ * Name column with row actions
+ *
+ * @param object $item Webhook item.
+ * @return string
+ */
+ public function column_name( $item ) {
+ $edit_url = add_query_arg( [
+ 'page' => 'graphql-webhooks',
+ 'action' => 'edit',
+ 'id' => $item->id,
+ ], admin_url( 'admin.php' ) );
+
+ $delete_url = wp_nonce_url(
+ add_query_arg( [
+ 'page' => 'graphql-webhooks',
+ 'action' => 'delete',
+ 'webhook' => $item->id,
+ ], admin_url( 'admin.php' ) ),
+ 'delete-webhook-' . $item->id
+ );
+
+ $actions = [
+ 'edit' => sprintf( '%s', esc_url( $edit_url ), __( 'Edit', 'wp-graphql-webhooks' ) ),
+ 'test' => sprintf( '%s', $item->id, __( 'Test', 'wp-graphql-webhooks' ) ),
+ 'delete' => sprintf( '%s', esc_url( $delete_url ), __( 'Delete', 'wp-graphql-webhooks' ) ),
+ ];
+
+ return sprintf(
+ '%s%s',
+ esc_url( $edit_url ),
+ esc_html( $item->name ),
+ $this->row_actions( $actions )
+ );
+ }
+
+ /**
+ * Display when no items
+ */
+ public function no_items() {
+ _e( 'No webhooks found.', 'wp-graphql-webhooks' );
+ }
+
+ /**
+ * Extra controls to be displayed between bulk actions and pagination
+ *
+ * @param string $which Top or bottom.
+ */
+ protected function extra_tablenav( $which ) {
+ if ( 'top' === $which ) {
+ ?>
+
+
+
+
+
+
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..cc9e7abd
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php
@@ -0,0 +1,124 @@
+
+
+
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..9af6f985
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventMapper.php b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventMapper.php
new file mode 100644
index 00000000..cf4b07e5
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventMapper.php
@@ -0,0 +1,125 @@
+
+ */
+ private static $event_map = [
+ // Post Events (lowercase and uppercase variants)
+ 'post_create' => 'post_published',
+ 'post_update' => 'post_updated',
+ 'post_delete' => 'post_deleted',
+ 'post_updated' => 'post_updated',
+ 'post_deleted' => 'post_deleted',
+ 'post_CREATE' => 'post_published',
+ 'post_UPDATE' => 'post_updated',
+ 'post_DELETE' => 'post_deleted',
+ 'post_reassigned_to_user' => 'post_updated',
+ 'postmeta_changed (meta_key' => 'post_meta_change', // This will match partial string
+
+ // Term Events
+ 'term_created' => 'term_created',
+ 'term_updated' => 'term_updated',
+ 'term_saved' => 'term_updated',
+ 'term_deleted' => 'term_deleted',
+ 'term_CREATE' => 'term_created',
+ 'term_UPDATE' => 'term_updated',
+ 'term_DELETE' => 'term_deleted',
+ 'term_relationship_added' => 'term_assigned',
+ 'term_relationship_deleted' => 'term_unassigned',
+
+ // User Events
+ 'user_profile_updated' => 'user_updated',
+ 'user_meta_updated' => 'user_updated',
+ 'user_deleted' => 'user_deleted',
+ 'user_reassigned' => 'user_reassigned',
+ 'user_UPDATE' => 'user_updated',
+ 'user_DELETE' => 'user_deleted',
+
+ // Menu Events
+ 'updated_nav_menu' => 'menu_updated',
+ 'nav_menu_created' => 'menu_created',
+ 'set_nav_menu_location' => 'menu_updated',
+ 'menu_meta_updated' => 'menu_updated',
+ 'nav_menu_item_added' => 'menu_item_created',
+ 'update_menu_item' => 'menu_item_updated',
+ 'nav_menu_item_deleted' => 'menu_item_deleted',
+ 'menu_item_meta_changed' => 'menu_item_updated',
+
+ // Media Events
+ 'add_attachment' => 'media_uploaded',
+ 'attachment_edited' => 'media_updated',
+ 'attachment_deleted' => 'media_deleted',
+ 'media_UPDATE' => 'media_updated',
+ 'media_DELETE' => 'media_deleted',
+
+ // Comment Events
+ 'comment_transition' => 'comment_status',
+ 'comment_approved' => 'comment_inserted',
+ 'comment_UPDATE' => 'comment_status',
+ 'comment_DELETE' => 'comment_status',
+
+ // Cache Events
+ 'purge all' => 'cache_purged',
+
+ // Node type mappings (for handle_purge_nodes)
+ 'post' => 'post_updated',
+ 'term' => 'term_updated',
+ 'user' => 'user_updated',
+ 'comment' => 'comment_status',
+ 'mediaitem' => 'media_updated',
+ 'menu' => 'menu_updated',
+ 'menuitem' => 'menu_item_updated',
+ ];
+
+ /**
+ * Get the mapped webhook event key for a given Smart Cache event.
+ *
+ * @param string $smart_cache_event
+ * @return string|null Returns mapped event key or null if no mapping found.
+ */
+ public static function mapEvent( string $smart_cache_event ): ?string {
+ // First try direct lookup
+ if ( isset( self::$event_map[ $smart_cache_event ] ) ) {
+ return self::$event_map[ $smart_cache_event ];
+ }
+
+ // Try lowercase version
+ $lowercase_event = strtolower( $smart_cache_event );
+ if ( isset( self::$event_map[ $lowercase_event ] ) ) {
+ return self::$event_map[ $lowercase_event ];
+ }
+
+ // Handle postmeta_changed partial match
+ if ( strpos( $smart_cache_event, 'postmeta_changed (meta_key' ) === 0 ) {
+ return 'post_meta_change';
+ }
+
+ // Handle list: prefixed events (from purge method calls)
+ if ( strpos( $smart_cache_event, 'list:' ) === 0 ) {
+ $type = substr( $smart_cache_event, 5 );
+ return self::mapEvent( $type ); // Recursive call to handle the type
+ }
+
+ // Handle skipped: prefixed events
+ if ( strpos( $smart_cache_event, 'skipped:' ) === 0 ) {
+ $type = substr( $smart_cache_event, 8 );
+ return self::mapEvent( $type ); // Recursive call to handle the type
+ }
+
+ return null;
+ }
+
+ /**
+ * Get all mapped webhook events.
+ *
+ * @return array
+ */
+ public static function getMappedEvents(): array {
+ return self::$event_map;
+ }
+}
\ No newline at end of file
diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheWebhookManager.php b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheWebhookManager.php
new file mode 100644
index 00000000..7db05313
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheWebhookManager.php
@@ -0,0 +1,256 @@
+repository = $repository;
+ $this->handler = $handler;
+ }
+
+ /**
+ * Register Smart Cache purge hooks
+ */
+ public function register_hooks(): void {
+ add_action( 'wpgraphql_cache_purge_nodes', [ $this, 'handle_purge_nodes' ], 10, 2 );
+ add_action( 'graphql_purge', [ $this, 'handle_graphql_purge' ], 10, 3 );
+ }
+
+ /**
+ * Handle node purge events from Smart Cache
+ */
+ public function handle_purge_nodes( string $key, array $nodes ): void {
+ error_log( "[Webhook] handle_purge_nodes - Key: $key, Node count: " . count( $nodes ) );
+
+ // Handle empty nodes array
+ if ( empty( $nodes ) ) {
+ error_log( "[Webhook] handle_purge_nodes - No nodes provided for key: $key" );
+ return;
+ }
+
+ $node_type = $nodes[0]['type'] ?? null;
+
+ if ( empty( $node_type ) ) {
+ error_log( "[Webhook] handle_purge_nodes - No node type found in first node for key: $key" );
+ return;
+ }
+
+ $event = SmartCacheEventMapper::mapEvent( strtolower( $node_type ) );
+
+ if ( $event === null ) {
+ error_log( "[Webhook] handle_purge_nodes - No mapped event found for node type: $node_type" );
+ return;
+ }
+
+ error_log( "[Webhook] handle_purge_nodes - Mapped '$node_type' to event: $event" );
+
+ $path = $this->get_path_from_key( $key );
+ $smart_cache_keys = $this->get_smart_cache_keys( $nodes );
+
+ $this->trigger_webhooks( $event, [
+ 'key' => $key,
+ 'path' => $path,
+ 'nodes' => $nodes,
+ 'smart_cache_keys' => $smart_cache_keys
+ ] );
+ }
+
+ /**
+ * Handle general purge events from Smart Cache
+ */
+ public function handle_graphql_purge( string $key, string $event, string $graphql_endpoint ): void {
+ error_log( "[Webhook] handle_graphql_purge - Key: $key, Event: $event, Endpoint: $graphql_endpoint" );
+
+ // Skip special prefixed keys (they're not actual entity IDs)
+ if ( strpos( $key, 'skipped:' ) === 0 || strpos( $key, 'list:' ) === 0 ) {
+ error_log( "[Webhook] Skipping webhook trigger for special key: $key" );
+ return;
+ }
+ $mapped_event = SmartCacheEventMapper::mapEvent( strtolower( $event ) );
+
+ if ( $mapped_event === null ) {
+ error_log( "[Webhook] handle_graphql_purge - No mapped event found for Smart Cache event: $event" );
+ return;
+ }
+
+ error_log( "[Webhook] handle_graphql_purge - Mapped '$event' to event: $mapped_event" );
+
+ $path = $this->get_path_from_key( $key );
+
+ $this->trigger_webhooks( $mapped_event, [
+ 'key' => $key,
+ 'path' => $path,
+ 'graphql_endpoint' => $graphql_endpoint,
+ 'smart_cache_keys' => [ $key ]
+ ] );
+ }
+
+ /**
+ * Trigger webhooks with Smart Cache formatted payload
+ */
+ private function trigger_webhooks( string $event, array $payload ): void {
+ // Event is already mapped, no need to map again
+ $allowed_events = $this->repository->get_allowed_events();
+
+ if ( ! array_key_exists( $event, $allowed_events ) ) {
+ error_log( "[Webhook] Event '$event' is not in allowed events list." );
+ return;
+ }
+
+ // Set uri fallback if smart_cache_keys is empty
+ if ( empty( $payload['smart_cache_keys'] ) ) {
+ $payload['uri'] = $payload['path'] ?? '';
+ }
+
+ error_log( "[Webhook] Triggering webhooks for event: $event with payload: " . var_export( $payload, true ) );
+
+ do_action( 'graphql_webhooks_before_trigger', $event, $payload );
+
+ $webhooks = $this->repository->get_all();
+ error_log( "[Webhook] Found " . count( $webhooks ) . " webhooks for event: $event" );
+ $triggered_count = 0;
+
+ foreach ( $webhooks as $webhook ) {
+ if ( $webhook->event === $event ) {
+ $this->handler->handle( $webhook, $payload );
+ $triggered_count++;
+ }
+ }
+
+ error_log( "[Webhook] Triggered $triggered_count webhooks for event: $event" );
+
+ do_action( 'graphql_webhooks_after_trigger', $event, $payload );
+ }
+
+ /**
+ * Extract Smart Cache keys from nodes
+ */
+ private function get_smart_cache_keys( array $nodes ): array {
+ $keys = [];
+
+ foreach ( $nodes as $node ) {
+ if ( isset( $node['id'] ) && ! empty( $node['id'] ) ) {
+ $keys[] = $node['id'];
+ } elseif ( isset( $node['databaseId'] ) && ! empty( $node['databaseId'] ) ) {
+ // Fallback to databaseId if id is not available
+ $keys[] = $node['databaseId'];
+ }
+ }
+
+ return array_filter( $keys ); // Remove empty values
+ }
+
+ /**
+ * Get the path from the key
+ *
+ * Supports all post types, terms, users, and falls back gracefully.
+ * Handles special prefixed keys like 'skipped:post', 'list:post', etc.
+ *
+ * @param string $key The key to get the path from
+ *
+ * @return string
+ */
+ public function get_path_from_key( $key ) {
+ $path = '';
+
+ if ( empty( $key ) ) {
+ error_log( "[Webhook] Empty key provided to get_path_from_key" );
+ return $path;
+ }
+
+ // Handle special prefixed keys (skipped:, list:, etc.)
+ if ( strpos( $key, ':' ) !== false ) {
+ error_log( "[Webhook] Prefixed key detected: $key - cannot generate path for non-entity keys" );
+ return $path;
+ }
+
+ try {
+ $node_id = Relay::fromGlobalId( $key );
+ } catch (Exception $e) {
+ error_log( "[Webhook] Failed to decode GraphQL global ID: $key - " . $e->getMessage() );
+ return $path;
+ }
+
+ $node_type = $node_id['type'] ?? null;
+ $database_id = $node_id['id'] ?? null;
+
+ if ( empty( $node_type ) || empty( $database_id ) ) {
+ error_log( "[Webhook] Invalid node ID structure for key: $key (type: $node_type, id: $database_id)" );
+ return $path;
+ }
+
+ $permalink = null;
+ error_log( "[Webhook] Processing key: $key (type: $node_type, database_id: $database_id)" );
+
+ switch ( $node_type ) {
+ case 'post':
+ case 'page':
+ default:
+ $post_id = absint( $database_id );
+ if ( $post_id > 0 ) {
+ $post = get_post( $post_id );
+ if ( $post && ! is_wp_error( $post ) ) {
+ $permalink = get_permalink( $post_id );
+ error_log( "[Webhook] Generated permalink for post $post_id: $permalink" );
+ } else {
+ error_log( "[Webhook] Post not found or error for ID: $post_id" );
+ }
+ }
+ break;
+
+ case 'term':
+ $term_id = absint( $database_id );
+ if ( $term_id > 0 ) {
+ $term = get_term( $term_id );
+ if ( $term && ! is_wp_error( $term ) ) {
+ $permalink = get_term_link( $term_id );
+ error_log( "[Webhook] Generated permalink for term $term_id: $permalink" );
+ } else {
+ error_log( "[Webhook] Term not found or error for ID: $term_id" );
+ }
+ }
+ break;
+
+ case 'user':
+ $user_id = absint( $database_id );
+ if ( $user_id > 0 ) {
+ $user = get_user_by( 'id', $user_id );
+ if ( $user instanceof \WP_User ) {
+ $permalink = home_url( '/author/' . $user->user_nicename . '/' );
+ error_log( "[Webhook] Generated permalink for user $user_id: $permalink" );
+ } else {
+ error_log( "[Webhook] User not found for ID: $user_id" );
+ }
+ }
+ break;
+ }
+
+ if ( ! empty( $permalink ) && is_string( $permalink ) && ! is_wp_error( $permalink ) ) {
+ $parsed_path = parse_url( $permalink, PHP_URL_PATH );
+ if ( $parsed_path !== false ) {
+ $path = $parsed_path;
+ error_log( "[Webhook] Final path for key $key: $path" );
+ } else {
+ error_log( "[Webhook] Failed to parse URL path from permalink: $permalink" );
+ }
+ } else {
+ error_log( "[Webhook] No valid permalink generated for key: $key" );
+ }
+
+ return $path;
+ }
+}
\ No newline at end of file
diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php
index 3776fae7..207696a9 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php
@@ -47,6 +47,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 );
+
+ // Register test webhook event handler
+ add_action( 'graphql_webhooks_test_event', [ $this, 'on_test_webhook' ], 10, 2 );
}
/**
@@ -177,4 +180,22 @@ public function on_comment_status( $new_status, $old_status, $comment ) {
'new_status' => $new_status,
] );
}
+
+ /**
+ * Handle test webhook event.
+ *
+ * @param \WPGraphQL\Webhooks\Entity\Webhook $webhook The webhook being tested.
+ * @param array $payload The test payload.
+ */
+ public function on_test_webhook( $webhook, $payload = [] ) {
+ // For test webhooks, we want to send directly to the specific webhook
+ // Enable test mode for blocking requests during tests
+ add_filter( 'graphql_webhooks_test_mode', '__return_true' );
+
+ // Send the webhook with the test payload
+ $this->handler->handle( $webhook, $payload );
+
+ // Remove the test mode filter
+ remove_filter( 'graphql_webhooks_test_mode', '__return_true' );
+ }
}
\ No newline at end of file
diff --git a/plugins/wp-graphql-headless-webhooks/src/Handlers/WebhookHandler.php b/plugins/wp-graphql-headless-webhooks/src/Handlers/WebhookHandler.php
index d546baca..eecd7d12 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Handlers/WebhookHandler.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Handlers/WebhookHandler.php
@@ -20,25 +20,90 @@ class WebhookHandler implements Handler {
* @return void
*/
public function handle( Webhook $webhook, array $payload ): void {
+ // Log webhook dispatch initiation
+ $dispatch_timestamp = current_time( 'mysql' );
+ error_log( "\n========== WEBHOOK DISPATCH ==========" );
+ error_log( "Timestamp: {$dispatch_timestamp}" );
+ error_log( "Webhook: {$webhook->name} (ID: {$webhook->id})" );
+ error_log( "Event: {$webhook->event}" );
+ error_log( "Target URL: {$webhook->url}" );
+ error_log( "Method: {$webhook->method}" );
+
$args = [
'headers' => $webhook->headers ?: [ 'Content-Type' => 'application/json' ],
- 'timeout' => 5,
+ 'timeout' => apply_filters( 'graphql_webhooks_timeout', 15 ), // Configurable timeout with a default of 15 seconds
'blocking' => false,
+ 'sslverify' => apply_filters( 'graphql_webhooks_sslverify', true, $webhook ),
+ 'user-agent' => 'WPGraphQL-Webhooks/' . ( defined( 'WPGRAPHQL_WEBHOOKS_VERSION' ) ? WPGRAPHQL_WEBHOOKS_VERSION : '1.0.0' ),
+ ];
+
+ // Apply payload filter
+ $payload = apply_filters( 'graphql_webhooks_payload', $payload, $webhook );
+
+ // Add webhook metadata to payload
+ $payload['_webhook_meta'] = [
+ 'sent_at' => $dispatch_timestamp,
+ 'webhook_id' => $webhook->id,
+ 'webhook_name' => $webhook->name,
+ 'event_type' => $webhook->event,
];
- $payload = apply_filters( 'graphql_webhooks_payload', $payload, $webhook );
+ // Handle different HTTP methods
if ( strtoupper( $webhook->method ) === 'GET' ) {
$url = add_query_arg( $payload, $webhook->url );
$args['method'] = 'GET';
+ error_log( "Payload (GET query params): " . wp_json_encode( $payload ) );
} else {
$url = $webhook->url;
- $args['method'] = 'POST';
+ $args['method'] = strtoupper( $webhook->method );
$args['body'] = wp_json_encode( $payload );
+
+ // Ensure Content-Type header is set for non-GET requests
if ( empty( $args['headers']['Content-Type'] ) ) {
$args['headers']['Content-Type'] = 'application/json';
}
+
+ error_log( "Payload ({$args['method']} body): " . $args['body'] );
+ error_log( "Payload size: " . strlen( $args['body'] ) . " bytes" );
+ }
+
+ // Log headers
+ error_log( "Headers: " . wp_json_encode( $args['headers'] ) );
+
+ // For test mode or debugging, optionally use blocking mode
+ if ( apply_filters( 'graphql_webhooks_test_mode', false, $webhook ) ) {
+ $args['blocking'] = true;
+ error_log( "Test mode enabled - using blocking request" );
+ }
+
+ error_log( "====================================\n" );
+
+ // Send the webhook
+ $start_time = microtime( true );
+ $response = wp_remote_request( $url, $args );
+ $end_time = microtime( true );
+ $duration = round( ( $end_time - $start_time ) * 1000, 2 );
+
+ // Log response if in blocking mode
+ if ( $args['blocking'] ) {
+ if ( is_wp_error( $response ) ) {
+ error_log( "\n========== WEBHOOK ERROR ==========" );
+ error_log( "❌ ERROR: " . $response->get_error_message() );
+ error_log( "Duration: {$duration}ms" );
+ error_log( "==================================\n" );
+ } else {
+ $response_code = wp_remote_retrieve_response_code( $response );
+ $response_body = wp_remote_retrieve_body( $response );
+
+ error_log( "\n========== WEBHOOK RESPONSE ==========" );
+ error_log( ( $response_code >= 200 && $response_code < 300 ? "✅" : "⚠️" ) . " Response Code: {$response_code}" );
+ error_log( "Duration: {$duration}ms" );
+ error_log( "Response Body: " . ( strlen( $response_body ) > 500 ? substr( $response_body, 0, 500 ) . '...' : $response_body ) );
+ error_log( "====================================\n" );
+ }
}
- wp_remote_request( $url, $args );
- do_action( 'graphql_webhooks_sent', $webhook, $payload );
+
+ // Trigger action after webhook is sent
+ do_action( 'graphql_webhooks_sent', $webhook, $payload, $response );
}
}
\ No newline at end of file
diff --git a/plugins/wp-graphql-headless-webhooks/src/Plugin.php b/plugins/wp-graphql-headless-webhooks/src/Plugin.php
index 4eba694a..51d49a1a 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Plugin.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Plugin.php
@@ -14,6 +14,9 @@
use WPGraphQL\Webhooks\PostTypes\WebhookPostType;
use WPGraphQL\Webhooks\Repository\WebhookRepository;
use WPGraphQL\Webhooks\Events\WebhookEventManager;
+use WPGraphQL\Webhooks\Events\SmartCacheWebhookManager;
+use WPGraphQL\Webhooks\Services\Interfaces\ServiceLocator;
+use WPGraphQL\Webhooks\Services\PluginServiceLocator;
/**
* Plugin singleton class.
@@ -30,25 +33,11 @@ final class Plugin {
private static ?self $instance = null;
/**
- * Webhook repository.
+ * Service locator instance.
*
- * @var WebhookRepository
+ * @var ServiceLocator
*/
- private WebhookRepository $repository;
-
- /**
- * Webhook handler.
- *
- * @var WebhookHandler
- */
- private WebhookHandler $handler;
-
- /**
- * Webhook event manager.
- *
- * @var WebhookEventManager
- */
- private WebhookEventManager $event_manager;
+ private ServiceLocator $services;
/**
* Get singleton instance.
@@ -72,17 +61,54 @@ public static function instance(): self {
return self::$instance;
}
- /**
- * Setup plugin.
- */
private function setup(): void {
Helper::set_hook_prefix( 'graphql_webhooks' );
WebhookPostType::init();
- $this->repository = new WebhookRepository();
- $this->handler = new WebhookHandler();
- $this->event_manager = new WebhookEventManager( $this->repository, $this->handler );
- $this->event_manager->register_hooks();
+ $this->services = new PluginServiceLocator();
+
+ // Register services
+ $this->services->set( 'repository', function () {
+ return new WebhookRepository();
+ } );
+
+ $this->services->set( 'handler', function () {
+ return new WebhookHandler();
+ } );
+
+ $this->services->set( 'event_manager', function () {
+ $repository = $this->services->get( 'repository' );
+ $handler = $this->services->get( 'handler' );
+
+ if ( class_exists( 'WPGraphQL\SmartCache\Document' ) || defined( 'WPGRAPHQL_SMART_CACHE_VERSION' ) ) {
+ return new SmartCacheWebhookManager( $repository, $handler );
+ }
+
+ return new WebhookEventManager( $repository, $handler );
+ } );
+ // Initialize event manager and register hooks
+ $eventManager = $this->services->get( 'event_manager' );
+ $eventManager->register_hooks();
+
+ // Initialize admin UI
+ if ( is_admin() ) {
+ $repository = $this->services->get( 'repository' );
+
+ if ( class_exists( 'WPGraphQL\Webhooks\Admin\WebhooksAdmin' ) ) {
+ $admin = new \WPGraphQL\Webhooks\Admin\WebhooksAdmin( $repository );
+ $admin->init();
+ }
+ }
+
+ // Initialize REST endpoints
+ add_action( 'rest_api_init', function () {
+ $repository = $this->services->get( 'repository' );
+
+ if ( class_exists( 'WPGraphQL\Webhooks\Rest\WebhookTestEndpoint' ) ) {
+ $testEndpoint = new \WPGraphQL\Webhooks\Rest\WebhookTestEndpoint( $repository );
+ $testEndpoint->register_routes();
+ }
+ } );
}
/**
@@ -97,7 +123,7 @@ private function includes(): void {
require_once WPGRAPHQL_HEADLESS_WEBHOOKS_PLUGIN_DIR . 'vendor/autoload.php';
}
}
-
+
/**
* Get the webhook repository instance.
*
@@ -106,7 +132,7 @@ private function includes(): void {
* @return WebhookRepository The repository instance.
*/
public function get_repository(): WebhookRepository {
- return $this->repository;
+ return $this->services->get( 'repository' );
}
/**
diff --git a/plugins/wp-graphql-headless-webhooks/src/Repository/Interfaces/WebhookRepositoryInterface.php b/plugins/wp-graphql-headless-webhooks/src/Repository/Interfaces/WebhookRepositoryInterface.php
index 422c1f06..885eeb9e 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Repository/Interfaces/WebhookRepositoryInterface.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Repository/Interfaces/WebhookRepositoryInterface.php
@@ -29,29 +29,31 @@ public function get(int $id): ?Webhook;
/**
* Creates a new webhook.
*
- * @param string $name The name (title) of the webhook.
- * @param string $event The event identifier the webhook listens to.
- * @param string $url The target URL the webhook will send data to.
- * @param string $method The HTTP method to use when sending the webhook (e.g., 'POST').
- * @param array $headers Optional associative array of headers to send with the request.
+ * @param array $data The webhook data containing:
+ * - name (string): The name (title) of the webhook.
+ * - event (string): The event identifier the webhook listens to.
+ * - url (string): The target URL the webhook will send data to.
+ * - method (string): The HTTP method to use when sending the webhook (e.g., 'POST').
+ * - headers (array): Optional associative array of headers to send with the request.
*
* @return int|\WP_Error The new webhook's post ID on success, or WP_Error on failure.
*/
- public function create(string $name, string $event, string $url, string $method, array $headers);
+ public function create(array $data);
/**
* Updates an existing webhook.
*
- * @param int $id The webhook post ID.
- * @param string $name The updated name (title) of the webhook.
- * @param string $event The updated event identifier.
- * @param string $url The updated target URL.
- * @param string $method The updated HTTP method.
- * @param array $headers The updated array of headers.
+ * @param int $id The webhook post ID.
+ * @param array $data The webhook data containing:
+ * - name (string): The updated name (title) of the webhook.
+ * - event (string): The updated event identifier.
+ * - url (string): The updated target URL.
+ * - method (string): The updated HTTP method.
+ * - headers (array): The updated array of headers.
*
* @return bool|\WP_Error True on success, or WP_Error on failure.
*/
- public function update(int $id, string $name, string $event, string $url, string $method, array $headers);
+ public function update(int $id, array $data);
/**
* Deletes a webhook by its ID.
diff --git a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php
index 05cfe9dc..2f48a191 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php
@@ -2,6 +2,7 @@
namespace WPGraphQL\Webhooks\Repository;
use WPGraphQL\Webhooks\Entity\Webhook;
+use WPGraphQL\Webhooks\Events\SmartCacheEventMapper;
use WPGraphQL\Webhooks\Repository\Interfaces\WebhookRepositoryInterface;
use WP_Error;
use WP_Post;
@@ -18,212 +19,246 @@
*/
class WebhookRepository implements WebhookRepositoryInterface {
- /**
- * Allowed event keys and labels for UI.
- *
- * @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',
- ];
-
- /**
- * Get the list of allowed webhook events.
- *
- * @return array Associative array of event keys and labels.
- */
- public function get_allowed_events(): array {
- return apply_filters('graphql_webhooks_allowed_events', $this->default_events);
- }
-
- /**
- * Retrieve all published webhook entities.
- *
- * @return Webhook[] Array of Webhook entity objects.
- */
- public function get_all(): array {
- $webhooks = [];
-
- $posts = get_posts([
- 'post_type' => 'graphql_webhook',
- 'post_status' => 'publish',
- 'numberposts' => -1,
- ]);
-
- foreach ($posts as $post) {
- $webhooks[] = $this->mapPostToEntity($post);
- }
-
- return $webhooks;
- }
-
- /**
- * Retrieve a webhook entity by its post ID.
- *
- * @param int $id The post ID of the webhook.
- * @return Webhook|null The Webhook entity or null if not found or invalid post type.
- */
- public function get($id): ?Webhook {
- $post = get_post($id);
- if (!$post || $post->post_type !== 'graphql_webhook') {
- return null;
- }
- return $this->mapPostToEntity($post);
- }
-
- /**
- * Create a new webhook entity.
- *
- * @param string $name Name/title of the webhook.
- * @param string $event Event key the webhook listens to.
- * @param string $url Target URL for the webhook request.
- * @param string $method HTTP method (GET or POST).
- * @param array $headers Associative array of HTTP headers.
- *
- * @return int|WP_Error Post ID on success, or WP_Error on failure.
- */
- public function create($name, $event, $url, $method, $headers) {
- $validation = $this->validate_data($event, $url, $method);
- if (is_wp_error($validation)) {
- return $validation;
- }
-
- $postId = wp_insert_post([
- 'post_title' => $name,
- 'post_type' => 'graphql_webhook',
- 'post_status' => 'publish',
- ], true);
-
- if (is_wp_error($postId)) {
- return $postId;
- }
-
- update_post_meta($postId, '_webhook_event', sanitize_text_field($event));
- update_post_meta($postId, '_webhook_url', esc_url_raw($url));
- update_post_meta($postId, '_webhook_method', strtoupper($method));
- update_post_meta($postId, '_webhook_headers', wp_json_encode($headers));
-
- return $postId;
- }
-
- /**
- * Update an existing webhook entity.
- *
- * @param int $id Post ID of the webhook to update.
- * @param string $name New name/title of the webhook.
- * @param string $event New event key.
- * @param string $url New target URL.
- * @param string $method New HTTP method.
- * @param array $headers New HTTP headers.
- *
- * @return bool|WP_Error True on success, or WP_Error on failure.
- */
- public function update($id, $name, $event, $url, $method, $headers) {
- $post = get_post($id);
- if (!$post || $post->post_type !== 'graphql_webhook') {
- return new WP_Error('invalid_webhook', __('Webhook not found.', 'wp-graphql-headless-webhooks'));
- }
-
- $validation = $this->validate_data($event, $url, $method);
- if (is_wp_error($validation)) {
- return $validation;
- }
-
- $postData = [
- 'ID' => $id,
- 'post_title' => sanitize_text_field($name),
- ];
-
- $updated = wp_update_post($postData, true);
- if (is_wp_error($updated)) {
- return $updated;
- }
-
- update_post_meta($id, '_webhook_event', sanitize_text_field($event));
- update_post_meta($id, '_webhook_url', esc_url_raw($url));
- update_post_meta($id, '_webhook_method', strtoupper($method));
- update_post_meta($id, '_webhook_headers', wp_json_encode($headers));
-
- return true;
- }
-
- /**
- * Delete a webhook entity by post ID.
- *
- * @param int $id Post ID of the webhook to delete.
- * @return bool True if deleted, false otherwise.
- */
- public function delete($id): bool {
- $post = get_post($id);
- if (!$post || $post->post_type !== 'graphql_webhook') {
- return false;
- }
-
- $deleted = wp_delete_post($id, true);
-
- return (bool) $deleted;
- }
-
- /**
- * Validate webhook data before creation or update.
- *
- * @param string $event Event key to validate.
- * @param string $url URL to validate.
- * @param string $method HTTP method to validate.
- *
- * @return bool|WP_Error True if valid, WP_Error if invalid.
- */
- public function validate_data($event, $url, $method) {
- if (!isset($this->get_allowed_events()[$event])) {
- return new WP_Error('invalid_event', 'Invalid event type.');
- }
- if (!filter_var($url, FILTER_VALIDATE_URL)) {
- return new WP_Error('invalid_url', 'Invalid URL.');
- }
- if (!in_array(strtoupper($method), ['GET', 'POST'], true)) {
- return new WP_Error('invalid_method', 'Invalid HTTP method.');
- }
- return apply_filters('graphql_webhooks_validate_data', true, $event, $url, $method);
- }
-
- /**
- * Map a WP_Post object to a Webhook entity.
- *
- * @param WP_Post $post The webhook post object.
- *
- * @return Webhook The mapped Webhook entity.
- */
- private function mapPostToEntity(WP_Post $post) {
- $event = get_post_meta($post->ID, '_webhook_event', true);
- $url = get_post_meta($post->ID, '_webhook_url', true);
- $method = get_post_meta($post->ID, '_webhook_method', true) ?: 'POST';
- $headers = get_post_meta($post->ID, '_webhook_headers', true);
- $headers = $headers ? json_decode($headers, true) : [];
-
- return new Webhook(
- $post->ID,
- $post->post_title,
- $event,
- $url,
- $method,
- $headers
- );
- }
+ /**
+ * Allowed event keys and labels for UI.
+ *
+ * @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',
+ ];
+
+ /**
+ * Get the list of allowed webhook events.
+ *
+ * @return array Associative array of event keys and labels.
+ */
+ public function get_allowed_events(): array {
+ $default_events = $this->default_events;
+ $mapped_events = SmartCacheEventMapper::getMappedEvents();
+ $filtered_events = array_intersect_key( $default_events, $mapped_events );
+
+ return apply_filters( 'graphql_webhooks_allowed_events', $filtered_events );
+ }
+
+ /**
+ * Get the list of allowed HTTP methods.
+ *
+ * @return array Array of allowed HTTP methods.
+ */
+ public function get_allowed_methods(): array {
+ $methods = [ 'POST', 'GET' ];
+ return apply_filters( 'graphql_webhooks_allowed_methods', $methods );
+ }
+
+ /**
+ * Retrieve all published webhook entities.
+ *
+ * @return Webhook[] Array of Webhook entity objects.
+ */
+ public function get_all(): array {
+ $webhooks = [];
+
+ $posts = get_posts( [
+ 'post_type' => 'graphql_webhook',
+ 'post_status' => 'publish',
+ 'numberposts' => -1,
+ ] );
+
+ foreach ( $posts as $post ) {
+ $webhooks[] = $this->mapPostToEntity( $post );
+ }
+
+ return $webhooks;
+ }
+
+ /**
+ * Retrieve a webhook entity by its post ID.
+ *
+ * @param int $id The post ID of the webhook.
+ * @return Webhook|null The Webhook entity or null if not found or invalid post type.
+ */
+ public function get( $id ): ?Webhook {
+ $post = get_post( $id );
+ if ( ! $post || $post->post_type !== 'graphql_webhook' ) {
+ return null;
+ }
+ return $this->mapPostToEntity( $post );
+ }
+
+ /**
+ * Create a new webhook entity.
+ *
+ * @param array $data {
+ * Webhook data.
+ *
+ * @type string $name Name/title of the webhook.
+ * @type string $event Event key the webhook listens to.
+ * @type string $url Target URL for the webhook request.
+ * @type string $method HTTP method (GET, POST, etc).
+ * @type array $headers Associative array of HTTP headers.
+ * }
+ *
+ * @return int|WP_Error Post ID on success, or WP_Error on failure.
+ */
+ public function create( $data ) {
+ $name = $data['name'] ?? '';
+ $event = $data['event'] ?? '';
+ $url = $data['url'] ?? '';
+ $method = $data['method'] ?? 'POST';
+ $headers = $data['headers'] ?? [];
+
+ $validation = $this->validate_data( $event, $url, $method );
+ if ( is_wp_error( $validation ) ) {
+ return $validation;
+ }
+
+ $postId = wp_insert_post( [
+ 'post_title' => $name,
+ 'post_type' => 'graphql_webhook',
+ 'post_status' => 'publish',
+ ], true );
+
+ if ( is_wp_error( $postId ) ) {
+ return $postId;
+ }
+
+ update_post_meta( $postId, '_webhook_event', sanitize_text_field( $event ) );
+ update_post_meta( $postId, '_webhook_url', esc_url_raw( $url ) );
+ update_post_meta( $postId, '_webhook_method', strtoupper( $method ) );
+ update_post_meta( $postId, '_webhook_headers', wp_json_encode( $headers ) );
+
+ return $postId;
+ }
+
+ /**
+ * Update an existing webhook entity.
+ *
+ * @param int $id Post ID of the webhook to update.
+ * @param array $data {
+ * Webhook data.
+ *
+ * @type string $name New name/title of the webhook.
+ * @type string $event New event key.
+ * @type string $url New target URL.
+ * @type string $method New HTTP method.
+ * @type array $headers New HTTP headers.
+ * }
+ *
+ * @return bool|WP_Error True on success, or WP_Error on failure.
+ */
+ public function update( $id, $data ) {
+ $post = get_post( $id );
+ if ( ! $post || $post->post_type !== 'graphql_webhook' ) {
+ return new WP_Error( 'invalid_webhook', __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ) );
+ }
+
+ $name = $data['name'] ?? '';
+ $event = $data['event'] ?? '';
+ $url = $data['url'] ?? '';
+ $method = $data['method'] ?? 'POST';
+ $headers = $data['headers'] ?? [];
+
+ $validation = $this->validate_data( $event, $url, $method );
+ if ( is_wp_error( $validation ) ) {
+ return $validation;
+ }
+
+ $postData = [
+ 'ID' => $id,
+ 'post_title' => sanitize_text_field( $name ),
+ ];
+
+ $updated = wp_update_post( $postData, true );
+ if ( is_wp_error( $updated ) ) {
+ return $updated;
+ }
+
+ update_post_meta( $id, '_webhook_event', sanitize_text_field( $event ) );
+ update_post_meta( $id, '_webhook_url', esc_url_raw( $url ) );
+ update_post_meta( $id, '_webhook_method', strtoupper( $method ) );
+ update_post_meta( $id, '_webhook_headers', wp_json_encode( $headers ) );
+
+ return true;
+ }
+
+ /**
+ * Delete a webhook entity by post ID.
+ *
+ * @param int $id Post ID of the webhook to delete.
+ * @return bool True if deleted, false otherwise.
+ */
+ public function delete( $id ): bool {
+ $post = get_post( $id );
+ if ( ! $post || $post->post_type !== 'graphql_webhook' ) {
+ return false;
+ }
+
+ $deleted = wp_delete_post( $id, true );
+
+ return (bool) $deleted;
+ }
+
+ /**
+ * Validate webhook data before creation or update.
+ *
+ * @param string $event Event key to validate.
+ * @param string $url URL to validate.
+ * @param string $method HTTP method to validate.
+ *
+ * @return bool|WP_Error True if valid, WP_Error if invalid.
+ */
+ public function validate_data( $event, $url, $method ) {
+ if ( ! isset( $this->get_allowed_events()[ $event ] ) ) {
+ return new WP_Error( 'invalid_event', 'Invalid event type.' );
+ }
+ if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
+ return new WP_Error( 'invalid_url', 'Invalid URL.' );
+ }
+ if ( ! in_array( strtoupper( $method ), $this->get_allowed_methods(), true ) ) {
+ return new WP_Error( 'invalid_method', 'Invalid HTTP method.' );
+ }
+ return apply_filters( 'graphql_webhooks_validate_data', true, $event, $url, $method );
+ }
+
+ /**
+ * Map a WP_Post object to a Webhook entity.
+ *
+ * @param WP_Post $post The webhook post object.
+ *
+ * @return Webhook The mapped Webhook entity.
+ */
+ private function mapPostToEntity( WP_Post $post ) {
+ $event = get_post_meta( $post->ID, '_webhook_event', true );
+ $url = get_post_meta( $post->ID, '_webhook_url', true );
+ $method = get_post_meta( $post->ID, '_webhook_method', true ) ?: 'POST';
+ $headers = get_post_meta( $post->ID, '_webhook_headers', true );
+ $headers = $headers ? json_decode( $headers, true ) : [];
+
+ return new Webhook(
+ $post->ID,
+ $post->post_title,
+ $event,
+ $url,
+ $method,
+ $headers
+ );
+ }
}
diff --git a/plugins/wp-graphql-headless-webhooks/src/Rest/WebhookTestEndpoint.php b/plugins/wp-graphql-headless-webhooks/src/Rest/WebhookTestEndpoint.php
new file mode 100644
index 00000000..4fc4481a
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Rest/WebhookTestEndpoint.php
@@ -0,0 +1,166 @@
+repository = $repository;
+ }
+
+ /**
+ * Register REST routes.
+ */
+ public function register_routes(): void {
+ register_rest_route(
+ 'graphql-webhooks/v1',
+ '/test',
+ [
+ 'methods' => 'POST',
+ 'callback' => [ $this, 'test_webhook' ],
+ 'permission_callback' => [ $this, 'permission_callback' ],
+ 'args' => [
+ 'webhook_id' => [
+ 'required' => true,
+ 'type' => 'integer',
+ 'sanitize_callback' => 'absint',
+ ],
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Permission callback.
+ *
+ * @return bool Whether user has permission.
+ */
+ public function permission_callback(): bool {
+ return current_user_can( 'manage_options' );
+ }
+
+ /**
+ * Test a webhook.
+ *
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response|WP_Error Response.
+ */
+ public function test_webhook( WP_REST_Request $request ): WP_REST_Response|WP_Error {
+ $webhook_id = $request->get_param( 'webhook_id' );
+ $webhook = $this->repository->get( $webhook_id );
+
+ if ( ! $webhook ) {
+ return new WP_Error(
+ 'webhook_not_found',
+ __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ),
+ [ 'status' => 404 ]
+ );
+ }
+
+ // Log test initiation
+ $test_timestamp = current_time( 'mysql' );
+ error_log( "\n========== WEBHOOK TEST INITIATED ==========" );
+ error_log( "Timestamp: {$test_timestamp}" );
+ error_log( "Webhook ID: {$webhook_id}" );
+ error_log( "Webhook Name: {$webhook->name}" );
+ error_log( "Target URL: {$webhook->url}" );
+ error_log( "HTTP Method: {$webhook->method}" );
+ error_log( "Event: {$webhook->event}" );
+ error_log( "Headers: " . wp_json_encode( $webhook->headers ) );
+ error_log( "==========================================\n" );
+
+ // Create test payload
+ $test_payload = [
+ 'event' => 'test',
+ 'timestamp' => $test_timestamp,
+ 'webhook' => [
+ 'id' => $webhook->id,
+ 'name' => $webhook->name,
+ 'event' => $webhook->event,
+ ],
+ 'test_data' => [
+ 'message' => 'This is a test webhook payload',
+ 'triggered_by' => wp_get_current_user()->user_login,
+ 'site_url' => get_site_url(),
+ ],
+ ];
+
+ // Allow filtering of test payload
+ $test_payload = apply_filters( 'graphql_webhooks_test_payload', $test_payload, $webhook );
+
+ // Trigger test event with enhanced logging
+ $start_time = microtime( true );
+
+ try {
+ do_action( 'graphql_webhooks_test_event', $webhook, $test_payload );
+
+ $end_time = microtime( true );
+ $duration = round( ( $end_time - $start_time ) * 1000, 2 ); // Convert to milliseconds
+
+ error_log( "\n========== WEBHOOK TEST COMPLETED ==========" );
+ error_log( "✅ SUCCESS: Test webhook dispatched" );
+ error_log( "Duration: {$duration}ms" );
+ error_log( "Completed at: " . current_time( 'mysql' ) );
+ error_log( "==========================================\n" );
+
+ return new WP_REST_Response(
+ [
+ 'success' => true,
+ 'message' => __( 'Test webhook dispatched successfully.', 'wp-graphql-headless-webhooks' ),
+ 'details' => [
+ 'webhook_id' => $webhook_id,
+ 'webhook_name' => $webhook->name,
+ 'target_url' => $webhook->url,
+ 'method' => $webhook->method,
+ 'duration_ms' => $duration,
+ 'timestamp' => $test_timestamp,
+ 'payload_size' => strlen( wp_json_encode( $test_payload ) ) . ' bytes',
+ ],
+ ],
+ 200
+ );
+ } catch ( \Exception $e ) {
+ error_log( "\n========== WEBHOOK TEST ERROR ==========" );
+ error_log( "❌ ERROR: " . $e->getMessage() );
+ error_log( "Stack trace: " . $e->getTraceAsString() );
+ error_log( "========================================\n" );
+
+ return new WP_Error(
+ 'webhook_test_failed',
+ sprintf(
+ /* translators: %s: error message */
+ __( 'Failed to dispatch test webhook: %s', 'wp-graphql-headless-webhooks' ),
+ $e->getMessage()
+ ),
+ [ 'status' => 500 ]
+ );
+ }
+ }
+}
diff --git a/plugins/wp-graphql-headless-webhooks/src/Services/Interfaces/ServiceLocator.php b/plugins/wp-graphql-headless-webhooks/src/Services/Interfaces/ServiceLocator.php
new file mode 100644
index 00000000..a7cf2774
--- /dev/null
+++ b/plugins/wp-graphql-headless-webhooks/src/Services/Interfaces/ServiceLocator.php
@@ -0,0 +1,28 @@
+factories[$name] = $factory;
+ unset($this->instances[$name]);
+ }
+
+ public function has(string $name): bool {
+ return isset($this->factories[$name]);
+ }
+
+ public function get(string $name) {
+ if (!isset($this->factories[$name])) {
+ throw new UnexpectedValueException("Service not found: {$name}");
+ }
+
+ if (!isset($this->instances[$name])) {
+ $this->instances[$name] = call_user_func($this->factories[$name]);
+ }
+
+ return $this->instances[$name];
+ }
+}
\ No newline at end of file