PluginProbe
Parse.ly / 3.8.4
Parse.ly v3.8.4
3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 3.20.3 All 105 releases
wp-parsely / src / UI / class-admin-columns-parsely-stats.php

class-admin-columns-parsely-stats.php in Parse.ly 3.8.4, at src/UI/class-admin-columns-parsely-stats.php

438 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * UI: Class for adding `Parse.ly Stats` on admin columns
4 *
5 * @package Parsely
6 * @since 3.7.0
7 */
8
9 declare(strict_types=1);
10
11 namespace Parsely\UI;
12
13 use DateTime;
14 use Parsely\Parsely;
15 use Parsely\RemoteAPI\Remote_API_Base;
16 use Parsely\RemoteAPI\Analytics_Posts_API;
17 use WP_Screen;
18
19 use function Parsely\Utils\get_asset_info;
20 use function Parsely\Utils\get_formatted_number;
21 use function Parsely\Utils\get_formatted_time;
22
23 use const Parsely\PARSELY_FILE;
24 use const Parsely\Utils\DATE_UTC_FORMAT;
25
26 /**
27 * Class for adding `Parse.ly Stats` on admin columns.
28 *
29 * @since 3.7.0
30 *
31 * @phpstan-import-type Analytics_Post_API_Params from Analytics_Posts_API
32 * @phpstan-import-type Analytics_Post from Analytics_Posts_API
33 * @phpstan-import-type Remote_API_Error from Remote_API_Base
34 *
35 * @phpstan-type Parsely_Post_Stats array{
36 * page_views: string,
37 * visitors: string,
38 * avg_time: string,
39 * }
40 *
41 * @phpstan-type Parsely_Posts_Stats_Response array{
42 * data: array<string, Parsely_Post_Stats>|null,
43 * error: Remote_API_Error|null,
44 * }
45 */
46 class Admin_Columns_Parsely_Stats {
47 /**
48 * Instance of Parsely class.
49 *
50 * @var Parsely
51 */
52 private $parsely;
53
54 /**
55 * Instance of Parsely Analytics Posts API.
56 *
57 * @var Analytics_Posts_API
58 */
59 private $analytics_api;
60
61 /**
62 * Internal Variable.
63 *
64 * @var WP_Screen|null
65 */
66 private $current_screen;
67
68 /**
69 * Published times of visible posts.
70 *
71 * Analytics Endpoint don't support post_ids as param so to limit the API we will pass the
72 * min/max publish time.
73 *
74 * @var string[]
75 */
76 private $utc_published_times = array();
77
78 /**
79 * Constructor.
80 *
81 * @param Parsely $parsely Instance of Parsely class.
82 */
83 public function __construct( Parsely $parsely ) {
84 $this->parsely = $parsely;
85 }
86
87 /**
88 * Registers action and filter hook callbacks.
89 *
90 * @since 3.7.0
91 */
92 public function run(): void {
93 if ( ! $this->should_add_hooks() ) {
94 return;
95 }
96
97 add_action( 'current_screen', array( $this, 'set_current_screen' ) );
98 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_parsely_stats_styles' ) );
99 add_filter( 'manage_posts_columns', array( $this, 'add_parsely_stats_column_on_list_view' ) );
100 add_action( 'manage_posts_custom_column', array( $this, 'update_published_times_and_show_placeholder' ) );
101 add_filter( 'manage_pages_columns', array( $this, 'add_parsely_stats_column_on_list_view' ) );
102 add_action( 'manage_pages_custom_column', array( $this, 'update_published_times_and_show_placeholder' ) );
103 add_action( 'admin_footer', array( $this, 'enqueue_parsely_stats_script_with_data' ) );
104 }
105
106 /**
107 * Determines whether we should add hooks or not depending on plugin settings
108 * and user capabilities.
109 */
110 public function should_add_hooks(): bool {
111 if ( ! $this->parsely->site_id_is_set() || ! $this->parsely->api_secret_is_set() ) {
112 return false;
113 }
114
115 $this->analytics_api = new Analytics_Posts_API( $this->parsely );
116
117 // Don't add the column if the user is not allowed to make the API call.
118 if ( ! $this->analytics_api->is_user_allowed_to_make_api_call() ) {
119 return false;
120 }
121
122 return true;
123 }
124
125 /**
126 * Sets current screen property.
127 *
128 * @since 3.7.0
129 */
130 public function set_current_screen(): void {
131 $this->current_screen = get_current_screen();
132 }
133
134 /**
135 * Enqueues styles for Parse.ly Stats.
136 *
137 * @since 3.7.0
138 */
139 public function enqueue_parsely_stats_styles(): void {
140 if ( ! $this->is_tracked_as_post_type() ) {
141 return;
142 }
143
144 $admin_settings_asset = get_asset_info( 'build/admin-parsely-stats.asset.php' );
145 $built_assets_url = plugin_dir_url( PARSELY_FILE ) . 'build/';
146
147 wp_enqueue_style(
148 'admin-parsely-stats-styles',
149 $built_assets_url . 'admin-parsely-stats.css',
150 $admin_settings_asset['dependencies'],
151 $admin_settings_asset['version']
152 );
153 }
154
155 /**
156 * Adds `Parse.ly Stats` column on admin columns.
157 *
158 * @since 3.7.0
159 *
160 * @param array<string, string> $columns Columns array which contain keys and labels.
161 *
162 * @return array<string, string>
163 */
164 public function add_parsely_stats_column_on_list_view( array $columns ): array {
165 if ( $this->is_tracked_as_post_type() ) {
166 $columns['parsely-stats'] = __( 'Parse.ly Stats (7d)', 'wp-parsely' );
167 }
168
169 return $columns;
170 }
171
172 /**
173 * Updates our published_times list with current post info and show placeholder.
174 *
175 * Note: We don't have `the_posts` hook for hierarchical post types like pages so we are following this approach:
176 *
177 * 1. Get post publish times and show a placeholder while displaying rows on Admin List.
178 * 2. Make Parsely Analytics API call limited by publish dates inside `wp_footer` hook and pass the stats data to JS script.
179 * 3. Show data on each Admin Row using JS.
180 *
181 * @since 3.7.0
182 *
183 * @param string $column_name The name of the column to display.
184 */
185 public function update_published_times_and_show_placeholder( string $column_name ): void {
186 if ( 'parsely-stats' !== $column_name || ! $this->is_tracked_as_post_type() ) {
187 return;
188 }
189
190 global $post;
191
192 if ( 'publish' === $post->post_status && $this->parsely->api_secret_is_set() ) {
193 array_push( $this->utc_published_times, $post->post_date_gmt );
194 }
195
196 $stats_key = $this->get_unique_stats_key_of_current_post();
197 ?>
198 <div class="parsely-post-stats" data-stats-key="<?php echo esc_attr( $stats_key ); ?>">
199 <span class="parsely-post-stats-placeholder">...</span>
200 </div>
201 <?php
202 }
203
204 /**
205 * Enqueues script and pass Parse.ly Stats data for showing on Frontend.
206 *
207 * @since 3.7.0
208 */
209 public function enqueue_parsely_stats_script_with_data(): void {
210 if ( ! $this->is_tracked_as_post_type() ) {
211 return;
212 }
213
214 if ( $this->is_parsely_stats_column_hidden() ) {
215 return; // Avoid calling the API if column is hidden.
216 }
217
218 $parsely_stats_response = $this->get_parsely_stats_response( $this->analytics_api );
219
220 if ( null === $parsely_stats_response ) {
221 return;
222 }
223
224 $admin_settings_asset = get_asset_info( 'build/admin-parsely-stats.asset.php' );
225 $built_assets_url = plugin_dir_url( PARSELY_FILE ) . 'build/';
226
227 wp_enqueue_script(
228 'admin-parsely-stats-script',
229 $built_assets_url . 'admin-parsely-stats.js',
230 $admin_settings_asset['dependencies'],
231 $admin_settings_asset['version'],
232 true
233 );
234
235 wp_add_inline_script(
236 'admin-parsely-stats-script',
237 "window.wpParselyPostsStatsResponse = '" . wp_json_encode( $parsely_stats_response ) . "';",
238 'before'
239 );
240 }
241
242 /**
243 * Return TRUE if Parse.ly Stats column is hidden.
244 *
245 * @return bool
246 */
247 public function is_parsely_stats_column_hidden(): bool {
248 if ( ! isset( $this->current_screen ) ) {
249 return false;
250 }
251
252 return in_array( 'parsely-stats', get_hidden_columns( $this->current_screen ), true );
253 }
254
255 /**
256 * Calls Parse.ly Analytics API and get stats data.
257 *
258 * @since 3.7.0
259 *
260 * @param Analytics_Posts_API $analytics_api Instance of Analytics_Posts_API.
261 *
262 * @return Parsely_Posts_Stats_Response|null
263 */
264 public function get_parsely_stats_response( $analytics_api ) {
265 if ( ! $this->is_tracked_as_post_type() ) {
266 return null;
267 }
268
269 if ( ! $this->parsely->api_secret_is_set() ) {
270 return array(
271 'data' => null,
272 'error' => array(
273 'code' => 403,
274 'message' => __( 'Forbidden.', 'wp-parsely' ),
275 'htmlMessage' => '<p>' .
276 __(
277 'We are unable to retrieve data for Parse.ly Stats. Please contact <a href=\\"mailto:support@parsely.com\\">support@parsely.com</a> for help resolving this issue.',
278 'wp-parsely'
279 ) .
280 '</p>',
281 ),
282 );
283 }
284
285 $date_params = $this->get_publish_date_params_for_analytics_api();
286 if ( is_null( $date_params ) ) {
287 return null;
288 }
289
290 $response = $analytics_api->get_posts_analytics(
291 array(
292 'period_start' => Analytics_Posts_API::ANALYTICS_API_DAYS_LIMIT . 'd',
293 'pub_date_start' => $date_params['pub_date_start'] ?? '',
294 'pub_date_end' => $date_params['pub_date_end'] ?? '',
295 'limit' => Analytics_Posts_API::MAX_RECORDS_LIMIT,
296 'sort' => 'avg_engaged', // Note: API sends different stats on different sort options.
297 )
298 );
299
300 if ( is_wp_error( $response ) ) {
301 return array(
302 'data' => null,
303 'error' => array(
304 'code' => (int) $response->get_error_code(),
305 'message' => $response->get_error_message(),
306 'htmlMessage' => (
307 '<p>' .
308 esc_html__( 'Error while getting data for Parse.ly Stats.', 'wp-parsely' ) . '<br/>' .
309 esc_html__( 'Detail: ', 'wp-parsely' ) . esc_html( "({$response->get_error_code()}) {$response->get_error_message()}" ) .
310 '</p>'
311 ),
312 ),
313 );
314 }
315
316 if ( null === $response ) {
317 return array(
318 'data' => array(),
319 'error' => null,
320 );
321 }
322
323 /**
324 * Variable.
325 *
326 * @var array<string, Parsely_Post_Stats>
327 */
328 $parsely_stats_map = array();
329
330 foreach ( $response as $post_analytics ) {
331 $key = $this->get_unique_stats_key_from_analytics( $post_analytics );
332
333 if ( '' === $key || ! isset( $post_analytics['metrics'] ) ) {
334 continue;
335 }
336
337 $metrics = $post_analytics['metrics'];
338 $views = isset( $metrics['views'] ) ? $metrics['views'] : 0;
339 $visitors = isset( $metrics['visitors'] ) ? $metrics['visitors'] : 0;
340 $engaged_seconds = isset( $metrics['avg_engaged'] ) ? round( $metrics['avg_engaged'], 2 ) * 60 : 0;
341
342 /**
343 * Variable.
344 *
345 * @var Parsely_Post_Stats
346 */
347 $stats = array(
348 'page_views' => get_formatted_number( $views ) . ' ' . _n( 'page view', 'page views', $views, 'wp-parsely' ),
349 'visitors' => get_formatted_number( $visitors ) . ' ' . _n( 'visitor', 'visitors', $visitors, 'wp-parsely' ),
350 'avg_time' => get_formatted_time( $engaged_seconds ) . ' ' . __( 'avg time', 'wp-parsely' ),
351 );
352
353 $parsely_stats_map[ $key ] = $stats;
354 }
355
356 return array(
357 'data' => $parsely_stats_map,
358 'error' => null,
359 );
360 }
361
362 /**
363 * Gets publish date params which we can used as params in analytics API.
364 *
365 * Uses published times list to get the min/max dates which we can use to limit the API.
366 *
367 * @since 3.7.0
368 *
369 * @return Analytics_Post_API_Params|null
370 */
371 private function get_publish_date_params_for_analytics_api() {
372 $published_times = $this->utc_published_times;
373
374 if ( count( $published_times ) === 0 ) {
375 return null;
376 }
377
378 return array(
379 'pub_date_start' => ( new DateTime( min( $published_times ) ) )->format( DATE_UTC_FORMAT ),
380 'pub_date_end' => ( new DateTime( max( $published_times ) ) )->format( DATE_UTC_FORMAT ),
381 );
382 }
383
384 /**
385 * Gets unique key which we can use for Parse.ly stats map.
386 *
387 * Needed because Parse.ly Analytics API doesn't have anything unique through which we can identify the post.
388 *
389 * @since 3.7.0
390 *
391 * @param Analytics_Post $analytics_post Post analytics obj returned from Parse.ly API.
392 *
393 * @return string
394 */
395 private function get_unique_stats_key_from_analytics( $analytics_post ): string {
396 if ( ! isset( $analytics_post['url'] ) ) {
397 return '';
398 }
399
400 return (string) wp_parse_url( $analytics_post['url'], PHP_URL_PATH );
401 }
402
403 /**
404 * Gets unique key from currently set post which we can use to get data from Parse.ly stats map.
405 *
406 * @since 3.7.0
407 *
408 * @return string
409 */
410 private function get_unique_stats_key_of_current_post(): string {
411 return (string) wp_parse_url( (string) get_permalink(), PHP_URL_PATH );
412 }
413
414 /**
415 * Returns TRUE if the current screen is the list screen and we are tracking it as `Post` in plugin settings.
416 *
417 * @since 3.7.0
418 *
419 * @return bool
420 */
421 private function is_tracked_as_post_type(): bool {
422 if ( is_null( $this->current_screen ) ) {
423 return false;
424 }
425
426 // Analytics API doesn't include Non-Post data, so we can only show stats on tracked post types.
427 $track_post_types = $this->parsely->get_options()['track_post_types'];
428
429 foreach ( $track_post_types as $track_post_type ) {
430 if ( 'edit' === $this->current_screen->base && $track_post_type === $this->current_screen->post_type ) {
431 return true;
432 }
433 }
434
435 return false;
436 }
437 }
438