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/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 2f1a2ee6..51d49a1a 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Plugin.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Plugin.php
@@ -89,6 +89,26 @@ private function setup(): void {
// 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();
+ }
+ } );
}
/**
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 12eaf433..2f48a191 100644
--- a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php
+++ b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php
@@ -58,6 +58,16 @@ public function get_allowed_events(): array {
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.
*
@@ -96,15 +106,25 @@ public function get( $id ): ?Webhook {
/**
* 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.
+ * @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( $name, $event, $url, $method, $headers ) {
+ 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;
@@ -131,21 +151,31 @@ public function create( $name, $event, $url, $method, $headers ) {
/**
* 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.
+ * @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, $name, $event, $url, $method, $headers ) {
+ 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;
@@ -202,7 +232,7 @@ public function validate_data( $event, $url, $method ) {
if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
return new WP_Error( 'invalid_url', 'Invalid URL.' );
}
- if ( ! in_array( strtoupper( $method ), [ 'GET', 'POST' ], true ) ) {
+ 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 );
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 ]
+ );
+ }
+ }
+}