Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add standalone jw player adapter and api logic #10

Merged
merged 23 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3dde99b
add standalone jw player adapter and api logic
manewc Sep 20, 2024
8c57df8
correct and update plugin point version
manewc Sep 20, 2024
31c3aeb
remove unnecessary origin modified date
manewc Sep 23, 2024
1bc6198
assign allowed null value to type declaration
manewc Sep 23, 2024
dcd3f22
refactor request arg retrieval for api requests
manewc Sep 23, 2024
6e57ffb
refactor logic to make the api calls more abstract
manewc Sep 23, 2024
5b66892
consolidate repeated logic and moved to a parent class
manewc Sep 24, 2024
8d24cc1
address phpcs sniffs
manewc Sep 26, 2024
1cfbed8
address phpstan errors
manewc Sep 26, 2024
4a728e9
check for function existence. PHPStan not checking the parent class m…
manewc Sep 26, 2024
400d6c6
strict parameter definition to conform to the wp_remote_get parameter…
manewc Sep 26, 2024
91d61c9
correct formatting
manewc Sep 26, 2024
55c36fc
Testing CI
srtfisher Sep 26, 2024
97b5f7b
Enable debug mode
srtfisher Sep 26, 2024
910760d
Fix action
srtfisher Sep 26, 2024
b15d7a2
Remove debug mode
srtfisher Sep 26, 2024
0bb364a
add unit test for the JW Player Adapter integration
manewc Sep 27, 2024
020ded3
provide base README documentation outlining class methods
manewc Oct 1, 2024
f54af24
implement php constructor property promotion
manewc Oct 7, 2024
e7e8ce0
ensure get_last_modified_date method is applied within adapter
manewc Oct 7, 2024
3ad4dd5
restrict setting last modified date to a DateTimeImmutable object
manewc Oct 7, 2024
7397082
ensure last modified date is a DateTimeImmutable instance, encapsulat…
manewc Oct 7, 2024
31a0135
update unit test to remove dependency class param
manewc Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 3 additions & 20 deletions src/adapters/class-jw-player-7-for-wp.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,14 @@
namespace Alley\WP\WP_Video_Sync\Adapters;

use Alley\WP\WP_Video_Sync\Interfaces\Adapter;
use Alley\WP\WP_Video_Sync\Last_Modified_Date;
use DateTimeImmutable;
use stdClass;

/**
* JW Player 7 for WP Adapter. Supports both the free and premium versions of the plugin.
*/
class JW_Player_7_For_WP implements Adapter {
/**
* The date of the last modification to the last batch of videos.
*
* @var ?DateTimeImmutable
*/
private ?DateTimeImmutable $last_modified_date;

/**
* Fetches the date of the last modification to the last batch of videos.
*
* @return ?DateTimeImmutable
*/
public function get_last_modified_date(): ?DateTimeImmutable {
return $this->last_modified_date;
}
class JW_Player_7_For_WP extends Last_Modified_Date implements Adapter {

/**
* Fetches videos from JW Player that were modified after the provided DateTime.
Expand All @@ -54,10 +40,7 @@ public function get_videos( DateTimeImmutable $updated_after, int $batch_size ):

// Attempt to set the last modified date.
if ( isset( $videos[ count( $videos ) - 1 ]->last_modified ) ) {
$last_modified_date = DateTimeImmutable::createFromFormat( DATE_W3C, $videos[ count( $videos ) - 1 ]->last_modified );
if ( $last_modified_date instanceof DateTimeImmutable ) {
$this->last_modified_date = $last_modified_date;
}
$this->set_last_modified_date( $videos[ count( $videos ) - 1 ]->last_modified );
}

return $videos;
Expand Down
76 changes: 76 additions & 0 deletions src/adapters/class-jw-player.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
/**
* WP Video Sync: JW Player Adapter.
*
* @package wp-video-sync
*/

namespace Alley\WP\WP_Video_Sync\Adapters;

use Alley\WP\WP_Video_Sync\API\JW_Player_API;
use Alley\WP\WP_Video_Sync\API\Request;
use Alley\WP\WP_Video_Sync\Interfaces\Adapter;
use Alley\WP\WP_Video_Sync\Last_Modified_Date;
use DateTimeImmutable;

/**
* JW Player Adapter.
*/
class JW_Player extends Last_Modified_Date implements Adapter {

/**
* The JW Player API.
*
* @var JW_Player_API
*/
public JW_Player_API $jw_player_api;

/**
* Constructor.
*
* @param JW_Player_API $api Instance of the JW Player API object.
*/
public function __construct( JW_Player_API $api ) {
$this->jw_player_api = $api;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a modern PHP constructor:

Suggested change
/**
* The JW Player API.
*
* @var JW_Player_API
*/
public JW_Player_API $jw_player_api;
/**
* Constructor.
*
* @param JW_Player_API $api Instance of the JW Player API object.
*/
public function __construct( JW_Player_API $api ) {
$this->jw_player_api = $api;
}
/**
* Constructor.
*
* @param JW_Player_API $jw_player_api Instance of the JW Player API object.
*/
public function __construct( public readonly JW_Player_API $jw_player_api ) {}


/**
* Fetches videos from JW Player that were modified after the provided DateTime.
*
* @param DateTimeImmutable $updated_after Return videos modified after this date.
* @param int $batch_size The number of videos to fetch in each batch.
*
* @return array<mixed> An array of video data objects.
*/
public function get_videos( DateTimeImmutable $updated_after, int $batch_size ): array {
// Set the request URL based on the arguments.
$this->jw_player_api->set_request_url(
$updated_after->format( 'Y-m-d' ),
$batch_size
);

// Perform the request.
$videos = ( new Request( $this->jw_player_api ) )->get();

// Check for an API error.
if ( ! empty( $videos['error'] ) ) {
return [];
}

// Validate the media property.
if ( ! is_array( $videos['media'] ) ) {
return [];
}

// Attempt to set the last modified date.
if (
! empty( $videos['media'][ count( $videos['media'] ) - 1 ] )
&& isset( $videos['media'][ count( $videos['media'] ) - 1 ]->last_modified )
) {
$this->set_last_modified_date( $videos['media'][ count( $videos ) - 1 ]->last_modified );
}

// Return the videos.
return ! empty( $videos['media'] ) ? $videos['media'] : [];
}
}
127 changes: 127 additions & 0 deletions src/api/class-jw-player-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php
/**
* WP Video Sync: JW Player API integration.
*
* @package wp-video-sync
*/

namespace Alley\WP\WP_Video_Sync\API;

use Alley\WP\WP_Video_Sync\Interfaces\API_Requester;

/**
* JW Player API.
*/
class JW_Player_API implements API_Requester {

/**
* The API URL.
*
* @var string
*/
public string $api_url = 'https://api.jwplayer.com/v2/sites';

/**
* The API public key.
*
* @var string
*/
public string $api_key;

/**
* The API v2 secret key.
*
* @var string
*/
public string $api_secret;

/**
* The request URL.
*
* @var string
*/
public string $request_url;

/**
* Constructor.
*
* @param string $api_key The API key.
* @param string $api_secret The API secret.
*/
public function __construct( string $api_key, string $api_secret ) {
$this->api_key = $api_key;
$this->api_secret = $api_secret;
}

/**
* Generate the request URL.
*
* @param string $last_modified_date The date of the last modification to the last batch of videos.
* @param int $batch_size The number of videos to fetch in each batch.
*/
public function set_request_url( string $last_modified_date, int $batch_size ): void {
$request_url = $this->api_url . '/' . $this->api_key . '/media/';

$this->request_url = add_query_arg(
[
'q' => 'last_modified:[' . $last_modified_date . ' TO *]',
'page' => 1,
'sort' => 'last_modified:asc',
'page_length' => $batch_size,
],
$request_url
);
}

/**
* Get the request URL.
*
* @return string
*/
public function get_request_url(): string {
return $this->request_url;
}

/**
* Get the request arguments.
*
* @return array<string, array<string, string>>
*/
public function get_request_args(): array {
return [
'headers' => [
'Authorization' => 'Bearer ' . $this->api_secret,
'Content-Type' => 'application/json',
],
];
}

/**
* Parse the API error response.
*
* @param array<mixed> $response_object The API response object.
*
* @return array<string, string>
*/
public function parse_error( array $response_object ): array {
return ! empty( $response_object['errors'] )
&& is_array( $response_object['errors'] )
&& isset( $response_object['errors'][0]->description )
? [ 'error' => $response_object['errors'][0]->description ]
: [];
}

/**
* Parse the API successful response.
*
* @param array<mixed> $response_object The API response object.
*
* @return array<string, mixed>
*/
public function parse_success( array $response_object ): array {
return ! empty( $response_object['media'] )
&& is_array( $response_object['media'] )
? [ 'media' => $response_object['media'] ]
: [];
}
}
151 changes: 151 additions & 0 deletions src/api/class-request.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php
/**
* WP Video Sync: API request integration.
*
* @package wp-video-sync
*/

namespace Alley\WP\WP_Video_Sync\API;

use Alley\WP\WP_Video_Sync\Interfaces\API_Requester;

/**
* Perform an API request.
*/
class Request {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting - I've always implemented API adapters as self-contained, rather than passing them as dependencies using DI into a generic request class. Is this a pattern that we're using elsewhere? I can see how it would cut down on repeated code, but I can also see how it would introduce some confusion into utilization (you have to set the properties of the API object including request URL, then pass it to the request class for execution, versus calling a method on the API class to get the data). A way to make this a bit more user friendly would be to encapsulate this functionality in the JW Player API class like so:

public function get_videos_after( DateTimeImmutable $updated_after, int $batch_size ): array {
	$this->set_request_url(
		$updated_after->format( 'Y-m-d' ),
		$batch_size
	);

	return ( new Request( $this ) )->get();
}

That code could be moved out of the adapter and into the API wrapper, and then the adapter would call $this->jw_player_api->get_videos_after( $updated_after, $batch_size ) and would otherwise proceed as normal. That makes the way the API class makes its requests an implementation detail, rather than something that the adapter class needs to worry about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been updated in the latest commit: 7397082


/**
* The API requester.
*
* @var API_Requester
*/
public API_Requester $api_requester;

/**
* Constructor.
*
* @param API_Requester $api_requester The API requester.
*/
public function __construct( API_Requester $api_requester ) {
$this->api_requester = $api_requester;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* The API requester.
*
* @var API_Requester
*/
public API_Requester $api_requester;
/**
* Constructor.
*
* @param API_Requester $api_requester The API requester.
*/
public function __construct( API_Requester $api_requester ) {
$this->api_requester = $api_requester;
}
/**
* Constructor.
*
* @param API_Requester $api_requester The API requester.
*/
public function __construct( public API_Requester $api_requester ) {}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1


/**
* Set the user agent in the request headers for identification purposes.
*
* @return string
*/
public function user_agent(): string {
global $wp_version;

return 'WordPress/' . $wp_version . ' WPVideoSync/' . WP_VIDEO_SYNC_VERSION . ' PHP/' . phpversion();
}

/**
* Get the request arguments.
*
* @return array<string, mixed>
*/
public function get_request_args(): array {
$requester_args = $this->api_requester->get_request_args();

if ( empty( $requester_args['user-agent'] ) ) {
$requester_args['user-agent'] = $this->user_agent();
}

/**
* Allow the request arguments to be filtered before the request is made.
*
* @param array<string, string|float|int|bool|array> $requester_args The request arguments.
*/
return apply_filters( 'wp_video_sync_request_args', $requester_args );
}

/**
* Parse the API response.
*
* @param mixed $response The API response.
*
* @return array<string, mixed>
*/
private function parse_response( mixed $response ): array {
// Failed request expressed as a WP_Error.
if ( is_wp_error( $response ) || empty( $response ) || ! is_array( $response ) ) {
return [];
}

// Condition for when the response body is empty.
$response_body = wp_remote_retrieve_body( $response );

if ( empty( $response_body ) ) {
return [];
}

// Assign the response object for further evaluation.
$response_object = (array) json_decode( $response_body );

// Explicitly state the results based on response code.
return 200 === wp_remote_retrieve_response_code( $response )
? $this->api_requester->parse_success( $response_object )
: $this->api_requester->parse_error( $response_object );
}

/**
* Perform a GET request.
*
* @return array<string, mixed>
*/
public function get(): array {
if ( function_exists( 'vip_safe_wp_remote_get' ) ) {
$api_request = vip_safe_wp_remote_get(
$this->api_requester->get_request_url(),
'',
3,
5,
3,
$this->get_request_args()
);
} else {
// Get the base request arguments.
$custom_args = $this->get_request_args();

// Explicitly define the request arguments.
// NOTE: this is addressing an error stemming from PHPStan where the passed arguments to the `wp_remote_get()` were not accepted.
$request_args = [];

// Request method.
if ( ! empty( $custom_args['method'] ) && is_string( $custom_args['method'] ) ) {
$request_args['method'] = $custom_args['method'];
}

// Request headers.
if (
! empty( $custom_args['headers'] )
&& (
is_string( $custom_args['headers'] )
|| is_array( $custom_args['headers'] )
)
) {
$request_args['headers'] = $custom_args['headers'];
}

// Request timeout.
if ( ! empty( $custom_args['timeout'] ) && is_float( $custom_args['timeout'] ) ) {
$request_args['timeout'] = $custom_args['timeout'];
}

// Request user agent.
if ( ! empty( $custom_args['user-agent'] ) && is_string( $custom_args['user-agent'] ) ) {
$request_args['user-agent'] = $custom_args['user-agent'];
}

// Perform the request.
$api_request = wp_remote_get( // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get
$this->api_requester->get_request_url(),
array_filter( $request_args )
);
}

return $this->parse_response( $api_request );
}
}
Loading
Loading