Automattic\WooCommerce\Internal\PushNotifications\DataStores
PushTokensDataStore{} │ WC 10.5.0
Data store class for push tokens.
No Hooks.
Usage
$PushTokensDataStore = new PushTokensDataStore(); // use class methods
Methods
- public create( array $data )
- public delete( int $id )
- public get_by_token_or_device_id( array $data )
- public get_tokens_for_roles( array $roles, ?int $page = null, ?int $per_page = null )
- public read( int $id )
- public update( PushToken $push_token )
- private build_meta_array_from_database( int $id )
- private build_meta_array_from_token( PushToken $push_token )
Changelog
| Since 10.5.0 | Introduced. |
PushTokensDataStore{} PushTokensDataStore{} code WC 10.8.1
class PushTokensDataStore {
/**
* In-memory cache for get_tokens_for_roles() results, keyed by the
* comma-joined role list (with optional pagination suffix). Avoids
* repeated DB queries within the same PHP request.
*
* @var array<string, PushToken[]|array{tokens: PushToken[], total: int, total_pages: int}>
*/
private array $tokens_by_roles_cache = array();
const SUPPORTED_META = array(
'origin',
'device_uuid',
'token',
'platform',
'device_locale',
'metadata',
);
/**
* Creates a post representing the push token.
*
* @since 10.5.0
* @param array $data Token data with keys: user_id, token, platform, device_uuid (optional), origin.
* @throws PushTokenInvalidDataException If the token data is invalid.
* @throws WC_Data_Exception If the token creation fails.
* @return PushToken The created push token with ID set.
*/
public function create( array $data ): PushToken {
$push_token = new PushToken( $data );
if ( ! $push_token->can_be_created() ) {
throw new PushTokenInvalidDataException(
'Can\'t create push token because the push token data provided is invalid.'
);
}
$id = wp_insert_post(
array(
'post_author' => (int) $push_token->get_user_id(),
'post_type' => PushToken::POST_TYPE,
'post_status' => 'private',
'meta_input' => $this->build_meta_array_from_token( $push_token ),
),
true
);
if ( is_wp_error( $id ) ) {
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new WC_Data_Exception(
(string) $id->get_error_code(),
$id->get_error_message(),
WP_Http::INTERNAL_SERVER_ERROR
);
// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
$push_token->set_id( $id );
return $push_token;
}
/**
* Gets post representing a push token.
*
* @since 10.5.0
* @param int $id The push token ID.
* @throws PushTokenInvalidDataException If the ID is invalid.
* @throws PushTokenNotFoundException If the token can't be found.
* @return PushToken The populated push token.
*/
public function read( int $id ): PushToken {
$push_token = new PushToken( array( 'id' => $id ) );
$post = get_post( $push_token->get_id() );
if ( ! $post || PushToken::POST_TYPE !== $post->post_type ) {
throw new PushTokenNotFoundException();
}
$meta = $this->build_meta_array_from_database( (int) $push_token->get_id() );
if (
empty( $meta['token'] )
|| empty( $meta['platform'] )
|| empty( $meta['origin'] )
|| (
empty( $meta['device_uuid'] )
&& PushToken::PLATFORM_BROWSER !== $meta['platform']
)
) {
throw new PushTokenInvalidDataException(
'Can\'t read push token because the push token record is malformed.'
);
}
$push_token->set_user_id( (int) $post->post_author );
$push_token->set_token( $meta['token'] );
$push_token->set_device_uuid( $meta['device_uuid'] ?? null );
$push_token->set_platform( $meta['platform'] );
$push_token->set_origin( $meta['origin'] );
/**
* These meta items were added after the ability to store tokens, so may
* not be available for older tokens. Use sensible defaults.
*/
$push_token->set_device_locale( $meta['device_locale'] ?? PushToken::DEFAULT_DEVICE_LOCALE );
$push_token->set_metadata( $meta['metadata'] ?? array() );
return $push_token;
}
/**
* Updates a post representing the push token.
*
* @since 10.5.0
* @param PushToken $push_token The push token to update.
* @throws PushTokenInvalidDataException If the token can't be updated.
* @throws WC_Data_Exception If the token update fails.
* @return bool True on success.
*/
public function update( PushToken $push_token ): bool {
if ( ! $push_token->can_be_updated() ) {
throw new PushTokenInvalidDataException(
'Can\'t update push token because the push token data provided is invalid.'
);
}
$result = wp_update_post(
array(
'ID' => (int) $push_token->get_id(),
'post_author' => (int) $push_token->get_user_id(),
'post_type' => PushToken::POST_TYPE,
'post_status' => 'private',
'meta_input' => $this->build_meta_array_from_token( $push_token ),
),
true
);
if ( is_wp_error( $result ) ) {
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new WC_Data_Exception(
(string) $result->get_error_code(),
$result->get_error_message(),
WP_Http::INTERNAL_SERVER_ERROR
);
// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
if ( null === $push_token->get_device_uuid() ) {
delete_post_meta( (int) $push_token->get_id(), 'device_uuid' );
}
return true;
}
/**
* Deletes a push token.
*
* @since 10.5.0
* @param int $id The push token ID.
* @throws PushTokenNotFoundException If the token can't be found.
* @return bool True on success.
*/
public function delete( int $id ): bool {
$post = get_post( $id );
if ( ! $post || PushToken::POST_TYPE !== $post->post_type ) {
throw new PushTokenNotFoundException();
}
return (bool) wp_delete_post( (int) $id, true );
}
/**
* Find tokens for this user and platform that match either the token
* or device UUID. We check the token value to avoid creating a duplicate.
* We check the device UUID value because only one token should be issued
* per device, therefore if we already have one then we can update it to
* avoid creating a duplicate.
*
* @since 10.5.0
* @param array $data Token data with keys: user_id, platform, origin, token (optional), device_uuid (optional).
* @return null|PushToken
* @throws PushTokenInvalidDataException If push token is missing data.
*/
public function get_by_token_or_device_id( array $data ): ?PushToken {
$user_id = $data['user_id'] ?? null;
$platform = $data['platform'] ?? null;
$origin = $data['origin'] ?? null;
$token = $data['token'] ?? null;
$device_uuid = $data['device_uuid'] ?? null;
if (
! $user_id
|| ! $platform
|| ! $origin
|| (
/**
* Platforms iOS and Android require token OR device UUID.
*/
PushToken::PLATFORM_BROWSER !== $platform
&& ! $token
&& ! $device_uuid
)
|| (
/**
* Browsers don't have device UUIDs, so require token.
*/
PushToken::PLATFORM_BROWSER === $platform
&& ! $token
)
) {
throw new PushTokenInvalidDataException(
'Can\'t retrieve push token because the push token data provided is invalid.'
);
}
$query = new WP_Query(
array(
'post_type' => PushToken::POST_TYPE,
'post_status' => 'private',
'author' => $user_id,
'posts_per_page' => -1,
'orderby' => 'ID',
'order' => 'DESC',
'fields' => 'ids',
)
);
/**
* Typehint for PHPStan, specifies these are IDs and not instances of
* WP_Post.
*
* @var int[] $post_ids
*/
$post_ids = $query->posts;
if ( empty( $post_ids ) ) {
return null;
}
update_meta_cache( 'post', $post_ids );
foreach ( $post_ids as $post_id ) {
try {
$meta = $this->build_meta_array_from_database( $post_id );
} catch ( Exception $e ) {
wc_get_logger()->warning(
'Failed to load meta for push token.',
array(
'token_id' => $post_id,
'error' => $e->getMessage(),
)
);
continue;
}
if (
$meta['platform'] === $platform
&& $meta['origin'] === $origin
&& (
( $token && $token === $meta['token'] )
|| ( $device_uuid && $device_uuid === $meta['device_uuid'] )
)
) {
return new PushToken(
array(
'id' => $post_id,
'user_id' => $user_id,
'token' => $meta['token'],
'device_uuid' => $meta['device_uuid'] ?? null,
'platform' => $meta['platform'],
'origin' => $meta['origin'],
/**
* These meta items were added after the ability to store
* tokens, so may not be available for older tokens. Use
* sensible defaults.
*/
'device_locale' => $meta['device_locale'] ?? PushToken::DEFAULT_DEVICE_LOCALE,
'metadata' => $meta['metadata'] ?? array(),
)
);
}
}
return null;
}
/**
* Returns push tokens belonging to users with the given roles.
*
* When called without pagination parameters, returns all tokens as a
* flat array (cached per-request). When $page and $per_page are
* provided, returns a paginated result with total counts.
*
* @param string[] $roles The roles to query tokens for.
* @param int|null $page Optional page number (1-based).
* @param int|null $per_page Optional number of tokens per page.
* @return PushToken[]|array{tokens: PushToken[], total: int, total_pages: int}
*
* @since 10.7.0
*/
public function get_tokens_for_roles( array $roles, ?int $page = null, ?int $per_page = null ) {
$paginate = null !== $page && null !== $per_page;
$cache_key = $paginate ? implode( ',', $roles ) . ":$page:$per_page" : implode( ',', $roles );
$empty_result = $paginate
? array(
'tokens' => array(),
'total' => 0,
'total_pages' => 0,
)
: array();
if ( empty( $roles ) ) {
return $empty_result;
}
if ( isset( $this->tokens_by_roles_cache[ $cache_key ] ) ) {
return $this->tokens_by_roles_cache[ $cache_key ];
}
$user_ids = get_users(
array(
'role__in' => $roles,
'fields' => 'ID',
)
);
if ( empty( $user_ids ) ) {
$this->tokens_by_roles_cache[ $cache_key ] = $empty_result;
return $this->tokens_by_roles_cache[ $cache_key ];
}
$query_args = array(
'post_type' => PushToken::POST_TYPE,
'post_status' => 'private',
'author__in' => $user_ids,
'posts_per_page' => $paginate ? $per_page : -1,
'fields' => 'ids',
);
if ( $paginate ) {
$query_args['paged'] = $page;
$query_args['orderby'] = 'ID';
$query_args['order'] = 'ASC';
}
$query = new WP_Query( $query_args );
/**
* Typehint for PHPStan, specifies these are IDs and not instances of
* WP_Post.
*
* @var int[] $post_ids
*/
$post_ids = $query->posts;
if ( empty( $post_ids ) ) {
$this->tokens_by_roles_cache[ $cache_key ] = $empty_result;
return $this->tokens_by_roles_cache[ $cache_key ];
}
update_meta_cache( 'post', $post_ids );
$tokens = array();
foreach ( $post_ids as $post_id ) {
try {
$tokens[] = $this->read( (int) $post_id );
} catch ( WC_Data_Exception $e ) {
wc_get_logger()->warning(
'Skipping malformed push token during role-based query.',
array(
'token_id' => $post_id,
'error' => $e->getMessage(),
)
);
}
}
$result = $paginate
? array(
'tokens' => $tokens,
'total' => (int) $query->found_posts,
'total_pages' => (int) $query->max_num_pages,
)
: $tokens;
$this->tokens_by_roles_cache[ $cache_key ] = $result;
return $result;
}
/**
* Returns an associative array of post meta as key => value pairs for the
* keys defined in SUPPORTED_META; missing keys return null. Use
* `update_meta_cache` with `get_post_meta` to allow reading the meta as
* single values which automatically unserialize when requires,
* rather than nested arrays that don't.
*
* @since 10.5.0
* @param int $id The push token ID.
* @return array
*/
private function build_meta_array_from_database( int $id ): array {
$meta_by_key = array_fill_keys( static::SUPPORTED_META, null );
foreach ( static::SUPPORTED_META as $key ) {
$meta = get_post_meta( $id, $key, true );
if ( '' !== $meta ) {
$meta_by_key[ $key ] = $meta;
}
}
return $meta_by_key;
}
/**
* Returns an associative array of post meta as key => value pairs, built
* using push token properties.
*
* @since 10.5.0
* @param PushToken $push_token An instance of PushToken.
* @return array
*/
private function build_meta_array_from_token( PushToken $push_token ) {
return array_filter(
array(
'platform' => $push_token->get_platform(),
'token' => $push_token->get_token(),
'device_uuid' => $push_token->get_device_uuid(),
'origin' => $push_token->get_origin(),
'device_locale' => $push_token->get_device_locale(),
'metadata' => $push_token->get_metadata(),
),
fn ( $value ) => null !== $value && '' !== $value
);
}
}