PluginProbe
Parse.ly / 3.24.2
Parse.ly v3.24.2
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 3.20.2 All 106 releases
wp-parsely / src / rest-api / stats / class-endpoint-post.php

class-endpoint-post.php in Parse.ly 3.24.2, at src/rest-api/stats/class-endpoint-post.php

517 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Stats API Endpoint: Post
4 *
5 * @package Parsely
6 * @since 3.17.0
7 */
8
9 declare(strict_types=1);
10
11 namespace Parsely\REST_API\Stats;
12
13 use Parsely\REST_API\Base_Endpoint;
14 use Parsely\REST_API\Use_Post_ID_Parameter_Trait;
15 use Parsely\Services\Content_API\Content_API_Service;
16 use Parsely\Utils\Utils;
17 use stdClass;
18 use WP_Error;
19 use WP_Post;
20 use WP_REST_Request;
21 use WP_REST_Response;
22
23 /**
24 * The Stats API Post endpoint.
25 *
26 * Provides an endpoint for retrieving post details, referrers, and related
27 * posts for a given post.
28 *
29 * @since 3.17.0
30 *
31 * @phpstan-type Referrer_Data array{
32 * metrics: array{
33 * referrers_views?: int
34 * },
35 * type?: string,
36 * name?: string
37 * }
38 *
39 * @phpstan-type Referrer_Type_Data array{
40 * views: string,
41 * viewsPercentage: string
42 * }
43 *
44 * @phpstan-type Referrers_Data_Item array{
45 * views: string,
46 * viewsPercentage: string,
47 * datasetViewsPercentage: string
48 * }
49 */
50 class Endpoint_Post extends Base_Endpoint {
51 use Use_Post_ID_Parameter_Trait;
52 use Post_Data_Trait;
53 use Related_Posts_Trait;
54
55 /**
56 * The Parse.ly Content API service.
57 *
58 * @since 3.17.0
59 *
60 * @var Content_API_Service $content_api
61 */
62 public $content_api;
63
64 /**
65 * The total views of the post.
66 *
67 * @since 3.17.0
68 *
69 * @var int
70 */
71 private $total_views = 0;
72
73 /**
74 * Constructor.
75 *
76 * @since 3.17.0
77 *
78 * @param Stats_Controller $controller The controller.
79 */
80 public function __construct( Stats_Controller $controller ) {
81 parent::__construct( $controller );
82 $this->content_api = $this->parsely->get_content_api();
83 }
84
85 /**
86 * Returns the endpoint's name.
87 *
88 * @since 3.17.0
89 *
90 * @return string The endpoint's name.
91 */
92 public static function get_endpoint_name(): string {
93 return 'post';
94 }
95
96 /**
97 * Registers the routes for the endpoint.
98 *
99 * @since 3.17.0
100 */
101 public function register_routes(): void {
102 /**
103 * GET /stats/post/{post_id}/details
104 * Returns the analytics details of a post.
105 */
106 $this->register_rest_route_with_post_id(
107 '/details',
108 array( 'GET' ),
109 array( $this, 'get_post_details' ),
110 array_merge(
111 array(
112 'period_start' => array(
113 'description' => __( 'The start of the period.', 'wp-parsely' ),
114 'type' => 'string',
115 'required' => false,
116 ),
117 'period_end' => array(
118 'description' => __( 'The end of the period.', 'wp-parsely' ),
119 'type' => 'string',
120 'required' => false,
121 ),
122 ),
123 $this->get_itm_source_param_args()
124 )
125 );
126
127 /**
128 * GET /stats/post/{post_id}/referrers
129 * Returns the referrers of a post.
130 */
131 $this->register_rest_route_with_post_id(
132 '/referrers',
133 array( 'GET' ),
134 array( $this, 'get_post_referrers' ),
135 array(
136 'period_start' => array(
137 'description' => __( 'The start of the period.', 'wp-parsely' ),
138 'type' => 'string',
139 'required' => false,
140 ),
141 'period_end' => array(
142 'description' => __( 'The end of the period.', 'wp-parsely' ),
143 'type' => 'string',
144 'required' => false,
145 ),
146 'total_views' => array(
147 'description' => __( 'The total views of the post.', 'wp-parsely' ),
148 'type' => 'string',
149 'required' => false,
150 'default' => '0',
151 ),
152 )
153 );
154
155 /**
156 * GET /stats/post/{post_id}/related
157 * Returns the related posts of a post.
158 */
159 $this->register_rest_route_with_post_id(
160 '/related',
161 array( 'GET' ),
162 array( $this, 'get_related_posts' ),
163 $this->get_related_posts_param_args()
164 );
165 }
166
167 /**
168 * API Endpoint: GET /stats/post/{post_id}/details
169 *
170 * Gets the details of a post.
171 *
172 * @since 3.17.0
173 *
174 * @param WP_REST_Request $request The request object.
175 * @return WP_REST_Response|WP_Error The response object.
176 */
177 public function get_post_details( WP_REST_Request $request ) {
178 $post = $this->get_request_post( $request );
179
180 if ( ! $post instanceof WP_Post ) {
181 return new WP_Error(
182 'invalid_post',
183 __( 'Invalid post.', 'wp-parsely' ),
184 array( 'status' => 404 )
185 );
186 }
187
188 $permalink = get_permalink( $post->ID );
189
190 if ( ! is_string( $permalink ) ) {
191 return new WP_Error(
192 'invalid_post',
193 __( 'Invalid post.', 'wp-parsely' ),
194 array( 'status' => 404 )
195 );
196 }
197
198 // Set the itm_source parameter.
199 $this->set_itm_source_from_request( $request );
200
201 // Get the data from the API.
202 $analytics_request = $this->content_api->get_post_details(
203 $permalink,
204 $request->get_param( 'period_start' ),
205 $request->get_param( 'period_end' )
206 );
207
208 if ( is_wp_error( $analytics_request ) ) {
209 return $analytics_request;
210 }
211
212 $post_data = array();
213
214 /**
215 * The analytics data object.
216 *
217 * @var array<string,array<mixed>> $analytics_request
218 */
219 foreach ( $analytics_request as $data ) {
220 $post_data[] = $this->extract_post_data( $data );
221 }
222
223 $response_data = array( 'data' => $post_data );
224
225 return new WP_REST_Response( $response_data, 200 );
226 }
227
228 /**
229 * API Endpoint: GET /stats/post/{post_id}/referrers
230 *
231 * Gets the referrers of a post.
232 *
233 * @since 3.17.0
234 *
235 * @param WP_REST_Request $request The request object.
236 * @return WP_REST_Response|WP_Error The response object.
237 */
238 public function get_post_referrers( WP_REST_Request $request ) {
239 $post = $this->get_request_post( $request );
240
241 if ( ! $post instanceof WP_Post ) {
242 return new WP_Error(
243 'invalid_post',
244 __( 'Invalid post.', 'wp-parsely' ),
245 array( 'status' => 404 )
246 );
247 }
248
249 $permalink = get_permalink( $post->ID );
250
251 if ( ! is_string( $permalink ) ) {
252 return new WP_Error(
253 'invalid_post',
254 __( 'Invalid post.', 'wp-parsely' ),
255 array( 'status' => 404 )
256 );
257 }
258
259 // Set the itm_source parameter.
260 $this->set_itm_source_from_request( $request );
261
262 // Get the total views.
263 $total_views = $request->get_param( 'total_views' ) ?? 0;
264
265 if ( is_string( $total_views ) ) {
266 $total_views = Utils::convert_to_positive_integer( $total_views );
267 }
268
269 $this->total_views = $total_views;
270
271 // Get the data from the API.
272 $analytics_request = $this->content_api->get_post_referrers(
273 $permalink,
274 $request->get_param( 'period_start' ),
275 $request->get_param( 'period_end' )
276 );
277
278 if ( is_wp_error( $analytics_request ) ) {
279 return $analytics_request;
280 }
281
282 /**
283 * The analytics data object.
284 *
285 * @var array<Referrer_Data> $analytics_request
286 */
287 $referrers_types = $this->generate_referrer_types_data( $analytics_request );
288 $direct_views = Utils::convert_to_positive_integer(
289 $referrers_types['direct']['views'] ?? '0'
290 );
291 $referrers_top = $this->generate_referrers_data( 5, $analytics_request, $direct_views );
292
293 $response_data = array(
294 'data' => array(
295 'top' => $referrers_top,
296 'types' => $referrers_types,
297 ),
298 );
299
300 return new WP_REST_Response( $response_data, 200 );
301 }
302
303 /**
304 * API Endpoint: GET /stats/post/{post_id}/related
305 *
306 * Gets the related posts of a post.
307 *
308 * @since 3.17.0
309 *
310 * @param WP_REST_Request $request The request object.
311 * @return WP_REST_Response|WP_Error The response data.
312 */
313 public function get_related_posts( WP_REST_Request $request ) {
314 $post = $this->get_request_post( $request );
315
316 if ( ! $post instanceof WP_Post ) {
317 return new WP_Error(
318 'invalid_post',
319 __( 'Invalid post.', 'wp-parsely' ),
320 array( 'status' => 404 )
321 );
322 }
323
324 $permalink = get_permalink( $post->ID );
325
326 if ( ! is_string( $permalink ) ) {
327 return new WP_Error(
328 'invalid_post',
329 __( 'Invalid post.', 'wp-parsely' ),
330 array( 'status' => 404 )
331 );
332 }
333
334 $related_posts = $this->get_related_posts_of_url( $request, $permalink );
335
336 if ( is_wp_error( $related_posts ) ) {
337 return $related_posts;
338 }
339
340 return new WP_REST_Response( array( 'data' => $related_posts ), 200 );
341 }
342
343 /**
344 * Generates the referrer types data.
345 *
346 * Referrer types are:
347 * - `social`: Views coming from social media.
348 * - `search`: Views coming from search engines.
349 * - `other`: Views coming from other referrers, like external websites.
350 * - `internal`: Views coming from linking pages of the same website.
351 *
352 * Returned object properties:
353 * - `views`: The number of views.
354 * - `viewsPercentage`: The number of views as a percentage, compared to the
355 * total views of all referrer types.
356 *
357 * @since 3.6.0
358 * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class.
359 *
360 * @param array<Referrer_Data> $response The response received by the proxy.
361 * @return array<string, Referrer_Type_Data> The generated data.
362 */
363 private function generate_referrer_types_data( array $response ): array {
364 $result = array();
365 $total_referrer_views = 0; // Views from all referrer types combined.
366
367 // Set referrer type order as it is displayed in the Parse.ly dashboard.
368 $referrer_type_keys = array( 'social', 'search', 'other', 'internal', 'direct' );
369 foreach ( $referrer_type_keys as $key ) {
370 $result[ $key ] = array( 'views' => 0 );
371 }
372
373 // Set views and views totals.
374 foreach ( $response as $referrer_data ) {
375 /**
376 * @var int $current_views
377 */
378 $current_views = $referrer_data['metrics']['referrers_views'] ?? 0;
379 $total_referrer_views += $current_views;
380
381 /**
382 * @var string $current_key
383 */
384 $current_key = $referrer_data['type'] ?? '';
385 if ( '' !== $current_key ) {
386 if ( ! isset( $result[ $current_key ]['views'] ) ) {
387 $result[ $current_key ] = array( 'views' => 0 );
388 }
389
390 $result[ $current_key ]['views'] += $current_views;
391 }
392 }
393
394 // Add direct and total views to the object.
395 $result['direct']['views'] = $this->total_views - $total_referrer_views;
396 $result['totals'] = array( 'views' => $this->total_views );
397
398 // Remove referrer types without views.
399 foreach ( $referrer_type_keys as $key ) {
400 if ( 0 === $result[ $key ]['views'] ) {
401 unset( $result[ $key ] );
402 }
403 }
404
405 // Set percentage values and format numbers.
406 foreach ( $result as $key => $value ) {
407 // Set and format percentage values.
408 $result[ $key ]['viewsPercentage'] = $this->get_i18n_percentage(
409 absint( $value['views'] ),
410 $this->total_views
411 );
412
413 // Format views values.
414 $result[ $key ]['views'] = number_format_i18n( $result[ $key ]['views'] );
415 }
416
417 return $result;
418 }
419
420 /**
421 * Generates the top referrers data.
422 *
423 * Returned object properties:
424 * - `views`: The number of views.
425 * - `viewsPercentage`: The number of views as a percentage, compared to the
426 * total views of all referrer types.
427 * - `datasetViewsPercentage`: The number of views as a percentage, compared
428 * to the total views of the current dataset.
429 *
430 * @since 3.6.0
431 * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class.
432 *
433 * @param int $limit The limit of returned referrers.
434 * @param array<Referrer_Data> $response The response received by the proxy.
435 * @param int $direct_views The count of direct views.
436 * @return array<string, Referrers_Data_Item> The generated data.
437 */
438 private function generate_referrers_data(
439 int $limit,
440 array $response,
441 int $direct_views
442 ): array {
443 $temp_views = array();
444 $totals = 0;
445 $referrer_count = count( $response );
446
447 // Set views and views totals.
448 $loop_count = $referrer_count > $limit ? $limit : $referrer_count;
449 for ( $i = 0; $i < $loop_count; $i++ ) {
450 $data = $response[ $i ];
451
452 /**
453 * @var int $referrer_views
454 */
455 $referrer_views = $data['metrics']['referrers_views'] ?? 0;
456 $totals += $referrer_views;
457 if ( isset( $data['name'] ) ) {
458 $temp_views[ $data['name'] ] = $referrer_views;
459 }
460 }
461
462 // If applicable, add the direct views.
463 if ( isset( $referrer_views ) && $direct_views >= $referrer_views ) {
464 $temp_views['direct'] = $direct_views;
465 $totals += $direct_views;
466 arsort( $temp_views );
467 if ( count( $temp_views ) > $limit ) {
468 $totals -= array_pop( $temp_views );
469 }
470 }
471
472 // Convert temporary array to result object and add totals.
473 $result = array();
474 foreach ( $temp_views as $key => $value ) {
475 $result[ $key ] = array( 'views' => $value );
476 }
477 $result['totals'] = array( 'views' => $totals );
478
479 // Set percentage values and format numbers.
480 foreach ( $result as $key => $value ) {
481 // Percentage against all referrer views, even those not included
482 // in the dataset due to the $limit argument.
483 $result[ $key ]['viewsPercentage'] = $this
484 ->get_i18n_percentage( absint( $value['views'] ), $this->total_views );
485
486 // Percentage against the current dataset that is limited due to the
487 // $limit argument.
488 $result[ $key ]['datasetViewsPercentage'] = $this
489 ->get_i18n_percentage( absint( $value['views'] ), $totals );
490
491 // Format views values.
492 $result[ $key ]['views'] = number_format_i18n( $result[ $key ]['views'] );
493 }
494
495 return $result;
496 }
497
498 /**
499 * Returns the passed number compared to the passed total, in an
500 * internationalized percentage format.
501 *
502 * @since 3.6.0
503 * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class.
504 *
505 * @param int $number The number to be calculated as a percentage.
506 * @param int $total The total number to compare against.
507 * @return string|false The internationalized percentage or false on error.
508 */
509 private function get_i18n_percentage( int $number, int $total ) {
510 if ( 0 === $total ) {
511 return false;
512 }
513
514 return number_format_i18n( $number / $total * 100, 2 );
515 }
516 }
517