Files
wp-graphql-woocommerce/includes/data/connection/class-downloadable-item-connection-resolver.php
506d352292 fix: downloadsRemaining returns null for numeric string values (#1000)
* fix: downloadsRemaining returns null for valid numeric string values

Use is_numeric() + intval() instead of 'integer' === gettype() to handle
cases where WooCommerce stores downloads_remaining as a numeric string.
Mirrors WooCommerce's own approach in its download templates.

Co-authored-by: Nestor Vera <hacknug@users.noreply.github.com>
Closes #937

* fix: Match CI phpcov php-code-coverage version to Docker container

The Docker container uses php-code-coverage 9.2.x which serializes
coverage with v9 classes (e.g. Xdebug3Driver). phpcov v9 bundles
php-code-coverage v11 with different classes, causing
__PHP_Incomplete_Class errors. Pin to phpcov v8 + php-code-coverage v9
to match the Docker environment.

---------

Co-authored-by: Nestor Vera <hacknug@users.noreply.github.com>
2026-03-26 23:03:30 -04:00

170 lines
4.7 KiB
PHP

<?php
/**
* ConnectionResolver - Downloadable_Item_Connection_Resolver
*
* Resolves connections to DownloadableItem
*
* @package WPGraphQL\WooCommerce\Data\Connection
* @since 0.4.0
*/
namespace WPGraphQL\WooCommerce\Data\Connection;
use WPGraphQL\Data\Connection\AbstractConnectionResolver;
use WPGraphQL\WooCommerce\Model\Customer;
/**
* Class Downloadable_Item_Connection_Resolver
*
* @property \WPGraphQL\WooCommerce\Data\Loader\WC_Db_Loader $loader
*
* @package WPGraphQL\WooCommerce\Data\Connection
*/
class Downloadable_Item_Connection_Resolver extends AbstractConnectionResolver {
/**
* Return the name of the loader to be used with the connection resolver
*
* @return string
*/
public function get_loader_name() {
return 'downloadable_item';
}
/**
* Confirms if downloadable items should be retrieved.
*
* @return bool
*/
public function should_execute() {
return true;
}
/**
* Creates downloadable item filters.
*
* @return array
*/
public function get_query_args() {
$query_args = [ 'filters' => [] ];
if ( ! empty( $this->args['where'] ) ) {
$where_args = $this->args['where'];
if ( isset( $where_args['active'] ) ) {
$active = $where_args['active'];
$query_args['filters'][] = static function ( $downloadable_item ) use ( $active ) {
$is_expired = isset( $downloadable_item['access_expires'] )
? time() > $downloadable_item['access_expires']->getTimestamp()
: false;
$downloads_remaining = ( is_numeric( $downloadable_item['downloads_remaining'] ) )
? 0 < intval( $downloadable_item['downloads_remaining'] )
: true;
return $active ? ( ! $is_expired && $downloads_remaining ) : ( $is_expired || ! $downloads_remaining );
};
}
if ( isset( $where_args['expired'] ) ) {
$expired = $where_args['expired'];
$query_args['filters'][] = static function ( $downloadable_item ) use ( $expired ) {
$is_expired = isset( $downloadable_item['access_expires'] )
? time() < $downloadable_item['access_expires']->getTimestamp()
: false;
return $expired === $is_expired;
};
}
if ( isset( $where_args['hasDownloadsRemaining'] ) ) {
$has_downloads_remaining = $where_args['hasDownloadsRemaining'];
$query_args['filters'][] = static function ( $downloadable_item ) use ( $has_downloads_remaining ) {
$downloads_remaining = ( is_numeric( $downloadable_item['downloads_remaining'] ) )
? 0 < intval( $downloadable_item['downloads_remaining'] )
: true;
return $has_downloads_remaining === $downloads_remaining;
};
}
}//end if
/**
* Filter the $query_args to allow folks to customize queries programmatically.
*
* @param array $query_args The args that will be passed to the WP_Query.
* @param mixed $source The source that's passed down the GraphQL queries.
* @param array<string, mixed>|null $args The inputArgs on the field.
* @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree.
* @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the GraphQL tree.
*/
$query_args = apply_filters( 'graphql_downloadable_item_connection_query_args', $query_args, $this->source, $this->args, $this->context, $this->info );
return $query_args;
}
/**
* Executes query
*
* @return array
*/
public function get_query() {
$items = 0;
if ( is_a( $this->source, Customer::class ) ) {
$items = wc_get_customer_available_downloads( $this->source->ID );
} else {
$items = $this->source->downloadable_items;
}
if ( empty( $items ) ) {
return [];
}
if ( ! empty( $this->query_args['filters'] ) && is_array( $this->query_args['filters'] ) ) {
foreach ( $this->query_args['filters'] as $filter ) {
$items = array_filter( $items, $filter );
}
}
// Cache items for later.
foreach ( $items as $item ) {
$this->loader->prime( $item['download_id'], $item );
}
return array_column( $items, 'download_id' );
}
/**
* Return an array of items from the query
*
* @return array
*/
public function get_ids_from_query() {
return ! empty( $this->query ) ? $this->query : [];
}
/**
* Validates offset.
*
* @param integer $offset Post ID.
*
* @return bool
*/
public function is_valid_offset( $offset ) {
return 'string' === gettype( $offset );
}
/**
* Validates Model.
*
* If model isn't a class with a `fields` member, this function with have be overridden in
* the Connection class.
*
* @param array $model Downloadable item model.
*
* @return bool
*/
protected function is_valid_model( $model ) {
return ! empty( $model ) && ! empty( $model['download_id'] );
}
}