PluginProbe
Parse.ly / 3.24.2
Parse.ly v3.24.2
3.24.3 3.24.2 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 All 107 releases
wp-parsely / src / content-helper / post-list-stats / class-post-list-stats.php

class-post-list-stats.php in Parse.ly 3.24.2, at src/content-helper/post-list-stats/class-post-list-stats.php

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