key ] = $d; } } } return $all; } /** @return string[] The only dimension keys that exist. */ public static function keys(): array { return array_keys( self::all() ); } public static function get( string $key ): ?Saddle_Analytics_Dimension { return self::all()[ $key ] ?? null; } /** Which touch the source dimensions read: 'last' only when the setting says so. */ public static function attribution(): string { return class_exists( 'Saddle_Analytics_Settings' ) && 'last' === Saddle_Analytics_Settings::get( 'attribution' ) ? 'last' : 'first'; } /** * Agent input → the dashboard's filters: any visitors-table dimension, keyed * as here. Slugs become the stored codes; an unknown slug is dropped, never * read as code 0 (Direct / no device). */ public static function filters_from( array $input ): array { $filters = array(); foreach ( self::all() as $key => $d ) { if ( 'visitors' !== $d->table || ! isset( $input[ $key ] ) ) { continue; } $value = (string) $input[ $key ]; $code = match ( $d->format ) { 'channel' => array_search( $value, Saddle_Analytics_Channel::keys(), true ), 'device' => array_search( $value, Saddle_Analytics_Device::keys(), true ), default => 'country' === $key ? strtoupper( $value ) : $value, }; if ( false !== $code ) { $filters[ $key ] = $code; } } return $filters; } /** * The filters as SQL on visitors columns: " AND {prefix}column = %s" each, * with their params. Names come only from this class (rule 3); a filter on * a hits dimension, or a non-scalar value, is ignored. * * @return array{0:string,1:array} */ public static function filter_clause( array $filters, string $prefix = '' ): array { $sql = ''; $params = array(); foreach ( $filters as $key => $value ) { $d = self::get( (string) $key ); if ( ! $d || 'visitors' !== $d->table || ! is_scalar( $value ) ) { continue; } $sql .= " AND {$prefix}{$d->column} = " . ( in_array( $d->column, array( 'first_channel', 'last_channel', 'device' ), true ) ? '%d' : '%s' ); $params[] = $value; } return array( $sql, $params ); } /** * The same filters for a hits query: " AND visitor_id IN (...)", or '' * when there are none. A subquery, so it runs on MySQL and SQLite alike. * * @return array{0:string,1:array} */ public static function visitor_scope( array $filters ): array { list( $sql, $params ) = self::filter_clause( $filters ); if ( '' === $sql ) { return array( '', array() ); } $t = Saddle_Analytics_Install::tables(); return array( " AND visitor_id IN (SELECT visitor_id FROM {$t['visitors']} WHERE 1=1{$sql})", $params ); } /** A stored value as agents see it — the slug filters_from() takes back — or null. */ public static function slug( Saddle_Analytics_Dimension $d, $value ): ?string { if ( null === $value ) { return null; } return match ( $d->format ) { 'channel' => Saddle_Analytics_Channel::key( (int) $value ), 'device' => Saddle_Analytics_Device::keys()[ (int) $value ] ?? (string) $value, default => (string) $value, }; } }