string, values => float[]. * * @var array> */ private $rows; /** * Column totals, aligned with the metric list. Re-aggregated by the API, not summed from rows. * * @var float[] */ private $totals; /** * Metric names, in the order the values appear. * * @var string[] */ private $metrics; /** * Envelope facts worth showing: start, end, timezone, currency, freshness, attribution. * * @var array */ private $meta; /** * Whether the result was capped. * * @var bool */ private $truncated; /** * Failure message, empty when the report succeeded. * * @var string */ private $error; /** * @param array> $rows Rows. * @param float[] $totals Column totals. * @param string[] $metrics Metric names. * @param array $meta Envelope facts. * @param bool $truncated Whether rows were capped. * @param string $error Failure message. */ private function __construct( array $rows, array $totals, array $metrics, array $meta, bool $truncated, string $error ) { $this->rows = $rows; $this->totals = $totals; $this->metrics = $metrics; $this->meta = $meta; $this->truncated = $truncated; $this->error = $error; } /** * A report that could not be produced. * * @param string $message Why. * @return self */ public static function failed( string $message ): self { return new self( array(), array(), array(), array(), false, $message ); } /** * Build from a decoded reporting API envelope. * * @param array $body Decoded response. * @return self */ public static function from_body( array $body ): self { $metrics = array(); foreach ( self::list_of( $body, 'metric_headers' ) as $header ) { if ( is_array( $header ) && isset( $header['name'] ) && is_scalar( $header['name'] ) ) { $metrics[] = (string) $header['name']; } } $rows = array(); foreach ( self::list_of( $body, 'rows' ) as $row ) { if ( ! is_array( $row ) ) { continue; } $dimensions = isset( $row['dimensions'] ) && is_array( $row['dimensions'] ) ? $row['dimensions'] : array(); $values = isset( $row['metrics'] ) && is_array( $row['metrics'] ) ? $row['metrics'] : array(); $rows[] = array( // Joined with a thin space rather than a comma: a two-dimension row such as // "google / cpc" reads as one thing, and the separator must not look like data. 'label' => implode( ' / ', array_map( array( __CLASS__, 'text' ), $dimensions ) ), 'dimensions' => array_map( array( __CLASS__, 'text' ), $dimensions ), 'values' => array_map( array( __CLASS__, 'number' ), array_values( $values ) ), ); } $totals = array(); if ( isset( $body['totals']['metrics'] ) && is_array( $body['totals']['metrics'] ) ) { $totals = array_map( array( __CLASS__, 'number' ), array_values( $body['totals']['metrics'] ) ); } return new self( $rows, $totals, $metrics, array( 'start' => self::text( $body['date_range']['start'] ?? '' ), 'end' => self::text( $body['date_range']['end'] ?? '' ), 'timezone' => self::text( $body['property']['timezone'] ?? '' ), 'currency' => self::text( $body['property']['currency'] ?? '' ), 'name' => self::text( $body['property']['name'] ?? '' ), 'freshness' => self::text( $body['data_freshness'] ?? '' ), 'attribution' => self::text( $body['attribution'] ?? '' ), ), ! empty( $body['truncated'] ), '' ); } /** * Whether there is anything to draw. * * @return bool */ public function ok(): bool { return '' === $this->error; } /** * Why the report is unavailable. * * @return string */ public function error(): string { return $this->error; } /** * Whether the report succeeded but found nothing. * * @return bool */ public function is_empty(): bool { return array() === $this->rows; } /** * The rows. * * @return array> */ public function rows(): array { return $this->rows; } /** * Metric names, in column order. * * @return string[] */ public function metrics(): array { return $this->metrics; } /** * The position of a metric in each row's value list. * * @param string $metric Metric name. * @return int Minus one when the metric is absent. */ public function column( string $metric ): int { $index = array_search( $metric, $this->metrics, true ); return false === $index ? -1 : (int) $index; } /** * One column total. * * @param string $metric Metric name. * @return float Zero when the metric is absent. */ public function total( string $metric ): float { $index = $this->column( $metric ); return $index >= 0 && isset( $this->totals[ $index ] ) ? $this->totals[ $index ] : 0.0; } /** * The largest value in one column, which is what a bar chart scales against. * * @param string $metric Metric name. * @return float */ public function peak( string $metric ): float { $index = $this->column( $metric ); if ( $index < 0 ) { return 0.0; } $peak = 0.0; foreach ( $this->rows as $row ) { $value = isset( $row['values'][ $index ] ) ? (float) $row['values'][ $index ] : 0.0; $peak = max( $peak, $value ); } return $peak; } /** * One column as a flat list, in row order. * * @param string $metric Metric name. * @return float[] */ public function series( string $metric ): array { $index = $this->column( $metric ); if ( $index < 0 ) { return array(); } return array_map( static function ( array $row ) use ( $index ): float { return isset( $row['values'][ $index ] ) ? (float) $row['values'][ $index ] : 0.0; }, $this->rows ); } /** * The first dimension of every row, in row order. * * @return string[] */ public function labels(): array { return array_map( static function ( array $row ): string { return isset( $row['dimensions'][0] ) ? (string) $row['dimensions'][0] : ''; }, $this->rows ); } /** * One envelope fact. * * @param string $key One of start, end, timezone, currency, name, freshness, attribution. * @return string */ public function meta( string $key ): string { return $this->meta[ $key ] ?? ''; } /** * Whether the API capped the rows it returned. * * @return bool */ public function is_truncated(): bool { return $this->truncated; } /** * A copy with different rows, for the one transformation a caller legitimately needs: filling the * days a time series left out, because a day with no visits is absent from the answer rather than * zero, and a chart that closed the gap would draw a week as though it were a day. * * @param array> $rows Replacement rows. * @return self */ public function with_rows( array $rows ): self { return new self( $rows, $this->totals, $this->metrics, $this->meta, $this->truncated, $this->error ); } /** * Coerce an API value to a display string. * * @param mixed $value Raw value. * @return string */ private static function text( $value ): string { return is_scalar( $value ) ? trim( (string) $value ) : ''; } /** * Coerce an API value to a number. * * @param mixed $value Raw value. * @return float */ private static function number( $value ): float { return is_numeric( $value ) ? (float) $value : 0.0; } /** * One top-level body key as a list. * * @param array $body Decoded response. * @param string $key Key to read. * @return array */ private static function list_of( array $body, string $key ): array { return isset( $body[ $key ] ) && is_array( $body[ $key ] ) ? array_values( $body[ $key ] ) : array(); } }