Files
2026-04-28 15:13:50 +02:00

354 lines
9.6 KiB
PHP

<?php
/**
* Class Google\Site_Kit\Modules\Search_Console\Datapoints\SearchAnalyticsBatch
*
* @package Google\Site_Kit\Modules\Search_Console\Datapoints
* @copyright 2025 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/
namespace Google\Site_Kit\Modules\Search_Console\Datapoints;
use Exception;
use Google\Site_Kit\Core\Modules\Datapoint;
use Google\Site_Kit\Core\Modules\Executable_Datapoint;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception;
use Google\Site_Kit_Dependencies\Google\Service\Exception as Google_Service_Exception;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole as Google_Service_SearchConsole;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\SearchAnalyticsQueryResponse;
use WP_Error;
/**
* Datapoint class for Search Console search analytics batch requests.
*
* @since 1.170.0
* @access private
* @ignore
*/
class SearchAnalyticsBatch extends Datapoint implements Executable_Datapoint {
const REQUEST_METHODS = array( 'POST' );
const REST_METHODS = array( 'POST' );
const DATAPOINT = 'searchanalytics-batch';
/**
* Callback to obtain the Search Console service.
*
* @since 1.170.0
* @var callable|\Closure
*/
private $get_service;
/**
* Callback to prepare single search analytics request arguments.
*
* @since 1.170.0
* @var callable|\Closure
*/
private $prepare_args;
/**
* Callback to build a search analytics request.
*
* @since 1.170.0
* @var callable|\Closure
*/
private $create_request;
/**
* Identifiers for the requested payloads.
*
* @since 1.170.0
* @var array
*/
private $request_identifiers = array();
/**
* Captured errors for individual requests.
*
* @since 1.170.0
* @var array
*/
private $request_errors = array();
/**
* Constructor.
*
* @since 1.170.0
*
* @param array $definition Datapoint definition.
*/
public function __construct( array $definition ) {
parent::__construct( $definition );
$this->get_service = isset( $definition['get_service'] ) ? $definition['get_service'] : null;
$this->prepare_args = isset( $definition['prepare_args'] ) ? $definition['prepare_args'] : null;
$this->create_request = isset( $definition['create_request'] ) ? $definition['create_request'] : null;
}
/**
* Creates a request object.
*
* @since 1.170.0
*
* @param Data_Request $data_request Data request object.
* @return callable|WP_Error Callable to execute the batch request, or WP_Error.
* @throws Missing_Required_Param_Exception Thrown when required parameters are missing.
*/
public function create_request( Data_Request $data_request ) {
$requests = isset( $data_request->data['requests'] ) ? $data_request->data['requests'] : null;
if ( empty( $requests ) || ! is_array( $requests ) ) {
throw new Missing_Required_Param_Exception( 'requests' );
}
$this->request_identifiers = array();
$this->request_errors = array();
$service = $this->get_searchconsole_service();
$batch = $service->createBatch();
$has_valid_requests = false;
foreach ( $requests as $request_data ) {
$identifier = $this->normalize_identifier( $request_data );
$this->request_identifiers[] = $identifier;
try {
$args = $this->prepare_request_args( $request_data );
if ( is_wp_error( $args ) ) {
$this->request_errors[ $identifier ] = $args;
continue;
}
$single_request = $this->build_single_request( $args );
if ( is_wp_error( $single_request ) ) {
$this->request_errors[ $identifier ] = $single_request;
continue;
}
$batch->add( $single_request, $identifier );
$has_valid_requests = true;
} catch ( Exception $exception ) {
$this->request_errors[ $identifier ] = $this->exception_to_error( $exception );
}
}
if ( empty( $this->request_identifiers ) ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'requests' ),
array( 'status' => 400 )
);
}
if ( ! $has_valid_requests ) {
return function () {
return array();
};
}
return function () use ( $batch ) {
return $batch->execute();
};
}
/**
* Parses a response.
*
* @since 1.170.0
*
* @param mixed $response Request response.
* @param Data_Request $data_request Data request object.
* @return array|WP_Error Associative array of responses keyed by identifier, or WP_Error on batch failure.
*/
public function parse_response( $response, Data_Request $data_request ) {
if ( is_wp_error( $response ) ) {
return $response;
}
$results = $this->request_errors;
if ( is_array( $response ) ) {
foreach ( $response as $identifier => $single_response ) {
$normalized_identifier = $this->normalize_response_identifier( $identifier );
$results[ $normalized_identifier ] = $this->parse_single_response( $single_response );
}
}
// Preserve the original request ordering and ensure all identifiers are represented.
$ordered_results = array();
foreach ( $this->request_identifiers as $identifier ) {
if ( array_key_exists( $identifier, $results ) ) {
$ordered_results[ $identifier ] = $results[ $identifier ];
} else {
$ordered_results[ $identifier ] = new WP_Error(
'searchanalytics_batch_missing_response',
__( 'Missing response from Search Console.', 'google-site-kit' )
);
}
}
// Append any unexpected identifiers returned by the API.
foreach ( $results as $identifier => $single_result ) {
if ( array_key_exists( $identifier, $ordered_results ) ) {
continue;
}
$ordered_results[ $identifier ] = $single_result;
}
return $ordered_results;
}
/**
* Parses a single batch response.
*
* @since 1.170.0
*
* @param mixed $response Single response.
* @return array|WP_Error Parsed rows or WP_Error.
*/
private function parse_single_response( $response ) {
if ( is_wp_error( $response ) ) {
return $response;
}
if ( $response instanceof Google_Service_Exception ) {
return $this->exception_to_error( $response );
}
if ( $response instanceof SearchAnalyticsQueryResponse ) {
return $response->getRows();
}
if ( is_object( $response ) && method_exists( $response, 'getRows' ) ) {
return $response->getRows();
}
return $response;
}
/**
* Builds a single request.
*
* @since 1.170.0
*
* @param array $args Prepared request arguments.
* @return mixed Request instance or WP_Error.
*/
private function build_single_request( array $args ) {
if ( ! is_callable( $this->create_request ) ) {
return new WP_Error(
'invalid_request_callback',
__( 'Invalid Search Console request callback.', 'google-site-kit' )
);
}
return call_user_func( $this->create_request, $args );
}
/**
* Prepares request arguments for a single search analytics request.
*
* @since 1.170.0
*
* @param array $request_data Raw request data.
* @return array|WP_Error Prepared arguments or WP_Error.
*/
private function prepare_request_args( array $request_data ) {
if ( ! is_callable( $this->prepare_args ) ) {
return new WP_Error(
'invalid_request_args_callback',
__( 'Invalid Search Console request arguments.', 'google-site-kit' )
);
}
return call_user_func( $this->prepare_args, $request_data );
}
/**
* Gets the Search Console service instance.
*
* @since 1.170.0
*
* @return Google_Service_SearchConsole Search Console service instance.
* @throws Missing_Required_Param_Exception When the service callback is missing.
*/
private function get_searchconsole_service() {
if ( is_callable( $this->get_service ) ) {
return call_user_func( $this->get_service );
}
throw new Missing_Required_Param_Exception( 'service' );
}
/**
* Normalizes a request identifier to a string.
*
* @since 1.170.0
*
* @param array $request_data Request data.
* @return string Normalized identifier.
* @throws Missing_Required_Param_Exception When the identifier is missing or invalid.
*/
private function normalize_identifier( array $request_data ) {
if ( isset( $request_data['identifier'] ) ) {
$identifier = $request_data['identifier'];
} elseif ( isset( $request_data['id'] ) ) {
$identifier = $request_data['id'];
} else {
throw new Missing_Required_Param_Exception( 'identifier' );
}
if ( ! is_scalar( $identifier ) ) {
throw new Missing_Required_Param_Exception( 'identifier' );
}
$identifier = (string) $identifier;
if ( '' === $identifier ) {
throw new Missing_Required_Param_Exception( 'identifier' );
}
return $identifier;
}
/**
* Normalizes a response identifier to align with requested keys.
*
* @since 1.170.0
*
* @param string|int $identifier Raw response identifier.
* @return string|int Normalized identifier.
*/
private function normalize_response_identifier( $identifier ) {
if ( is_string( $identifier ) && 0 === strpos( $identifier, 'response-' ) ) {
$identifier = substr( $identifier, strlen( 'response-' ) );
}
return $identifier;
}
/**
* Converts an exception to a WP_Error instance.
*
* @since 1.170.0
*
* @param Exception $exception Exception instance.
* @return WP_Error WP_Error instance.
*/
private function exception_to_error( Exception $exception ) {
$status = (int) ( $exception->getCode() ?: 500 );
return new WP_Error(
'searchanalytics_batch_request_failed',
$exception->getMessage(),
array( 'status' => $status )
);
}
}