diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e02823ea..de8120ef 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -22,8 +22,8 @@ jobs: runs-on: ubuntu-latest name: Detect plugins has php code quality configuration outputs: - plugins: ${{ steps.detect.outputs.plugins }} - has-plugins: ${{ steps.detect.outputs.has-plugins }} + plugins: ${{ steps.detect-plugin-slug.outputs.plugins }} + has-plugins: ${{ steps.detect-plugin-slug.outputs.has-plugins }} php-version: ${{ steps.detect-php-version.outputs.php-version }} steps: - name: Checkout diff --git a/plugins/wpgraphql-logging/bin/install-test-env.sh b/plugins/wpgraphql-logging/bin/install-test-env.sh index 0c8d1e01..bb2652e9 100644 --- a/plugins/wpgraphql-logging/bin/install-test-env.sh +++ b/plugins/wpgraphql-logging/bin/install-test-env.sh @@ -44,10 +44,10 @@ install_db() { # create database echo -e "$(status_message "Creating the database (if it does not exist)...")" - RESULT=$(mysql -u $WORDPRESS_DB_USER --password="$WORDPRESS_DB_PASSWORD" --skip-column-names -e "SHOW DATABASES LIKE '$WORDPRESS_DB_NAME'"$EXTRA) - if [ "$RESULT" != $WORDPRESS_DB_NAME ]; then - mysqladmin create $WORDPRESS_DB_NAME --user="$WORDPRESS_DB_USER" --password="$WORDPRESS_DB_PASSWORD"$EXTRA - fi + RESULT=$(mysql --no-defaults --ssl=false -u $WORDPRESS_DB_USER --password="$WORDPRESS_DB_PASSWORD" --skip-column-names -e "SHOW DATABASES LIKE '$WORDPRESS_DB_NAME'"$EXTRA) + if [ "$RESULT" != $WORDPRESS_DB_NAME ]; then + mysqladmin --no-defaults --ssl=false create $WORDPRESS_DB_NAME --user="$WORDPRESS_DB_USER" --password="$WORDPRESS_DB_PASSWORD"$EXTRA + fi } download() { @@ -117,7 +117,6 @@ configure_wordpress() { SITE_TITLE=${WORDPRESS_SITE_TITLE:-"WPGraphQL Logging Tests"} wp core install --title="$SITE_TITLE" --admin_user="$WORDPRESS_ADMIN_USER" --admin_password="$WORDPRESS_ADMIN_PASSWORD" --admin_email="$WORDPRESS_ADMIN_EMAIL" --skip-email --url="$WORDPRESS_URL" --allow-root - echo -e "$(status_message "Running WordPress version: $(wp core version --allow-root) at $(wp option get home --allow-root)")" } @@ -196,15 +195,6 @@ post_setup() { wp config set WP_AUTO_UPDATE_CORE false --raw --type=constant --quiet --allow-root wp config set AUTOMATIC_UPDATER_DISABLED true --raw --type=constant --quiet --allow-root - # Export the db for codeception to use - SQLDUMP="$WORDPRESS_ROOT_DIR/wp-content/plugins/$PLUGIN_SLUG/tests/_data/dump.sql" - mkdir -p "$(dirname "$SQLDUMP")" - if [ ! -f "$SQLDUMP" ]; then - echo -e "$(status_message "Exporting test database dump...")" - - wp db export "$SQLDUMP" --allow-root - fi - echo -e "$(status_message "Installed plugins")" wp plugin list --allow-root } diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php index db58ea18..54e433f8 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php @@ -150,21 +150,21 @@ public function get_fields(): array { $fields[ self::EVENT_LOG_SELECTION ] = new Select_Field( - self::EVENT_LOG_SELECTION, - $this->get_name(), - __( 'Log Points', 'wpgraphql-logging' ), - [ - Events::PRE_REQUEST => __( 'Pre Request', 'wpgraphql-logging' ), - Events::BEFORE_GRAPHQL_EXECUTION => __( 'Before Query Execution', 'wpgraphql-logging' ), - Events::BEFORE_RESPONSE_RETURNED => __( 'Before Response Returned', 'wpgraphql-logging' ), - Events::REQUEST_DATA => __( 'Request Data', 'wpgraphql-logging' ), - Events::REQUEST_RESULTS => __( 'Request Results', 'wpgraphql-logging' ), - Events::RESPONSE_HEADERS_TO_SEND => __( 'Response Headers', 'wpgraphql-logging' ), - ], - '', - __( 'Select which points in the request lifecycle to log. By default, all points are logged.', 'wpgraphql-logging' ), - true - ); + self::EVENT_LOG_SELECTION, + $this->get_name(), + __( 'Log Points', 'wpgraphql-logging' ), + [ + Events::PRE_REQUEST => __( 'Pre Request', 'wpgraphql-logging' ), + Events::BEFORE_GRAPHQL_EXECUTION => __( 'Before Query Execution', 'wpgraphql-logging' ), + Events::BEFORE_RESPONSE_RETURNED => __( 'Before Response Returned', 'wpgraphql-logging' ), + Events::REQUEST_DATA => __( 'Request Data', 'wpgraphql-logging' ), + Events::REQUEST_RESULTS => __( 'Request Results', 'wpgraphql-logging' ), + Events::RESPONSE_HEADERS_TO_SEND => __( 'Response Headers', 'wpgraphql-logging' ), + ], + '', + __( 'Select which points in the request lifecycle to log. By default, all points are logged.', 'wpgraphql-logging' ), + true + ); return apply_filters( 'wpgraphql_logging_basic_configuration_fields', $fields ); } diff --git a/plugins/wpgraphql-logging/src/Events/Events.php b/plugins/wpgraphql-logging/src/Events/Events.php index 602b8745..fcc15ad5 100644 --- a/plugins/wpgraphql-logging/src/Events/Events.php +++ b/plugins/wpgraphql-logging/src/Events/Events.php @@ -8,75 +8,75 @@ * List of available events that users can subscribe to with the EventManager. */ final class Events { - /** - * WPGraphQL action: do_graphql_request. - * - * Before the request is processed. - * - * @var string - */ - public const PRE_REQUEST = 'do_graphql_request'; + /** + * WPGraphQL action: do_graphql_request. + * + * Before the request is processed. + * + * @var string + */ + public const PRE_REQUEST = 'do_graphql_request'; - /** - * WPGraphQL action: graphql_before_execute. - * - * @var string - */ - public const BEFORE_GRAPHQL_EXECUTION = 'graphql_before_execute'; + /** + * WPGraphQL action: graphql_before_execute. + * + * @var string + */ + public const BEFORE_GRAPHQL_EXECUTION = 'graphql_before_execute'; - /** - * WPGraphQL action: graphql_return_response - * - * Before the response is returned to the client. - * - * @var string - */ - public const BEFORE_RESPONSE_RETURNED = 'graphql_return_response'; + /** + * WPGraphQL action: graphql_return_response + * + * Before the response is returned to the client. + * + * @var string + */ + public const BEFORE_RESPONSE_RETURNED = 'graphql_return_response'; - /** - * WPGraphQL filter: graphql_request_data. - * - * Allows the request data to be filtered. Ideal for capturing the - * full payload before processing. - * - * @var string - */ - public const REQUEST_DATA = 'graphql_request_data'; + /** + * WPGraphQL filter: graphql_request_data. + * + * Allows the request data to be filtered. Ideal for capturing the + * full payload before processing. + * + * @var string + */ + public const REQUEST_DATA = 'graphql_request_data'; - /** - * WPGraphQL filter: graphql_response_headers_to_send. - * - * Filters the headers to send in the GraphQL response. - * - * @var string - */ - public const RESPONSE_HEADERS_TO_SEND = 'graphql_response_headers_to_send'; + /** + * WPGraphQL filter: graphql_response_headers_to_send. + * + * Filters the headers to send in the GraphQL response. + * + * @var string + */ + public const RESPONSE_HEADERS_TO_SEND = 'graphql_response_headers_to_send'; - /** - * WPGraphQL filter: graphql_request_results. - * - * Filters the final results of the GraphQL execution. - * - * @var string - */ - public const REQUEST_RESULTS = 'graphql_request_results'; + /** + * WPGraphQL filter: graphql_request_results. + * + * Filters the final results of the GraphQL execution. + * + * @var string + */ + public const REQUEST_RESULTS = 'graphql_request_results'; - /** - * WPGraphQL filter: graphql_debug_enabled. - * - * Determines if GraphQL Debug is enabled. Useful for toggling logging. - * - * @var string - */ - public const DEBUG_ENABLED = 'graphql_debug_enabled'; + /** + * WPGraphQL filter: graphql_debug_enabled. + * + * Determines if GraphQL Debug is enabled. Useful for toggling logging. + * + * @var string + */ + public const DEBUG_ENABLED = 'graphql_debug_enabled'; - /** - * WPGraphQL filter: graphql_app_context_config. - * - * Filters the config for the AppContext. Useful for storing temporary - * data for the duration of a request. - * - * @var string - */ - public const APP_CONTEXT_CONFIG = 'graphql_app_context_config'; -} \ No newline at end of file + /** + * WPGraphQL filter: graphql_app_context_config. + * + * Filters the config for the AppContext. Useful for storing temporary + * data for the duration of a request. + * + * @var string + */ + public const APP_CONTEXT_CONFIG = 'graphql_app_context_config'; +} diff --git a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php index 1ebea818..bd9c66fc 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php +++ b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php @@ -23,184 +23,202 @@ * @since 0.0.1 */ class QueryActionLogger { - use LoggingHelper; + use LoggingHelper; - /** - * The logger service instance. - * - * @var \WPGraphQL\Logging\Logger\LoggerService - */ - protected LoggerService $logger; + /** + * The logger service instance. + * + * @var \WPGraphQL\Logging\Logger\LoggerService + */ + protected LoggerService $logger; - /** - * The basic configuration settings. - * - * @var array> - */ - protected array $config; + /** + * The basic configuration settings. + * + * @var array> + */ + protected array $config; - /** - * QueryActionLogger constructor. - * - * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. - * @param array $config The logging configuration. - */ - public function __construct( LoggerService $logger, array $config ) { - $this->logger = $logger; - $this->config = $config; - } + /** + * QueryActionLogger constructor. + * + * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. + * @param array $config The logging configuration. + */ + public function __construct( LoggerService $logger, array $config ) { + $this->logger = $logger; + $this->config = $config; + } - /** - * Initial Incoming Request. - * - * This method hooks into the `do_graphql_request` action. - * - * @param string $query - * @param string|null $operation_name - * @param array|null $variables - */ - public function log_pre_request( string $query, ?string $operation_name, ?array $variables ): void { - try { - if ( ! $this->is_logging_enabled( $this->config ) ) { - return; - } - $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; - if ( ! in_array( Events::PRE_REQUEST, $selected_events, true ) ) { - return; - } - $context = [ - 'query' => $query, - 'variables' => $variables, - 'operation_name' => $operation_name, - ]; - $payload = EventManager::transform( Events::PRE_REQUEST, [ 'context' => $context, 'level' => Level::Info ] ); - $this->logger->log( $payload['level'], 'WPGraphQL Pre Request', $payload['context'] ); - EventManager::publish( Events::PRE_REQUEST, [ 'context' => $payload['context'] ] ); - } catch ( \Throwable $e ) { - $this->process_application_error( Events::PRE_REQUEST, $e ); - } - } + /** + * Initial Incoming Request. + * + * This method hooks into the `do_graphql_request` action. + * + * @param string $query + * @param string|null $operation_name + * @param array|null $variables + */ + public function log_pre_request( string $query, ?string $operation_name, ?array $variables ): void { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return; + } + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { + return; + } + if ( ! in_array( Events::PRE_REQUEST, $selected_events, true ) ) { + return; + } + $context = [ + 'query' => $query, + 'variables' => $variables, + 'operation_name' => $operation_name, + ]; + $payload = EventManager::transform( Events::PRE_REQUEST, [ + 'context' => $context, + 'level' => Level::Info, + ] ); + $this->logger->log( $payload['level'], 'WPGraphQL Pre Request', $payload['context'] ); + EventManager::publish( Events::PRE_REQUEST, [ 'context' => $payload['context'] ] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::PRE_REQUEST, $e ); + } + } - /** - * Before Request Execution. - * - * This method hooks into the `graphql_before_execute` action. - * - * @param Request $request - */ - public function log_graphql_before_execute( Request $request ): void { - try { - if ( ! $this->is_logging_enabled( $this->config ) ) { - return; - } - $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; - if ( ! in_array( Events::BEFORE_GRAPHQL_EXECUTION, $selected_events, true ) ) { - return; - } - /** @var \GraphQL\Server\OperationParams $params */ - $params = $request->params; - $context = [ - 'query' => $params->query, - 'operation_name' => $params->operation, - 'variables' => $params->variables, - 'params' => $params, - ]; - $payload = EventManager::transform( Events::BEFORE_GRAPHQL_EXECUTION, [ 'context' => $context, 'level' => Level::Info ] ); - $this->logger->log( $payload['level'], 'WPGraphQL Before Query Execution', $payload['context'] ); - EventManager::publish( Events::BEFORE_GRAPHQL_EXECUTION, [ 'context' => $payload['context'] ] ); - } catch ( \Throwable $e ) { - $this->process_application_error( Events::BEFORE_GRAPHQL_EXECUTION, $e ); - } - } + /** + * Before Request Execution. + * + * This method hooks into the `graphql_before_execute` action. + * + * @param \WPGraphQL\Request $request + */ + public function log_graphql_before_execute( Request $request ): void { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return; + } + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { + return; + } + if ( ! in_array( Events::BEFORE_GRAPHQL_EXECUTION, $selected_events, true ) ) { + return; + } + /** @var \GraphQL\Server\OperationParams $params */ + $params = $request->params; + $context = [ + 'query' => $params->query, + 'operation_name' => $params->operation, + 'variables' => $params->variables, + 'params' => $params, + ]; + $payload = EventManager::transform( Events::BEFORE_GRAPHQL_EXECUTION, [ + 'context' => $context, + 'level' => Level::Info, + ] ); + $this->logger->log( $payload['level'], 'WPGraphQL Before Query Execution', $payload['context'] ); + EventManager::publish( Events::BEFORE_GRAPHQL_EXECUTION, [ 'context' => $payload['context'] ] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::BEFORE_GRAPHQL_EXECUTION, $e ); + } + } - /** - * Before the GraphQL response is returned to the client. - * - * This method hooks into the `graphql_return_response` action. - * - * @param array|\GraphQL\Executor\ExecutionResult $filtered_response - * @param array|\GraphQL\Executor\ExecutionResult $response - * @param WPSchema $schema - * @param string|null $operation - * @param string $query - * @param array|null $variables - * @param Request $request - * @param string|null $query_id - */ - public function log_before_response_returned( - array|ExecutionResult $filtered_response, - array|ExecutionResult $response, - WPSchema $schema, - ?string $operation, - string $query, - ?array $variables, - Request $request, - ?string $query_id - ): void { - try { - if ( ! $this->is_logging_enabled( $this->config ) ) { - return; - } - $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; - if ( ! in_array( Events::BEFORE_RESPONSE_RETURNED, $selected_events, true ) ) { - return; - } - $context = [ - 'response' => $response, - 'schema' => $schema, - 'operation_name' => $operation, - 'query' => $query, - 'variables' => $variables, - 'request' => $request, - 'query_id' => $query_id, - ]; - $level = Level::Info; - $message = 'WPGraphQL Response'; - $errors = $this->get_response_errors( $response ); - if ( null !== $errors && count( $errors ) > 0 ) { - $context['errors'] = $errors; - $level = Level::Error; - $message = 'WPGraphQL Response with Errors'; - } - $payload = EventManager::transform( Events::BEFORE_RESPONSE_RETURNED, [ 'context' => $context, 'level' => $level ] ); - $this->logger->log( $payload['level'], $message, $payload['context'] ); - EventManager::publish( Events::BEFORE_RESPONSE_RETURNED, [ 'context' => $payload['context'] ] ); - } catch ( \Throwable $e ) { - $this->process_application_error( Events::BEFORE_RESPONSE_RETURNED, $e ); - } - } + /** + * Before the GraphQL response is returned to the client. + * + * This method hooks into the `graphql_return_response` action. + * + * @param array|\GraphQL\Executor\ExecutionResult $filtered_response + * @param array|\GraphQL\Executor\ExecutionResult $response + * @param \WPGraphQL\WPSchema $schema + * @param string|null $operation + * @param string $query + * @param array|null $variables + * @param \WPGraphQL\Request $request + * @param string|null $query_id + */ + public function log_before_response_returned( + array|ExecutionResult $filtered_response, + array|ExecutionResult $response, + WPSchema $schema, + ?string $operation, + string $query, + ?array $variables, + Request $request, + ?string $query_id + ): void { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return; + } + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { + return; + } + if ( ! in_array( Events::BEFORE_RESPONSE_RETURNED, $selected_events, true ) ) { + return; + } + $context = [ + 'response' => $response, + 'schema' => $schema, + 'operation_name' => $operation, + 'query' => $query, + 'variables' => $variables, + 'request' => $request, + 'query_id' => $query_id, + ]; + $level = Level::Info; + $message = 'WPGraphQL Response'; + $errors = $this->get_response_errors( $response ); + if ( null !== $errors && count( $errors ) > 0 ) { + $context['errors'] = $errors; + $level = Level::Error; + $message = 'WPGraphQL Response with Errors'; + } + $payload = EventManager::transform( Events::BEFORE_RESPONSE_RETURNED, [ + 'context' => $context, + 'level' => $level, + ] ); + $this->logger->log( $payload['level'], $message, $payload['context'] ); + EventManager::publish( Events::BEFORE_RESPONSE_RETURNED, [ 'context' => $payload['context'] ] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::BEFORE_RESPONSE_RETURNED, $e ); + } + } - /** - * Get the context for the response. - * - * @param array|\GraphQL\Executor\ExecutionResult $response The response. - * - * @return array|null - */ - protected function get_response_errors( array|ExecutionResult $response ): ?array { - if ( $response instanceof ExecutionResult && [] !== $response->errors ) { - return $response->errors; - } + /** + * Get the context for the response. + * + * @param array|\GraphQL\Executor\ExecutionResult $response The response. + * + * @return array|null + */ + protected function get_response_errors( array|ExecutionResult $response ): ?array { + if ( $response instanceof ExecutionResult && [] !== $response->errors ) { + return $response->errors; + } - if ( ! is_array( $response ) ) { - return null; - } + if ( ! is_array( $response ) ) { + return null; + } - $errors = $response['errors'] ?? null; - if ( null === $errors || [] === $errors ) { - return null; - } + $errors = $response['errors'] ?? null; + if ( null === $errors || [] === $errors ) { + return null; + } - return $errors; - } + return $errors; + } - /** - * Handles and logs application errors. - * - * @param string $event - * @param \Throwable $exception - */ - protected function process_application_error( string $event, \Throwable $exception ): void { + /** + * Handles and logs application errors. + * + * @param string $event + * @param \Throwable $exception + */ + protected function process_application_error( string $event, \Throwable $exception ): void { error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore - } -} \ No newline at end of file + } +} diff --git a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php index b4b6c62b..2e309431 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php +++ b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php @@ -4,7 +4,6 @@ namespace WPGraphQL\Logging\Events; -use WPGraphQL\Logging\Admin\Settings\Fields\Tab\Basic_Configuration_Tab; use WPGraphQL\Logging\Logger\LoggerService; /** @@ -18,89 +17,105 @@ * @since 0.0.1 */ class QueryEventLifecycle { - /** - * The single instance of the class. - * - * @var \WPGraphQL\Logging\Events\QueryEventLifecycle|null - */ - private static ?QueryEventLifecycle $instance = null; - - /** - * The logger service instance. - * - * @var \WPGraphQL\Logging\Logger\LoggerService - */ - protected LoggerService $logger; - - /** - * The basic configuration settings. - * - * @var array> - */ - protected array $config; - - /** - * The logger for handling WordPress action hooks. - * - * @var \WPGraphQL\Logging\Events\QueryActionLogger - */ - protected QueryActionLogger $action_logger; - - /** - * The logger for handling WordPress filter hooks. - * - * @var \WPGraphQL\Logging\Events\QueryFilterLogger - */ - protected QueryFilterLogger $filter_logger; - - /** - * QueryEventLifecycle constructor. - * - * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. - */ - protected function __construct( LoggerService $logger ) { - $this->logger = $logger; - $full_config = get_option( WPGRAPHQL_LOGGING_SETTINGS_KEY, [] ); - $this->config = $full_config['basic_configuration'] ?? []; - - // Initialize the specialized logger components. - $this->action_logger = new QueryActionLogger( $this->logger, $this->config ); - $this->filter_logger = new QueryFilterLogger( $this->logger, $this->config ); - } - - /** - * Get or create the single instance of the class. - * - * @return QueryEventLifecycle - */ - public static function init(): QueryEventLifecycle { - if ( null === self::$instance ) { - $logger = LoggerService::get_instance(); - self::$instance = new self( $logger ); - self::$instance->setup(); - } - - return self::$instance; - } - - /** - * Register actions and filters to log the query event lifecycle. - * - * @psalm-suppress HookNotFound - */ - protected function setup(): void { + /** + * The logger service instance. + * + * @var \WPGraphQL\Logging\Logger\LoggerService + */ + protected LoggerService $logger; + + /** + * The basic configuration settings. + * + * @var array> + */ + protected array $config; + + /** + * The logger for handling WordPress action hooks. + * + * @var \WPGraphQL\Logging\Events\QueryActionLogger + */ + protected QueryActionLogger $action_logger; + + /** + * The logger for handling WordPress filter hooks. + * + * @var \WPGraphQL\Logging\Events\QueryFilterLogger + */ + protected QueryFilterLogger $filter_logger; + + /** + * The single instance of the class. + * + * @var \WPGraphQL\Logging\Events\QueryEventLifecycle|null + */ + private static ?QueryEventLifecycle $instance = null; + + /** + * QueryEventLifecycle constructor. + * + * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. + */ + protected function __construct( LoggerService $logger ) { + $this->logger = $logger; + $full_config = get_option( WPGRAPHQL_LOGGING_SETTINGS_KEY, [] ); + $this->config = $full_config['basic_configuration'] ?? []; + + // Initialize the specialized logger components. + $this->action_logger = new QueryActionLogger( $this->logger, $this->config ); + $this->filter_logger = new QueryFilterLogger( $this->logger, $this->config ); + } + + /** + * Get or create the single instance of the class. + */ + public static function init(): QueryEventLifecycle { + if ( null === self::$instance ) { + $logger = LoggerService::get_instance(); + self::$instance = new self( $logger ); + self::$instance->setup(); + } + + return self::$instance; + } + + /** + * Register actions and filters to log the query event lifecycle. + * + * @psalm-suppress HookNotFound + */ + protected function setup(): void { // Map of action events to their corresponding logger methods and accepted args. $action_events = [ - Events::PRE_REQUEST => [ 'method' => 'log_pre_request', 'accepted_args' => 3 ], - Events::BEFORE_GRAPHQL_EXECUTION => [ 'method' => 'log_graphql_before_execute', 'accepted_args' => 1 ], - Events::BEFORE_RESPONSE_RETURNED => [ 'method' => 'log_before_response_returned', 'accepted_args' => 8 ], + Events::PRE_REQUEST => [ + 'method' => 'log_pre_request', + 'accepted_args' => 3, + ], + Events::BEFORE_GRAPHQL_EXECUTION => [ + 'method' => 'log_graphql_before_execute', + 'accepted_args' => 1, + ], + Events::BEFORE_RESPONSE_RETURNED => [ + 'method' => 'log_before_response_returned', + 'accepted_args' => 8, + ], ]; // Map of filter events to their corresponding logger methods and accepted args. $filter_events = [ - Events::REQUEST_DATA => [ 'method' => 'log_graphql_request_data', 'accepted_args' => 1 ], - Events::REQUEST_RESULTS => [ 'method' => 'log_graphql_request_results', 'accepted_args' => 7 ], - Events::RESPONSE_HEADERS_TO_SEND => [ 'method' => 'add_logging_headers', 'accepted_args' => 1 ], + Events::REQUEST_DATA => [ + 'method' => 'log_graphql_request_data', + 'accepted_args' => 1, + ], + Events::REQUEST_RESULTS => [ + 'method' => 'log_graphql_request_results', + 'accepted_args' => 7, + ], + Events::RESPONSE_HEADERS_TO_SEND => [ + 'method' => 'add_logging_headers', + 'accepted_args' => 1, + ], ]; // Add action hooks. @@ -110,7 +125,8 @@ protected function setup(): void { // Add filter hooks. foreach ( $filter_events as $event_name => $data ) { + /** @psalm-suppress PossiblyInvalidArgument */ add_filter( $event_name, [ $this->filter_logger, $data['method'] ], 10, $data['accepted_args'] ); } } -} \ No newline at end of file +} diff --git a/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php b/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php index 781b18ac..72c70cb8 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php +++ b/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php @@ -22,152 +22,170 @@ * @since 0.0.1 */ class QueryFilterLogger { - use LoggingHelper; - /** - * The logger service instance. - * - * @var \WPGraphQL\Logging\Logger\LoggerService - */ - protected LoggerService $logger; - - /** - * The basic configuration settings. - * - * @var array> - */ - protected array $config; - - /** - * QueryFilterLogger constructor. - * - * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. - * @param array $config The logging configuration. - */ - public function __construct( LoggerService $logger, array $config ) { - $this->logger = $logger; - $this->config = $config; - } - - /** - * Logs and returns the GraphQL request data. - * - * This method hooks into the `graphql_request_data` filter. - * - * @param array $query_data The raw GraphQL request data. - * @return array The filtered query data. - */ - public function log_graphql_request_data( array $query_data ): array { - try { - if ( ! $this->is_logging_enabled( $this->config ) ) { - return $query_data; - } - - $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; - if ( ! in_array( Events::REQUEST_DATA, $selected_events, true ) ) { - return $query_data; - } - - $context = [ - 'query' => $query_data['query'] ?? null, - 'variables' => $query_data['variables'] ?? null, - 'operation_name' => $query_data['operationName'] ?? null, - ]; - - $payload = EventManager::transform( Events::REQUEST_DATA, ['context' => $context] ); - $this->logger->log( Level::Info, 'WPGraphQL Request Data', $payload['context'] ); - EventManager::publish( Events::REQUEST_DATA, ['context' => $payload['context']] ); - } catch ( \Throwable $e ) { - $this->process_application_error( Events::REQUEST_DATA, $e ); - } - - return $query_data; - } - - /** - * Logs and returns the final GraphQL request results. - * - * This method hooks into the `graphql_request_results` filter. - * - * @param array|\GraphQL\Executor\ExecutionResult $response The final GraphQL response. - * @param \WPGraphQL\WPSchema $schema The GraphQL schema. - * @param string|null $operation The name of the operation being executed. - * @param string|null $query The raw GraphQL query string. - * @param array|null $variables The query variables. - * @param \WPGraphQL\Request $request The WPGraphQL request instance. - * @param string|null $query_id The unique ID of the query. - * @return array|\GraphQL\Executor\ExecutionResult The filtered response. - */ - public function log_graphql_request_results( - array|ExecutionResult $response, - \WPGraphQL\WPSchema $schema, - ?string $operation, - ?string $query, - ?array $variables, - Request $request, - ?string $query_id - ): array|ExecutionResult { - try { - if ( ! $this->is_logging_enabled( $this->config ) ) { - return $response; - } - - $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; - if ( ! in_array( Events::REQUEST_RESULTS, $selected_events, true ) ) { - return $response; - } - - $context = [ - 'response' => $response, - 'operation_name' => $request->params->operation, - 'query' => $request->params->query, - 'variables' => $request->params->variables, - 'request' => $request, - ]; - - $level = Level::Info; - $message = 'WPGraphQL Response'; - if ( isset( $response['errors'] ) && ! empty( $response['errors'] ) ) { - $context['errors'] = $response['errors']; - $level = Level::Error; - $message = 'WPGraphQL Response with Errors'; - } - - $payload = EventManager::transform( Events::REQUEST_RESULTS, ['context' => $context, 'level' => $level] ); - $this->logger->log( $payload['level'], $message, $payload['context'] ); - EventManager::publish( Events::REQUEST_RESULTS, ['context' => $payload['context']] ); - } catch ( \Throwable $e ) { - $this->process_application_error( Events::REQUEST_RESULTS, $e ); - } - - return $response; - } - - /** - * Adds a unique logging ID to the GraphQL response headers. - * - * This method hooks into the `graphql_response_headers_to_send` filter. - * - * @param array $headers The array of response headers. - * @return array The filtered array of headers. - */ - public function add_logging_headers( array $headers ): array { - if ( ! $this->is_logging_enabled( $this->config ) ) { - return $headers; - } - - $request_id = uniqid( 'wpgql_log_' ); - $headers['X-WPGraphQL-Logging-ID'] = $request_id; - - return $headers; - } - - /** - * Handles and logs application errors. - * - * @param string $event The name of the event where the error occurred. - * @param \Throwable $exception The exception that was caught. - */ - protected function process_application_error( string $event, \Throwable $exception ): void { + use LoggingHelper; + + /** + * The logger service instance. + * + * @var \WPGraphQL\Logging\Logger\LoggerService + */ + protected LoggerService $logger; + + /** + * The basic configuration settings. + * + * @var array> + */ + protected array $config; + + /** + * QueryFilterLogger constructor. + * + * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. + * @param array $config The logging configuration. + */ + public function __construct( LoggerService $logger, array $config ) { + $this->logger = $logger; + $this->config = $config; + } + + /** + * Logs and returns the GraphQL request data. + * + * This method hooks into the `graphql_request_data` filter. + * + * @param array $query_data The raw GraphQL request data. + * + * phpcs:disable Generic.Metrics.CyclomaticComplexity, SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh + * + * @return array The filtered query data. + */ + public function log_graphql_request_data( array $query_data ): array { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return $query_data; + } + + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { + return $query_data; + } + if ( ! in_array( Events::REQUEST_DATA, $selected_events, true ) ) { + return $query_data; + } + + $context = [ + 'query' => $query_data['query'] ?? null, + 'variables' => $query_data['variables'] ?? null, + 'operation_name' => $query_data['operationName'] ?? null, + ]; + + $payload = EventManager::transform( Events::REQUEST_DATA, [ 'context' => $context ] ); + $this->logger->log( Level::Info, 'WPGraphQL Request Data', $payload['context'] ); + EventManager::publish( Events::REQUEST_DATA, [ 'context' => $payload['context'] ] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::REQUEST_DATA, $e ); + } + + return $query_data; + } + + /** + * Logs and returns the final GraphQL request results. + * + * This method hooks into the `graphql_request_results` filter. + * + * @param array|\GraphQL\Executor\ExecutionResult $response The final GraphQL response. + * @param \WPGraphQL\WPSchema $schema The GraphQL schema. + * @param string|null $operation The name of the operation being executed. + * @param string|null $query The raw GraphQL query string. + * @param array|null $variables The query variables. + * @param \WPGraphQL\Request $request The WPGraphQL request instance. + * @param string|null $query_id The unique ID of the query. + * + * @return array|\GraphQL\Executor\ExecutionResult The filtered response. + */ + public function log_graphql_request_results( + array|ExecutionResult $response, + \WPGraphQL\WPSchema $schema, + ?string $operation, + ?string $query, + ?array $variables, + Request $request, + ?string $query_id + ): array|ExecutionResult { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return $response; + } + + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { + return $response; + } + if ( ! in_array( Events::REQUEST_RESULTS, $selected_events, true ) ) { + return $response; + } + + /** @var \GraphQL\Server\OperationParams $params */ + $params = $request->params; + $context = [ + 'response' => $response, + 'operation_name' => $params->operation, + 'query' => $params->query, + 'variables' => $params->variables, + 'request' => $request, + 'query_id' => $query_id, + ]; + + $level = Level::Info; + $message = 'WPGraphQL Response'; + if ( is_array( $response ) && isset( $response['errors'] ) && ! empty( $response['errors'] ) ) { + $context['errors'] = $response['errors']; + $level = Level::Error; + $message = 'WPGraphQL Response with Errors'; + } + + $payload = EventManager::transform( Events::REQUEST_RESULTS, [ + 'context' => $context, + 'level' => $level, + ] ); + $this->logger->log( $payload['level'], $message, $payload['context'] ); + EventManager::publish( Events::REQUEST_RESULTS, [ 'context' => $payload['context'] ] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::REQUEST_RESULTS, $e ); + } + + return $response; + } + + /** + * Adds a unique logging ID to the GraphQL response headers. + * + * This method hooks into the `graphql_response_headers_to_send` filter. + * + * @param array $headers The array of response headers. + * + * @return array The filtered array of headers. + */ + public function add_logging_headers( array $headers ): array { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return $headers; + } + + $request_id = uniqid( 'wpgql_log_' ); + $headers['X-WPGraphQL-Logging-ID'] = $request_id; + + return $headers; + } + + /** + * Handles and logs application errors. + * + * @param string $event The name of the event where the error occurred. + * @param \Throwable $exception The exception that was caught. + */ + protected function process_application_error( string $event, \Throwable $exception ): void { error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore - } -} \ No newline at end of file + } +} diff --git a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php index b61d2e03..b742f410 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php @@ -10,50 +10,52 @@ * Trait for shared logging helper methods. */ trait LoggingHelper { - /** - * Checks if logging is enabled based on user settings. - * - * @param array $config The logging configuration. - * @return bool - */ - protected function is_logging_enabled( array $config ): bool { - $is_enabled = true; - - // Check the main "Enabled" checkbox. - if ( ! ( $config[ Basic_Configuration_Tab::ENABLED ] ?? false ) ) { - $is_enabled = false; - } - - // Check if the current user is an admin if that option is enabled. - if ( $is_enabled && ( $config[ Basic_Configuration_Tab::ADMIN_USER_LOGGING ] ?? false ) ) { - if ( ! current_user_can( 'manage_options' ) ) { - $is_enabled = false; - } - } - - // Check for IP restrictions. - $ip_restrictions = $config[ Basic_Configuration_Tab::IP_RESTRICTIONS ] ?? ''; - if ( $is_enabled && ! empty( $ip_restrictions ) ) { - $allowed_ips = array_map( 'trim', explode( ',', $ip_restrictions ) ); - if ( ! in_array( $_SERVER['REMOTE_ADDR'], $allowed_ips, true ) ) { - $is_enabled = false; - } - } - - // Check the data sampling rate. - if ( $is_enabled ) { - $sampling_rate = (int) ( $config[ Basic_Configuration_Tab::DATA_SAMPLING ] ?? 100 ); - if ( mt_rand( 0, 100 ) >= $sampling_rate ) { - $is_enabled = false; - } - } - - /** - * Filter the final decision on whether to log a request. - * - * @param bool $is_enabled True if logging is enabled, false otherwise. - * @param array $config The current logging configuration. - */ - return apply_filters( 'wpgraphql_logging_is_enabled', $is_enabled, $config ); - } -} \ No newline at end of file + /** + * Checks if logging is enabled based on user settings. + * + * @param array $config The logging configuration. + * + * phpcs:disable Generic.Metrics.CyclomaticComplexity, SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh + */ + protected function is_logging_enabled( array $config ): bool { + $is_enabled = true; + + // Check the main "Enabled" checkbox. + if ( ! (bool) ( $config[ Basic_Configuration_Tab::ENABLED ] ?? false ) ) { + $is_enabled = false; + } + + // Check if the current user is an admin if that option is enabled. + if ( $is_enabled && ( (bool) ( $config[ Basic_Configuration_Tab::ADMIN_USER_LOGGING ] ?? false ) ) ) { + if ( ! current_user_can( 'manage_options' ) ) { + $is_enabled = false; + } + } + + // Check for IP restrictions. + $ip_restrictions = $config[ Basic_Configuration_Tab::IP_RESTRICTIONS ] ?? ''; + if ( $is_enabled && ! empty( $ip_restrictions ) ) { + $allowed_ips = array_map( 'trim', explode( ',', $ip_restrictions ) ); + $remote_addr = $_SERVER['REMOTE_ADDR'] ?? ''; // @phpcs:ignore + if ( ! in_array( $remote_addr, $allowed_ips, true ) ) { + $is_enabled = false; + } + } + + // Check the data sampling rate. + if ( $is_enabled ) { + $sampling_rate = (int) ( $config[ Basic_Configuration_Tab::DATA_SAMPLING ] ?? 100 ); + if ( wp_rand( 0, 100 ) >= $sampling_rate ) { + $is_enabled = false; + } + } + + /** + * Filter the final decision on whether to log a request. + * + * @param bool $is_enabled True if logging is enabled, false otherwise. + * @param array $config The current logging configuration. + */ + return apply_filters( 'wpgraphql_logging_is_enabled', $is_enabled, $config ); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryEventLifecycleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryEventLifecycleTest.php deleted file mode 100644 index 855f4529..00000000 --- a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryEventLifecycleTest.php +++ /dev/null @@ -1,472 +0,0 @@ -test_handler = new TestHandler(); - $this->mock_logger = LoggerService::get_instance('test_lifecycle', [$this->test_handler], [], []); - $this->reset_lifecycle_instance(); - $this->reset_event_manager(); - } - - /** - * Clean up after each test. - */ - public function tearDown(): void { - parent::tearDown(); - $this->reset_lifecycle_instance(); - $this->reset_event_manager(); - $this->reset_logger_instances(); - } - - /** - * Reset QueryEventLifecycle singleton state. - */ - private function reset_lifecycle_instance(): void { - $reflection = new ReflectionClass(QueryEventLifecycle::class); - $instance_prop = $reflection->getProperty('instance'); - $instance_prop->setAccessible(true); - $instance_prop->setValue(null, null); - } - - /** - * Reset EventManager static state. - */ - private function reset_event_manager(): void { - $reflection = new ReflectionClass(EventManager::class); - - $events_prop = $reflection->getProperty('events'); - $events_prop->setAccessible(true); - $events_prop->setValue(null, []); - - $transforms_prop = $reflection->getProperty('transforms'); - $transforms_prop->setAccessible(true); - $transforms_prop->setValue(null, []); - } - - /** - * Reset LoggerService instances. - */ - private function reset_logger_instances(): void { - $reflection = new ReflectionClass(LoggerService::class); - $instances_prop = $reflection->getProperty('instances'); - $instances_prop->setAccessible(true); - $instances_prop->setValue(null, []); - } - - /** - * Create a mock LoggerService instance with injected test handler. - */ - private function create_lifecycle_with_mock_logger(): QueryEventLifecycle { - $reflection = new ReflectionClass(QueryEventLifecycle::class); - $lifecycle = $reflection->newInstanceWithoutConstructor(); - - $logger_prop = $reflection->getProperty('logger'); - $logger_prop->setAccessible(true); - $logger_prop->setValue($lifecycle, $this->mock_logger); - - return $lifecycle; - } - - /** - * Create a mock WPGraphQL Request object. - */ - private function create_mock_request(string $query = '{ posts { title } }', ?string $operation_name = null, ?array $variables = null): Request { - $mock_request = $this->createMock(Request::class); - - $mock_params = new \stdClass(); - $mock_params->query = $query; - $mock_params->operation = $operation_name; - $mock_params->variables = $variables; - - $mock_request->params = $mock_params; - - return $mock_request; - } - - /** - * Create a mock WPGraphQL Schema object. - */ - private function create_mock_schema(): WPSchema { - return $this->createMock(WPSchema::class); - } - - /** - * Create a mock ExecutionResult object. - */ - private function create_mock_execution_result(array $data = [], array $errors = []): ExecutionResult { - $mock_result = $this->createMock(ExecutionResult::class); - $mock_result->data = $data; - $mock_result->errors = $errors; - return $mock_result; - } - - public function test_init_returns_singleton_instance(): void { - $instance1 = QueryEventLifecycle::init(); - $instance2 = QueryEventLifecycle::init(); - - $this->assertInstanceOf(QueryEventLifecycle::class, $instance1); - $this->assertSame($instance1, $instance2, 'init() should return the same singleton instance'); - } - - public function test_init_creates_new_instance_when_none_exists(): void { - $reflection = new ReflectionClass(QueryEventLifecycle::class); - $instance_prop = $reflection->getProperty('instance'); - $instance_prop->setAccessible(true); - - $this->assertNull($instance_prop->getValue()); - - $instance = QueryEventLifecycle::init(); - - $this->assertInstanceOf(QueryEventLifecycle::class, $instance); - $this->assertSame($instance, $instance_prop->getValue()); - } - - public function test_log_pre_request_logs_correctly(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - - $query = '{ posts { title } }'; - $operation_name = 'GetPosts'; - $variables = ['limit' => 10]; - - $lifecycle->log_pre_request($query, $operation_name, $variables); - - $this->assertTrue($this->test_handler->hasInfoRecords()); - $records = $this->test_handler->getRecords(); - $this->assertCount(1, $records); - - $record = $records[0]; - $this->assertEquals('WPGraphQL Pre Request', $record['message']); - $this->assertEquals($query, $record['context']['query']); - $this->assertEquals($operation_name, $record['context']['operation_name']); - $this->assertEquals($variables, $record['context']['variables']); - } - - public function test_log_pre_request_handles_null_values(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - - $query = '{ posts { title } }'; - - $lifecycle->log_pre_request($query, null, null); - - $this->assertTrue($this->test_handler->hasInfoRecords()); - $records = $this->test_handler->getRecords(); - $record = $records[0]; - - $this->assertEquals($query, $record['context']['query']); - $this->assertNull($record['context']['operation_name']); - $this->assertNull($record['context']['variables']); - } - - public function test_log_graphql_before_execute_logs_correctly(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - - $query = '{ posts { title } }'; - $operation_name = 'GetPosts'; - $variables = ['limit' => 10]; - $request = $this->create_mock_request($query, $operation_name, $variables); - - $lifecycle->log_graphql_before_execute($request); - - $this->assertTrue($this->test_handler->hasInfoRecords()); - $records = $this->test_handler->getRecords(); - $record = $records[0]; - - $this->assertEquals('WPGraphQL Before Query Execution', $record['message']); - $this->assertEquals($query, $record['context']['query']); - $this->assertEquals($operation_name, $record['context']['operation_name']); - $this->assertEquals($variables, $record['context']['variables']); - $this->assertEquals($request->params, $record['context']['params']); - } - - - - - - public function test_log_before_response_returned_logs_info_for_success(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - - $filtered_response = $this->create_mock_execution_result(['posts' => []]); - $response = $this->create_mock_execution_result(['posts' => []]); - $schema = $this->create_mock_schema(); - $operation = 'GetPosts'; - $query = '{ posts { title } }'; - $variables = ['limit' => 10]; - $request = $this->create_mock_request(); - $query_id = 'query_123'; - - $lifecycle->log_before_response_returned($filtered_response, $response, $schema, $operation, $query, $variables, $request, $query_id); - - $this->assertTrue($this->test_handler->hasInfoRecords()); - $records = $this->test_handler->getRecords(); - $record = $records[0]; - - $this->assertEquals(200, $record['level']); - $this->assertEquals('WPGraphQL Response', $record['message']); - } - - public function test_log_before_response_returned_logs_error_for_errors(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - - $errors = [['message' => 'Field not found']]; - $response = $this->create_mock_execution_result([], $errors); - $schema = $this->create_mock_schema(); - $query = '{ invalidField }'; - $request = $this->create_mock_request(); - - $lifecycle->log_before_response_returned($response, $response, $schema, null, $query, null, $request, null); - - $this->assertTrue($this->test_handler->hasErrorRecords()); - $records = $this->test_handler->getRecords(); - $record = $records[0]; - - $this->assertEquals('WPGraphQL Response with Errors', $record['message']); - $this->assertEquals(400, $record['level']); - $this->assertEquals($errors, $record['context']['errors']); - } - - public function test_get_response_errors_extracts_from_execution_result(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - $reflection = new ReflectionClass($lifecycle); - $method = $reflection->getMethod('get_response_errors'); - $method->setAccessible(true); - - $errors = [['message' => 'Test error']]; - $response = $this->create_mock_execution_result([], $errors); - - $result = $method->invoke($lifecycle, $response); - - $this->assertEquals($errors, $result); - } - - public function test_get_response_errors_extracts_from_array(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - $reflection = new ReflectionClass($lifecycle); - $method = $reflection->getMethod('get_response_errors'); - $method->setAccessible(true); - - $errors = [['message' => 'Test error']]; - $response = ['data' => [], 'errors' => $errors]; - - $result = $method->invoke($lifecycle, $response); - - $this->assertEquals($errors, $result); - } - - public function test_get_response_errors_returns_null_for_no_errors(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - $reflection = new ReflectionClass($lifecycle); - $method = $reflection->getMethod('get_response_errors'); - $method->setAccessible(true); - - $response = $this->create_mock_execution_result(['data' => []], []); - - $result = $method->invoke($lifecycle, $response); - - $this->assertNull($result); - } - - public function test_get_response_errors_returns_null_for_empty_errors_array(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - $reflection = new ReflectionClass($lifecycle); - $method = $reflection->getMethod('get_response_errors'); - $method->setAccessible(true); - - $response = ['data' => [], 'errors' => []]; - - $result = $method->invoke($lifecycle, $response); - - $this->assertNull($result); - } - - public function test_integration_with_event_manager_transform(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - - $transform_called = false; - $received_payload = null; - - EventManager::subscribe_to_transform(Events::PRE_REQUEST, function($payload) use (&$transform_called, &$received_payload) { - $transform_called = true; - $received_payload = $payload; - $payload['context']['transformed'] = true; - return $payload; - }); - - $lifecycle->log_pre_request('{ test }', null, null); - - $this->assertTrue($transform_called); - $this->assertArrayHasKey('context', $received_payload); - $this->assertEquals('{ test }', $received_payload['context']['query']); - - $records = $this->test_handler->getRecords(); - $this->assertTrue($records[0]['context']['transformed']); - } - - public function test_integration_with_event_manager_publish(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - - $publish_called = false; - $received_payload = null; - - EventManager::subscribe(Events::PRE_REQUEST, function($payload) use (&$publish_called, &$received_payload) { - $publish_called = true; - $received_payload = $payload; - }); - - $lifecycle->log_pre_request('{ test }', null, null); - - $this->assertTrue($publish_called); - $this->assertArrayHasKey('context', $received_payload); - $this->assertArrayHasKey('level', $received_payload); - $this->assertEquals('INFO', $received_payload['level']); - } - - public function test_exception_handling_in_log_pre_request(): void { - // Create a lifecycle with a mock logger that throws an exception - $mock_logger = $this->createMock(LoggerService::class); - $mock_logger->method('log')->willThrowException(new \Exception('Logger error')); - - $reflection = new ReflectionClass(QueryEventLifecycle::class); - $lifecycle = $reflection->newInstanceWithoutConstructor(); - - $logger_prop = $reflection->getProperty('logger'); - $logger_prop->setAccessible(true); - $logger_prop->setValue($lifecycle, $mock_logger); - - // Should not throw an exception - $lifecycle->log_pre_request('{ test }', null, null); - - // The test passes if no exception is thrown - $this->assertTrue(true); - } - - - public function test_setup_registers_correct_priorities(): void { - $lifecycle = QueryEventLifecycle::init(); - - $this->assertEquals(10, has_action('do_graphql_request', [$lifecycle, 'log_pre_request'])); - $this->assertEquals(10, has_action('graphql_before_execute', [$lifecycle, 'log_graphql_before_execute'])); - $this->assertEquals(10, has_action('graphql_return_response', [$lifecycle, 'log_before_response_returned'])); - } - - /** - * Data provider for lifecycle methods. - */ - public function lifecycleMethodsProvider(): array { - return [ - [ - 'method' => 'log_pre_request', - 'event' => Events::PRE_REQUEST, - 'args' => ['{ test }', null, null], - 'expected_message' => 'WPGraphQL Pre Request', - ], - [ - 'method' => 'log_graphql_before_execute', - 'event' => Events::BEFORE_GRAPHQL_EXECUTION, - 'args' => [null], // Will be replaced with mock request - 'expected_message' => 'WPGraphQL Before Query Execution', - ], - ]; - } - - /** - * @dataProvider lifecycleMethodsProvider - */ - public function test_lifecycle_methods_use_correct_events(string $method, string $event, array $args, string $expected_message): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - - $event_published = false; - EventManager::subscribe($event, function() use (&$event_published) { - $event_published = true; - }); - - if ($method === 'log_graphql_before_execute') { - $args[0] = $this->create_mock_request(); - } - - $lifecycle->$method(...$args); - - $this->assertTrue($event_published, "Event {$event} should be published by {$method}"); - - $records = $this->test_handler->getRecords(); - $this->assertEquals($expected_message, $records[0]['message']); - } - - public function test_process_application_error_method_exists(): void { - $lifecycle = $this->create_lifecycle_with_mock_logger(); - $reflection = new ReflectionClass($lifecycle); - - $this->assertTrue($reflection->hasMethod('process_application_error')); - - $method = $reflection->getMethod('process_application_error'); - $method->setAccessible(true); - - // Should not throw an exception when called - $exception = new \Exception('Test exception'); - $method->invoke($lifecycle, Events::PRE_REQUEST, $exception); - - $this->assertTrue(true); - } - - public function test_all_lifecycle_methods_handle_exceptions_gracefully(): void { - // Create a mock logger that always throws exceptions - $mock_logger = $this->createMock(LoggerService::class); - $mock_logger->method('log')->willThrowException(new \Exception('Logger error')); - - $reflection = new ReflectionClass(QueryEventLifecycle::class); - $lifecycle = $reflection->newInstanceWithoutConstructor(); - - $logger_prop = $reflection->getProperty('logger'); - $logger_prop->setAccessible(true); - $logger_prop->setValue($lifecycle, $mock_logger); - - // Test all lifecycle methods - none should throw exceptions - $lifecycle->log_pre_request('{ test }', null, null); - - $request = $this->create_mock_request(); - $lifecycle->log_graphql_before_execute($request); - - $response = $this->create_mock_execution_result(); - $schema = $this->create_mock_schema(); - - $lifecycle->log_before_response_returned($response, $response, $schema, null, '{ test }', null, $request, null); - - $this->assertTrue(true, 'All lifecycle methods should handle exceptions gracefully'); - } -} diff --git a/plugins/wpgraphql-logging/wpgraphql-logging.php b/plugins/wpgraphql-logging/wpgraphql-logging.php index 7d8e0c76..91b33a12 100644 --- a/plugins/wpgraphql-logging/wpgraphql-logging.php +++ b/plugins/wpgraphql-logging/wpgraphql-logging.php @@ -167,7 +167,6 @@ static function (): void { } } - /** * Load plugin text domain. */