$body SearchAnalyticsQueryRequest body. * @return array */ public static function query( array $body ): array { $dims = array_values( array_map( 'strval', (array) ( $body['dimensions'] ?? array() ) ) ); $limit = isset( $body['rowLimit'] ) ? max( 1, (int) $body['rowLimit'] ) : 25; $dates = self::dates( (string) ( $body['startDate'] ?? '' ), (string) ( $body['endDate'] ?? '' ) ); $page = self::extract_page_filter( is_array( $body['dimensionFilterGroups'] ?? null ) ? $body['dimensionFilterGroups'] : array() ); $search = FakeData::CHANNEL_SHARES['Organic Search'] * self::CLICKS_PER_ORGANIC_SESSION; if ( in_array( 'page', $dims, true ) ) { $keys = array(); foreach ( Baseline::page_shares( array_keys( FakeReports::path_pool() ) ) as $path => $share ) { $keys[ (string) $path ] = array( home_url( (string) $path ), $share ); } } else { $scope = null !== $page ? FakeReports::paths_share( array( (string) wp_parse_url( $page, PHP_URL_PATH ) ) ) : 1.0; $keys = array(); foreach ( Baseline::ranked( self::keywords( $page ), self::LISTED_QUERY_SHARE * $scope ) as $keyword => $share ) { $keys[ (string) $keyword ] = array( (string) $keyword, $share ); } } // Floored once over the whole range (not per day), so small queries // still get their clicks; the sum of floors never exceeds the floor // of the sum, so queries still add up to at most their page. $sessions = Baseline::sum( $dates )['sessions']; $rows = array(); foreach ( $keys as $id => [ $key, $share ] ) { $clicks = (int) floor( $sessions * $share * $search ); $ctr = 0.02 + 0.12 * Baseline::hash01( 'ctr|' . $id ); $impressions = $clicks > 0 ? (int) round( $clicks / $ctr ) : 5 + (int) round( 60 * Baseline::hash01( 'imp|' . $id ) ); $rows[] = array( 'keys' => array( $key ), 'clicks' => $clicks, 'impressions' => $impressions, 'ctr' => $impressions > 0 ? round( $clicks / $impressions, 4 ) : 0.0, 'position' => round( 2 + 30 * Baseline::hash01( 'pos|' . $id ), 1 ), ); } // The real API returns rows by clicks, descending. usort( $rows, static fn( array $a, array $b ): int => $b['clicks'] <=> $a['clicks'] ); // No dimensions: one site-wide row, like the real API. if ( array() === $dims ) { $clicks = (int) array_sum( array_column( $rows, 'clicks' ) ); $impressions = (int) array_sum( array_column( $rows, 'impressions' ) ); $weighted = array_sum( array_map( static fn( array $row ): float => $row['position'] * $row['impressions'], $rows ) ); return array( 'rows' => array( array( 'keys' => array(), 'clicks' => $clicks, 'impressions' => $impressions, 'ctr' => $impressions > 0 ? round( $clicks / $impressions, 4 ) : 0.0, 'position' => $impressions > 0 ? round( $weighted / $impressions, 1 ) : 0.0, ), ), ); } return array( 'rows' => array_slice( $rows, 0, $limit ) ); } /** * Every date (Ymd) from start to end (Y-m-d), inclusive. * * @param string $start Start date. * @param string $end End date. * @return string[] */ private static function dates( string $start, string $end ): array { $cursor = strtotime( $start ); $stop = strtotime( $end ); $dates = array(); while ( false !== $cursor && false !== $stop && $cursor <= $stop ) { $dates[] = gmdate( 'Ymd', $cursor ); $cursor += DAY_IN_SECONDS; } return $dates; } /** * The query keywords: from the filtered page's title, or the generic * pool. * * @param string|null $page_filter The filtered page URL, if any. * @return string[] */ private static function keywords( ?string $page_filter ): array { return null !== $page_filter ? array_values( array_unique( self::keywords_from_page( $page_filter ) ) ) : FakeData::GSC_KEYWORDS; } /** * The page URL of a dimensionFilterGroups 'page' filter, if any: the * expression of an equals filter, or the site URL of the path in an * includingRegex filter of the form ^https?://[^/]+/path/?$. * * @param array $groups dimensionFilterGroups. */ private static function extract_page_filter( array $groups ): ?string { foreach ( $groups as $group ) { foreach ( (array) ( is_array( $group ) ? ( $group['filters'] ?? array() ) : array() ) as $filter ) { if ( ! is_array( $filter ) || 'page' !== ( $filter['dimension'] ?? '' ) ) { continue; } $expression = (string) ( $filter['expression'] ?? '' ); if ( 'includingRegex' === ( $filter['operator'] ?? '' ) && 1 === preg_match( '#^\^https\?://\[\^/\]\+(.*)/\?\$$#', $expression, $m ) ) { return home_url( (string) preg_replace( '/\\\\(.)/', '$1', $m[1] ) . '/' ); } return $expression; } } return null; } /** * Search keywords derived from a page's post title (two-word phrases), * falling back to the generic pool when the page has no matching post. * * @param string $page_url The filtered page URL. * @return string[] */ private static function keywords_from_page( string $page_url ): array { $post_id = url_to_postid( $page_url ); $title = $post_id > 0 ? (string) get_the_title( $post_id ) : ''; if ( '' === $title ) { return FakeData::GSC_KEYWORDS; } $words = array_values( array_filter( (array) preg_split( '/\s+/', strtolower( $title ) ) ) ); if ( count( $words ) < 2 ) { return array_merge( array( strtolower( $title ) ), FakeData::GSC_KEYWORDS ); } $phrases = array( strtolower( $title ) ); $word_count = count( $words ); $stop = array( 'a', 'an', 'and', 'as', 'for', 'how', 'in', 'is', 'made', 'of', 'on', 'our', 'that', 'the', 'to', 'what', 'with' ); for ( $i = 0; $i < $word_count - 1; $i++ ) { if ( in_array( $words[ $i ], $stop, true ) || in_array( $words[ $i + 1 ], $stop, true ) ) { continue; } $phrases[] = $words[ $i ] . ' ' . $words[ $i + 1 ]; } return array_merge( $phrases, FakeData::GSC_KEYWORDS ); } }