PluginProbe
Mailchimp List Subscribe Form / trunk
Mailchimp List Subscribe Form vtrunk
1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 All 55 releases
mailchimp / includes / class-mailchimp-form-performance.php

class-mailchimp-form-performance.php in Mailchimp List Subscribe Form trunk, at includes/class-mailchimp-form-performance.php

259 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Form performance (list-level submissions over time) data provider for the
4 * Analytics page.
5 *
6 * @package Mailchimp
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Class Mailchimp_Form_Performance
15 */
16 class Mailchimp_Form_Performance {
17
18 use Mailchimp_Analytics_Bucketing;
19
20 /**
21 * Transient key prefix for the cached total subscriber count.
22 */
23 const SUBSCRIBERS_CACHE_PREFIX = 'mailchimp_sf_total_subscribers_';
24
25 /**
26 * Transient TTL for the cached total subscriber count.
27 */
28 const SUBSCRIBERS_CACHE_TTL = 15 * MINUTE_IN_SECONDS;
29
30 /**
31 * Register hooks.
32 *
33 * @return void
34 */
35 public function init() {
36 add_action( 'wp_ajax_mailchimp_sf_get_form_performance', array( $this, 'handle_get' ) );
37 }
38
39 /**
40 * AJAX handler for `mailchimp_sf_get_form_performance`.
41 *
42 * @return void
43 */
44 public function handle_get() {
45 if ( ! current_user_can( MCSF_CAP_THRESHOLD ) ) {
46 wp_send_json_error(
47 array(
48 'message' => esc_html__( 'Unauthorized.', 'mailchimp' ),
49 'error_code' => 'unauthorized',
50 ),
51 403
52 );
53 }
54
55 check_ajax_referer( 'mailchimp_sf_analytics_admin_nonce', 'nonce' );
56
57 $list_id = isset( $_POST['list_id'] ) ? sanitize_text_field( wp_unslash( $_POST['list_id'] ) ) : '';
58 $date_from = isset( $_POST['date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['date_from'] ) ) : '';
59 $date_to = isset( $_POST['date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['date_to'] ) ) : '';
60
61 if ( empty( $list_id ) ) {
62 wp_send_json_error(
63 array(
64 'message' => esc_html__( 'Please select a list.', 'mailchimp' ),
65 'error_code' => 'invalid_request',
66 ),
67 400
68 );
69 }
70
71 if ( ! $this->is_valid_date( $date_from ) || ! $this->is_valid_date( $date_to ) ) {
72 wp_send_json_error(
73 array(
74 'message' => esc_html__( 'Invalid date range.', 'mailchimp' ),
75 'error_code' => 'invalid_request',
76 ),
77 400
78 );
79 }
80
81 if ( strtotime( $date_from ) > strtotime( $date_to ) ) {
82 wp_send_json_error(
83 array(
84 'message' => esc_html__( 'Start date must be before end date.', 'mailchimp' ),
85 'error_code' => 'invalid_request',
86 ),
87 400
88 );
89 }
90
91 $rows = $this->fetch_rows( $list_id, $date_from, $date_to );
92
93 if ( null === $rows ) {
94 wp_send_json_error(
95 array(
96 'message' => esc_html__( 'An internal error occurred while loading analytics. Please contact support if this persists.', 'mailchimp' ),
97 'error_code' => 'db_error',
98 ),
99 500
100 );
101 }
102
103 $response = $this->aggregate( $rows, $date_from, $date_to );
104
105 $response['total_subscribers'] = $this->fetch_total_subscribers( $list_id );
106
107 wp_send_json_success( $response );
108 }
109
110 /**
111 * Fetch (and cache) the current total subscriber count for a list.
112 *
113 * @param string $list_id List ID.
114 * @return int|null
115 */
116 public function fetch_total_subscribers( string $list_id ): ?int {
117 $cache_key = self::SUBSCRIBERS_CACHE_PREFIX . md5( $list_id );
118 $cached = get_transient( $cache_key );
119
120 if ( false !== $cached ) {
121 return (int) $cached;
122 }
123
124 $api = mailchimp_sf_get_api();
125 if ( ! $api ) {
126 return null;
127 }
128
129 $response = $api->get(
130 'lists/' . rawurlencode( $list_id ),
131 1,
132 array( 'stats.member_count' )
133 );
134
135 if ( is_wp_error( $response ) || ! is_array( $response ) ) {
136 return null;
137 }
138
139 if ( ! isset( $response['stats']['member_count'] ) ) {
140 return null;
141 }
142
143 $count = (int) $response['stats']['member_count'];
144 set_transient( $cache_key, $count, self::SUBSCRIBERS_CACHE_TTL );
145
146 return $count;
147 }
148
149 /**
150 * Fetch daily totals from the analytics table for the selected list/range.
151 *
152 * @param string $list_id List ID.
153 * @param string $date_from `Y-m-d`.
154 * @param string $date_to `Y-m-d`.
155 * @return array|null Rows of `{ event_date, views, submissions }`, or null on DB error.
156 */
157 public function fetch_rows( string $list_id, string $date_from, string $date_to ): ?array {
158 global $wpdb;
159
160 $table_name = Mailchimp_Analytics_Data::get_table_name();
161
162 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
163 $results = $wpdb->get_results(
164 $wpdb->prepare(
165 "SELECT event_date, SUM(views) AS views, SUM(submissions) AS submissions
166 FROM {$table_name}
167 WHERE list_id = %s AND event_date BETWEEN %s AND %s
168 GROUP BY event_date
169 ORDER BY event_date ASC",
170 $list_id,
171 $date_from,
172 $date_to
173 ),
174 ARRAY_A
175 );
176 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
177
178 if ( null === $results || ! empty( $wpdb->last_error ) ) {
179 return null;
180 }
181
182 return is_array( $results ) ? $results : array();
183 }
184
185 /**
186 * Bucket daily rows into the chart's interval, back-filling missing days
187 * with zeros so every bucket on the x-axis has a value
188 *
189 * @param array $rows Rows from `fetch_rows()`.
190 * @param string $date_from `Y-m-d`.
191 * @param string $date_to `Y-m-d`.
192 * @return array Response payload.
193 */
194 public function aggregate( array $rows, string $date_from, string $date_to ): array {
195 $tz = wp_timezone();
196 $from_dt = new DateTimeImmutable( $date_from, $tz );
197 $to_dt = new DateTimeImmutable( $date_to, $tz );
198
199 $requested_days = (int) $from_dt->diff( $to_dt )->days + 1;
200 $interval = $this->get_interval( $requested_days );
201
202 // Build the complete ordered set of bucket keys covering the range.
203 $buckets = array();
204 $one_day = new DateInterval( 'P1D' );
205 $cursor = $from_dt;
206 while ( $cursor <= $to_dt ) {
207 $date = $cursor->format( 'Y-m-d' );
208 $key = $this->get_bucket_key( $date, $interval, $tz );
209 if ( ! isset( $buckets[ $key ] ) ) {
210 $buckets[ $key ] = array(
211 'key' => $key,
212 'label' => $this->get_bucket_label( $date, $interval, $tz ),
213 'views' => 0,
214 'submissions' => 0,
215 );
216 }
217 $cursor = $cursor->add( $one_day );
218 }
219 ksort( $buckets );
220
221 $total_views = 0;
222 $total_submissions = 0;
223
224 foreach ( $rows as $row ) {
225 $date = isset( $row['event_date'] ) ? (string) $row['event_date'] : '';
226 $views = isset( $row['views'] ) ? (int) $row['views'] : 0;
227 $submissions = isset( $row['submissions'] ) ? (int) $row['submissions'] : 0;
228
229 if ( '' === $date ) {
230 continue;
231 }
232
233 $key = $this->get_bucket_key( $date, $interval, $tz );
234 if ( ! isset( $buckets[ $key ] ) ) {
235 continue;
236 }
237
238 $buckets[ $key ]['views'] += $views;
239 $buckets[ $key ]['submissions'] += $submissions;
240 $total_views += $views;
241 $total_submissions += $submissions;
242 }
243
244 $data = array();
245 foreach ( $buckets as $bucket ) {
246 $bucket['conversion_rate'] = $this->conversion_rate( $bucket['submissions'], $bucket['views'] );
247 $data[] = $bucket;
248 }
249
250 return array(
251 'interval' => $interval,
252 'data' => $data,
253 'total_views' => $total_views,
254 'total_submissions' => $total_submissions,
255 'total_conversion_rate' => $this->conversion_rate( $total_submissions, $total_views ),
256 );
257 }
258 }
259