Files

285 lines
8.0 KiB
PHP
Raw Permalink Normal View History

2023-06-21 13:21:29 -04:00
<?php
/**
* COT Cursor
*
* This class generates the SQL AND operators for cursor based pagination
* for the custom orders table/HPOS
*
* @package WPGraphQL\WooCommerce\Data\Cursor;
2023-06-21 14:41:43 -04:00
* @since 0.14.0
2023-06-21 13:21:29 -04:00
*/
namespace WPGraphQL\WooCommerce\Data\Cursor;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableMetaQuery;
use WPGraphQL\Data\Cursor\AbstractCursor;
2023-06-21 13:21:29 -04:00
/**
* Class COT_Cursor
*/
class COT_Cursor extends AbstractCursor {
/**
* Stores the cursory order node
*
* @var ?\WC_Abstract_Order
*/
public $cursor_node;
/**
* Counter for meta value joins
*
* @var integer
*/
public $meta_join_alias = 0;
/**
* The query instance to use when building the SQL statement.
*
* @var \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableQuery
2023-06-21 13:21:29 -04:00
*/
public $query;
/**
* Names of all COT tables (orders, addresses, operational_data, meta) in the form 'table_id' => 'table name'.
*
* @var array
*/
private $tables;
/**
* Undocumented variable
*
* @var array
*/
private $column_mappings;
/**
* Meta query parser.
*
* @var \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableMetaQuery|null
2023-06-21 13:21:29 -04:00
*/
private $meta_query;
/**
* COT_Cursor constructor.
*
* @param array $query_vars The query vars to use when building the SQL statement.
* @param \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableQuery $query The query to use when building the SQL statement.
* @param string|null $cursor Whether to generate the before or after cursor. Default "after".
2023-06-21 13:21:29 -04:00
*/
public function __construct( $query_vars, $query, $cursor = 'after' ) {
// Initialize the class properties.
parent::__construct( $query_vars, $cursor );
$this->query = $query;
// Get tables.
/** @var \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore $order_datastore */
$order_datastore = wc_get_container()->get( OrdersTableDataStore::class );
$this->tables = $order_datastore::get_all_table_names_with_id();
$mappings = $order_datastore->get_all_order_column_mappings();
$this->column_mappings = [];
foreach ( $mappings['orders'] as $column => $meta_value ) {
$this->column_mappings[ "{$this->tables['orders']}.{$column}" ] = $meta_value['name'];
}
if ( ! is_null( $this->get_query_var( 'meta_query' ) ) ) {
$this->meta_query = new OrdersTableMetaQuery( $this->query );
}
}
/**
* Resolves an orderby alias (e.g. 'date', 'total') to the fully qualified
* COT column name used in the column_mappings.
*
* @param string $alias The orderby alias.
*
* @return string The resolved column name, or the original alias if no mapping found.
*/
public function resolve_orderby_alias( $alias ) {
$alias_map = [
// Short aliases used by WC OrdersTableQuery.
'post_date' => 'date_created_gmt',
'date' => 'date_created_gmt',
'date_created' => 'date_created_gmt',
'modified' => 'date_updated_gmt',
'date_modified' => 'date_updated_gmt',
'type' => 'type',
'parent' => 'parent_order_id',
'total' => 'total_amount',
'order_total' => 'total_amount',
// Legacy meta keys that map to COT columns in HPOS mode.
'_order_total' => 'total_amount',
'_order_tax' => 'tax_amount',
'_cart_discount' => 'discount_total_amount',
'_date_paid' => 'date_paid_gmt',
'_date_completed' => 'date_completed_gmt',
'_order_key' => 'order_key',
];
return $alias_map[ $alias ] ?? $alias;
}
2023-06-21 13:21:29 -04:00
/**
* {@inheritDoc}
*
* @return ?\WC_Abstract_Order
*/
public function get_cursor_node() {
// If we have a bad cursor, just skip.
if ( ! $this->cursor_offset ) {
return null;
}
// Get the order.
$order = wc_get_order( $this->cursor_offset );
return $order instanceof \WC_Abstract_Order ? $order : null;
}
/**
* {@inheritDoc}
*/
public function to_sql() {
$orderby = isset( $this->query_vars['orderby'] ) ? $this->query_vars['orderby'] : null;
$orderby_should_not_convert_to_sql = isset( $orderby ) && in_array(
$orderby,
[
'include',
'id',
'parent_order_id',
],
true
);
if ( true === $orderby_should_not_convert_to_sql ) {
return '';
}
$sql = $this->builder->to_sql();
if ( empty( $sql ) ) {
return '';
}
return ' AND ' . $sql;
}
/**
* {@inheritDoc}
*/
public function get_where() {
// If we have a bad cursor, just skip.
if ( ! $this->is_valid_offset_and_node() ) {
return '';
}
$orderby = $this->get_query_var( 'orderby' );
$order = $this->get_query_var( 'order' );
if ( ! empty( $orderby ) && is_array( $orderby ) ) {
/**
* Loop through all order keys if it is an array
*/
foreach ( $orderby as $by => $order ) {
$this->compare_with( $by, $order );
}
} elseif ( ! empty( $orderby ) && is_string( $orderby ) ) {
/**
* If $orderby is just a string just compare with it directly as DESC
*/
$this->compare_with( $orderby, $order );
}
/**
* No custom comparing. Use the default date
*/
if ( ! $this->builder->has_fields() ) {
$this->compare_with_date();
}
$this->builder->add_field( "{$this->tables['orders']}.id", $this->cursor_offset, 'ID', $order );
return $this->to_sql();
}
/**
* Get AND operator for given order by key
*
* @param string $by The order by key.
* @param string $order The order direction ASC or DESC.
*
* @return void
*/
private function compare_with( $by, $order ) {
if ( null === $this->cursor_node ) {
return;
}
// Check for explicit cursor compare fields set by the connection resolver.
$key = $this->get_query_var( "graphql_cursor_compare_by_{$by}_key" );
$value = $this->get_query_var( "graphql_cursor_compare_by_{$by}_value" );
if ( ! empty( $key ) && ! empty( $value ) ) {
$this->builder->add_field( $key, $value, null, $order );
return;
}
2023-06-21 13:21:29 -04:00
// Resolve the orderby key to a COT column. For meta_value/meta_value_num,
// use the meta_key query var since that holds the actual field name.
$table_name = $this->tables['orders'];
$source = in_array( $by, [ 'meta_value', 'meta_value_num' ], true )
? ( $this->get_query_var( 'meta_key' ) ?? $by )
: $by;
$column = $this->resolve_orderby_alias( $source );
$orderby = "{$table_name}.{$column}";
$getter_name = $this->column_mappings[ $orderby ] ?? null;
2023-06-21 13:21:29 -04:00
if ( null !== $getter_name ) {
2023-06-21 13:21:29 -04:00
$method = "get_{$getter_name}";
$value = is_callable( [ $this->cursor_node, $method ] ) ? $this->cursor_node->$method() : null;
} else {
// Fall back to meta query if the field doesn't map to a COT column.
$meta_orderby_keys = $this->meta_query ? $this->meta_query->get_orderby_keys() : [];
if ( in_array( $by, $meta_orderby_keys, true ) && null !== $this->meta_query ) {
$orderby = $this->meta_query->get_orderby_clause_for_key( $by );
$value = $this->cursor_node->get_meta( $source, true ) ?? null;
} else {
return;
}
2023-06-21 13:21:29 -04:00
}
if ( ! empty( $value ) && is_a( $value, '\WC_DateTime' ) ) {
$value = ( new \DateTime( $value ) )->setTimezone( new \DateTimeZone( '+00:00' ) )->format( 'Y-m-d H:i:s' );
$this->builder->add_field( $orderby, $value, 'DATETIME', $order );
2023-06-21 13:21:29 -04:00
return;
}
/**
* Compare by the post field if the key matches a value
*/
if ( ! empty( $value ) ) {
$this->builder->add_field( $orderby, $value, null, $order );
2023-06-21 13:21:29 -04:00
}
}
/**
* Use post date based comparison
*
* @return void
*/
private function compare_with_date() {
$value = null;
if ( null !== $this->cursor_node ) {
$date_created = $this->cursor_node->get_date_created();
$value = ! empty( $date_created ) ? ( new \DateTime( $date_created ) )->setTimezone( new \DateTimeZone( '+00:00' ) )->format( 'Y-m-d H:i:s' ) : null;
}
$this->builder->add_field( "{$this->tables['orders']}.date_created_gmt", $value, 'DATETIME' );
}
}