woocommerce/includes/data-stores/class-wc-coupon-data-store-...

422 lines
14 KiB
PHP

<?php
/**
* Class WC_Coupon_Data_Store_CPT file.
*
* @package WooCommerce\DataStore
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Coupon Data Store: Custom Post Type.
*
* @version 3.0.0
*/
class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface, WC_Object_Data_Store_Interface {
/**
* Internal meta type used to store coupon data.
*
* @since 3.0.0
* @var string
*/
protected $meta_type = 'post';
/**
* Data stored in meta keys, but not considered "meta" for a coupon.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array(
'discount_type',
'coupon_amount',
'expiry_date',
'date_expires',
'usage_count',
'individual_use',
'product_ids',
'exclude_product_ids',
'usage_limit',
'usage_limit_per_user',
'limit_usage_to_x_items',
'free_shipping',
'product_categories',
'exclude_product_categories',
'exclude_sale_items',
'minimum_amount',
'maximum_amount',
'customer_email',
'_used_by',
'_edit_lock',
'_edit_last',
);
/**
* Method to create a new coupon in the database.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
*/
public function create( &$coupon ) {
$coupon->set_date_created( current_time( 'timestamp', true ) );
$coupon_id = wp_insert_post(
apply_filters(
'woocommerce_new_coupon_data',
array(
'post_type' => 'shop_coupon',
'post_status' => 'publish',
'post_author' => get_current_user_id(),
'post_title' => $coupon->get_code( 'edit' ),
'post_content' => '',
'post_excerpt' => $coupon->get_description( 'edit' ),
'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ),
)
),
true
);
if ( $coupon_id ) {
$coupon->set_id( $coupon_id );
$this->update_post_meta( $coupon );
$coupon->save_meta_data();
$coupon->apply_changes();
delete_transient( 'rest_api_coupons_type_count' );
do_action( 'woocommerce_new_coupon', $coupon_id, $coupon );
}
}
/**
* Method to read a coupon.
*
* @since 3.0.0
*
* @param WC_Coupon $coupon Coupon object.
*
* @throws Exception If invalid coupon.
*/
public function read( &$coupon ) {
$coupon->set_defaults();
$post_object = get_post( $coupon->get_id() );
if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) {
throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) );
}
$coupon_id = $coupon->get_id();
$coupon->set_props(
array(
'code' => $post_object->post_title,
'description' => $post_object->post_excerpt,
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine.
'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ),
'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ),
'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ),
'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ),
'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null,
'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
'exclude_sale_items' => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ),
'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ),
'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ),
'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
)
);
$coupon->read_meta_data();
$coupon->set_object_read( true );
do_action( 'woocommerce_coupon_loaded', $coupon );
}
/**
* Updates a coupon in the database.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
*/
public function update( &$coupon ) {
$coupon->save_meta_data();
$changes = $coupon->get_changes();
if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) {
$post_data = array(
'post_title' => $coupon->get_code( 'edit' ),
'post_excerpt' => $coupon->get_description( 'edit' ),
'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ),
'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
);
/**
* When updating this object, to prevent infinite loops, use $wpdb
* to update data, since wp_update_post spawns more calls to the
* save_post action.
*
* This ensures hooks are fired by either WP itself (admin screen save),
* or an update purely from CRUD.
*/
if ( doing_action( 'save_post' ) ) {
$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) );
clean_post_cache( $coupon->get_id() );
} else {
wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) );
}
$coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
}
$this->update_post_meta( $coupon );
$coupon->apply_changes();
delete_transient( 'rest_api_coupons_type_count' );
do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon );
}
/**
* Deletes a coupon from the database.
*
* @since 3.0.0
*
* @param WC_Coupon $coupon Coupon object.
* @param array $args Array of args to pass to the delete method.
*/
public function delete( &$coupon, $args = array() ) {
$args = wp_parse_args(
$args,
array(
'force_delete' => false,
)
);
$id = $coupon->get_id();
if ( ! $id ) {
return;
}
if ( $args['force_delete'] ) {
wp_delete_post( $id );
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' );
$coupon->set_id( 0 );
do_action( 'woocommerce_delete_coupon', $id );
} else {
wp_trash_post( $id );
do_action( 'woocommerce_trash_coupon', $id );
}
}
/**
* Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class.
*
* @param WC_Coupon $coupon Coupon object.
* @since 3.0.0
*/
private function update_post_meta( &$coupon ) {
$meta_key_to_props = array(
'discount_type' => 'discount_type',
'coupon_amount' => 'amount',
'individual_use' => 'individual_use',
'product_ids' => 'product_ids',
'exclude_product_ids' => 'excluded_product_ids',
'usage_limit' => 'usage_limit',
'usage_limit_per_user' => 'usage_limit_per_user',
'limit_usage_to_x_items' => 'limit_usage_to_x_items',
'usage_count' => 'usage_count',
'date_expires' => 'date_expires',
'free_shipping' => 'free_shipping',
'product_categories' => 'product_categories',
'exclude_product_categories' => 'excluded_product_categories',
'exclude_sale_items' => 'exclude_sale_items',
'minimum_amount' => 'minimum_amount',
'maximum_amount' => 'maximum_amount',
'customer_email' => 'email_restrictions',
);
$props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $coupon->{"get_$prop"}( 'edit' );
$value = is_string( $value ) ? wp_slash( $value ) : $value;
switch ( $prop ) {
case 'individual_use':
case 'free_shipping':
case 'exclude_sale_items':
$value = wc_bool_to_string( $value );
break;
case 'product_ids':
case 'excluded_product_ids':
$value = implode( ',', array_filter( array_map( 'intval', $value ) ) );
break;
case 'product_categories':
case 'excluded_product_categories':
$value = array_filter( array_map( 'intval', $value ) );
break;
case 'email_restrictions':
$value = array_filter( array_map( 'sanitize_email', $value ) );
break;
case 'date_expires':
$value = $value ? $value->getTimestamp() : null;
break;
}
$updated = $this->update_or_delete_post_meta( $coupon, $meta_key, $value );
if ( $updated ) {
$this->updated_props[] = $prop;
}
}
do_action( 'woocommerce_coupon_object_updated_props', $coupon, $this->updated_props );
}
/**
* Increase usage count for current coupon.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
* @param string $used_by Either user ID or billing email.
* @return int New usage count.
*/
public function increase_usage_count( &$coupon, $used_by = '' ) {
$new_count = $this->update_usage_count_meta( $coupon, 'increase' );
if ( $used_by ) {
add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
}
do_action( 'woocommerce_increase_coupon_usage_count', $coupon, $new_count, $used_by );
return $new_count;
}
/**
* Decrease usage count for current coupon.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
* @param string $used_by Either user ID or billing email.
* @return int New usage count.
*/
public function decrease_usage_count( &$coupon, $used_by = '' ) {
global $wpdb;
$new_count = $this->update_usage_count_meta( $coupon, 'decrease' );
if ( $used_by ) {
/**
* We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
* all instances where the key and value match, and we only want to delete one.
*/
$meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", $used_by, $coupon->get_id() ) );
if ( $meta_id ) {
delete_metadata_by_mid( 'post', $meta_id );
$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
}
}
do_action( 'woocommerce_decrease_coupon_usage_count', $coupon, $new_count, $used_by );
return $new_count;
}
/**
* Increase or decrease the usage count for a coupon by 1.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
* @param string $operation 'increase' or 'decrease'.
* @return int New usage count
*/
private function update_usage_count_meta( &$coupon, $operation = 'increase' ) {
global $wpdb;
$id = $coupon->get_id();
$operator = ( 'increase' === $operation ) ? '+' : '-';
add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true );
$wpdb->query(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
"UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;",
$id
)
);
// Get the latest value direct from the DB, instead of possibly the WP meta cache.
return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) );
}
/**
* Get the number of uses for a coupon by user ID.
*
* @since 3.0.0
* @param WC_Coupon $coupon Coupon object.
* @param id $user_id User ID.
* @return int
*/
public function get_usage_by_user_id( &$coupon, $user_id ) {
global $wpdb;
return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", $coupon->get_id(), $user_id ) );
}
/**
* Get the number of uses for a coupon by email address
*
* @since 3.6.4
* @param WC_Coupon $coupon Coupon object.
* @param string $email Email address.
* @return int
*/
public function get_usage_by_email( &$coupon, $email ) {
global $wpdb;
return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;", $coupon->get_id(), $email ) );
}
/**
* Return a coupon code for a specific ID.
*
* @since 3.0.0
* @param int $id Coupon ID.
* @return string Coupon Code
*/
public function get_code_by_id( $id ) {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"SELECT post_title
FROM $wpdb->posts
WHERE ID = %d
AND post_type = 'shop_coupon'
AND post_status = 'publish'",
$id
)
);
}
/**
* Return an array of IDs for for a specific coupon code.
* Can return multiple to check for existence.
*
* @since 3.0.0
* @param string $code Coupon code.
* @return array Array of IDs.
*/
public function get_ids_by_code( $code ) {
global $wpdb;
return $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC",
$code
)
);
}
}