setup_customer( $customer ); } /** * Given the customer data, let's set the variables * * @since 2.3 * * @param object $customer Customer object. * @return bool True if the object was setup correctly, false otherwise. */ private function setup_customer( $customer ) { if ( ! is_object( $customer ) ) { return false; } foreach ( $customer as $key => $value ) { switch ( $key ) { case 'purchase_value': $this->$key = floatval( $value ); break; case 'purchase_count': $this->$key = absint( $value ); break; default: $this->$key = $value; break; } } // Customer ID and email are the only things that are necessary, make sure they exist if ( ! empty( $this->id ) && ! empty( $this->email ) ) { return true; } return false; } /** * Magic __get method to dispatch a call to retrieve a protected property. * * @since 3.0 * * @param string $key * @return mixed */ public function __get( $key = '' ) { switch ( $key ) { case 'emails': return $this->get_emails(); case 'payment_ids': $payment_ids = $this->get_payment_ids(); $payment_ids = implode( ',', $payment_ids ); return $payment_ids; default: return isset( $this->{$key} ) ? $this->{$key} : edd_get_customer_meta( $this->id, $key ); } } /** * Magic __set method to dispatch a call to update a protected property. * * @since 3.0 * * @param string $key Property name. * @param mixed $value Property value. * * @return mixed Return value of setter being dispatched to. */ public function __set( $key, $value ) { $key = sanitize_key( $key ); // Only real properties can be saved. $keys = array_keys( get_class_vars( get_called_class() ) ); if ( ! in_array( $key, $keys, true ) ) { return false; } // Dispatch to setter method if value needs to be sanitized. if ( method_exists( $this, 'set_' . $key ) ) { return call_user_func( array( $this, 'set_' . $key ), $key, $value ); } else { $this->{$key} = $value; } } /** * Creates a customer based on class vars. * * @since 2.3 * * @param array $data Array of attributes for a customer * @return mixed False if not a valid creation, Customer ID if user is found or valid creation */ public function create( $data = array() ) { if ( 0 !== $this->id || empty( $data ) ) { return false; } $defaults = array( 'payment_ids' => '', ); $args = wp_parse_args( $data, $defaults ); $args = $this->sanitize_columns( $args ); if ( empty( $args['email'] ) || ! is_email( $args['email'] ) ) { return false; } /** * Fires before a customer is created * * @param array $args Contains customer information such as payment ID, name, and email. */ do_action( 'edd_customer_pre_create', $args ); $created = false; // Add the customer $customer_id = edd_add_customer( $args ); if ( ! empty( $customer_id ) ) { // Add the primary email address for this customer edd_add_customer_email_address( array( 'customer_id' => $customer_id, 'email' => $args['email'], 'type' => 'primary' ) ); // Maybe add payments if ( ! empty( $args['payment_ids'] ) && is_array( $args['payment_ids'] ) ) { $payment_ids = array_unique( array_values( $args['payment_ids'] ) ); foreach ( $payment_ids as $payment_id ) { edd_update_order( $payment_id, array( 'customer_id' => $customer_id ) ); } } // We've successfully added/updated the customer, reset the class vars with the new data $customer = edd_get_customer( $customer_id ); // Setup the customer data with the values from DB $this->setup_customer( $customer ); $created = $this->id; } /** * Fires after a customer is created * * @param int $created If created successfully, the customer ID. Defaults to false. * @param array $args Contains customer information such as payment ID, name, and email. */ do_action( 'edd_customer_post_create', $created, $args ); return $created; } /** * Update a customer record. * * @since 2.3 * * @param array $data Array of data attributes for a customer (checked via whitelist) * @return bool True if update was successful, false otherwise. */ public function update( $data = array() ) { if ( empty( $data ) ) { return false; } $data = $this->sanitize_columns( $data ); do_action( 'edd_customer_pre_update', $this->id, $data ); $updated = false; if ( edd_update_customer( $this->id, $data ) ) { $customer = edd_get_customer( $this->id ); $this->setup_customer( $customer ); $updated = true; } do_action( 'edd_customer_post_update', $updated, $this->id, $data ); return $updated; } /** * Attach an email address to the customer. * * @since 2.6 * @since 3.0.1 This method will return customer email ID or false, instead of bool * * @param string $email The email address to remove from the customer. * @param bool $primary Allows setting the email added as the primary. * * @return int|false ID of newly created customer email address, false on error. */ public function add_email( $email = '', $primary = false ) { if ( ! is_email( $email ) ) { return false; } // Bail if email exists in the universe. if ( $this->email_exists( $email ) ) { return false; } do_action( 'edd_customer_pre_add_email', $email, $this->id, $this ); // Primary or secondary $type = ( true === $primary ) ? 'primary' : 'secondary'; // Update is used to ensure duplicate emails are not added. $ret = edd_add_customer_email_address( array( 'customer_id' => $this->id, 'email' => $email, 'type' => $type, ) ); do_action( 'edd_customer_post_add_email', $email, $this->id, $this ); if ( $ret && true === $primary ) { $this->set_primary_email( $email ); } return $ret; } /** * Remove an email address from the customer. * * @since 2.6 * @since 3.0 Updated to use custom table. * * @param string $email The email address to remove from the customer. * @return bool True if the email was removed successfully, false otherwise. */ public function remove_email( $email = '' ) { if ( ! is_email( $email ) ) { return false; } do_action( 'edd_customer_pre_remove_email', $email, $this->id, $this ); $email_address = edd_get_customer_email_address_by( 'email', $email ); $ret = $email_address ? (bool) edd_delete_customer_email_address( $email_address->id ) : false; do_action( 'edd_customer_post_remove_email', $email, $this->id, $this ); return $ret; } /** * Check if an email address already exists somewhere in the known universe * of WordPress Users, or EDD customer email addresses. * * We intentionally skip the edd_customers table, to avoid race conditions * when adding new customers and their email addresses at the same time. * * @since 3.0 * * @param string $email Email address to check. * @return boolean True if assigned to existing customer, false otherwise. */ public function email_exists( $email = '' ) { // Bail if not an email address if ( ! is_email( $email ) ) { return false; } // Return true if found in users table if ( email_exists( $email ) ) { return true; } // Query email addresses table for this address $exists = edd_get_customer_email_address_by( 'email' , $email ); // Return true if found in email addresses table if ( ! empty( $exists ) ) { return true; } // Not found return false; } /** * Set an email address as the customer's primary email. * * This will move the customer's previous primary email to an additional email. * * @since 2.6 * @param string $new_primary_email The email address to remove from the customer. * @return bool True if the email was set as primary successfully, false otherwise. */ public function set_primary_email( $new_primary_email = '' ) { // Default return value $retval = false; // Bail if not an email if ( ! is_email( $new_primary_email ) ) { return $retval; } do_action( 'edd_customer_pre_set_primary_email', $new_primary_email, $this->id, $this ); // Bail if already primary if ( $new_primary_email === $this->email ) { return true; } // Get customer emails $emails = edd_get_customer_email_addresses( array( 'customer_id' => $this->id ) ); // Pluck addresses, to help with in_array() calls $plucked = wp_list_pluck( $emails, 'email' ); // Maybe fix a missing primary email address in the new table if ( ! in_array( $this->email, $plucked, true ) ) { // Attempt to add the current primary if it's missing $added = edd_add_customer_email_address( array( 'customer_id' => $this->id, 'email' => $this->email, 'type' => 'primary' ) ); // Maybe re-get all customer emails and re-pluck them if ( ! empty( $added ) ) { // Get customer emails $emails = edd_get_customer_email_addresses( array( 'customer_id' => $this->id ) ); // Pluck addresses, and look for the new one $plucked = wp_list_pluck( $emails, 'email' ); } } // Bail if not an address for this customer if ( ! in_array( $new_primary_email, $plucked, true ) ) { return $retval; } // Loop through addresses and juggle them foreach ( $emails as $email ) { // Make old primary a secondary if ( ( 'primary' === $email->type ) && ( $new_primary_email !== $email->email ) ) { edd_update_customer_email_address( $email->id, array( 'type' => 'secondary' ) ); } // Make new address primary if ( ( 'secondary' === $email->type ) && ( $new_primary_email === $email->email ) ) { edd_update_customer_email_address( $email->id, array( 'type' => 'primary' ) ); } } // Mismatch, so update the customer column if ( $this->email !== $new_primary_email ) { // Update the email column on the customer row $this->update( array( 'email' => $new_primary_email ) ); // Reload the customer emails for this object $this->email = $new_primary_email; $this->emails = $this->get_emails(); $retval = true; } do_action( 'edd_customer_post_set_primary_email', $new_primary_email, $this->id, $this ); return (bool) $retval; } /** * Before 3.0, when the primary email address was changed, it would cascade * through all previous purchases and update the email address associated * with it. Since 3.0, that is no longer the case. * * This method contains code that is no longer used, and is provided here as * a convenience function if needed. * * @since 3.0 */ public function update_order_email_addresses( $email = '' ) { // Get the payments $payment_ids = $this->get_payment_ids(); // Bail if no payments if ( empty( $payment_ids ) ) { return; } // Update payment emails to primary email foreach ( $payment_ids as $payment_id ) { edd_update_payment_meta( $payment_id, 'email', $email ); } } /** * Get the payment ids of the customer in an array. * * @since 2.6 * * @return array An array of payment IDs for the customer, or an empty array if none exist. */ public function get_payment_ids() { // Bail if no customer if ( empty( $this->id ) ) { return array(); } // Get total orders $count = edd_count_orders( array( 'customer_id' => $this->id ) ); // Get order IDs $ids = edd_get_orders( array( 'customer_id' => $this->id, 'number' => $count, 'fields' => 'ids', 'no_found_rows' => true ) ); // Cast IDs to ints return array_map( 'absint', $ids ); } /** * Get an array of EDD_Payment objects from the payment_ids attached to the customer. * * @since 2.6 * * @param array|string $status A single status as a string or an array of statuses. * @return array An array of EDD_Payment objects or an empty array. */ public function get_payments( $status = array() ) { // Get payment IDs $payment_ids = $this->get_payment_ids(); $payments = array(); // Bail if no IDs if ( empty( $payment_ids ) ) { return $payments; } // Get payments one at a time (ugh...) foreach ( $payment_ids as $payment_id ) { $payment = new EDD_Payment( $payment_id ); if ( empty( $status ) || ( is_array( $status ) && in_array( $payment->status, $status, true ) ) || $status === $payment->status ) { $payments[] = $payment; } } return $payments; } /** * Attach payment to the customer then triggers increasing statistics. * * @since 2.3 * * @param int $order_id The Order ID to attach to the customer. * @param bool $update_stats For backwards compatibility, if we should increase the stats or not. * * @return bool True if the attachment was successfully, false otherwise. */ public function attach_payment( $order_id = 0, $update_stats = true ) { // Bail if no payment ID. if ( empty( $order_id ) ) { return false; } // Get order. $order = edd_get_order( $order_id ); // Bail if payment does not exist. if ( empty( $order ) ) { return false; } do_action( 'edd_customer_pre_attach_payment', $order->id, $this->id, $this ); $success = (int) $order->customer_id === (int) $this->id; // Update the order if it isn't already attached. if ( ! $success ) { // Update the order. $success = (bool) edd_update_order( $order_id, array( 'customer_id' => $this->id, 'email' => $this->email, ) ); } // Maybe update stats. if ( ! empty( $success ) && ! empty( $update_stats ) ) { $this->recalculate_stats(); } do_action( 'edd_customer_post_attach_payment', $success, $order->id, $this->id, $this ); return $success; } /** * Remove a payment from this customer, then triggers reducing stats * * @since 2.3 * * @param integer $payment_id The Payment ID to remove. * @param bool $update_stats For backwards compatibility, if we should increase the stats or not. * * @return bool $detached True if removed successfully, false otherwise. */ public function remove_payment( $payment_id = 0, $update_stats = true ) { // Bail if no payment ID if ( empty( $payment_id ) ) { return false; } // Get payment $payment = edd_get_payment( $payment_id ); // Bail if payment does not exist if ( empty( $payment ) ) { return false; } // Get all previous payment IDs $payments = $this->get_payment_ids(); // Bail if already attached if ( ! in_array( $payment_id, $payments, true ) ) { return true; } // Only update stats when published or revoked if ( ! in_array( $payment->status, array( 'complete', 'revoked' ), true ) ) { $update_stats = false; } do_action( 'edd_customer_pre_remove_payment', $payment->ID, $this->id, $this ); // Update the order $success = (bool) edd_update_order( $payment_id, array( 'customer_id' => 0, 'email' => '' ) ); // Maybe update stats if ( ! empty( $success ) && ! empty( $update_stats ) ) { $this->recalculate_stats(); } do_action( 'edd_customer_post_remove_payment', $success, $payment->ID, $this->id, $this ); return $success; } /** * Recalculate stats for this customer. * * This replaces the older, less accurate increase/decrease methods. * * @since 3.0 */ public function recalculate_stats() { $this->purchase_count = edd_count_orders( array( 'customer_id' => $this->id, 'status' => edd_get_net_order_statuses(), 'type' => 'sale', ) ); global $wpdb; $statuses = edd_get_gross_order_statuses(); $status_string = implode(', ', array_fill( 0, count( $statuses ), '%s' ) ); $this->purchase_value = (float) $wpdb->get_var( $wpdb->prepare( "SELECT SUM(total / rate) FROM {$wpdb->edd_orders} WHERE customer_id = %d AND status IN({$status_string})", $this->id, ...$statuses ) ); // Update the customer purchase count & value return $this->update( array( 'purchase_count' => $this->purchase_count, 'purchase_value' => $this->purchase_value, ) ); } /** Notes *****************************************************************/ /** * Get the parsed notes for a customer as an array. * * @since 2.3 * @since 3.0 Use the new Notes component & API. * * @param integer $length The number of notes to get. * @param integer $paged What note to start at. * * @return array The notes requested. */ public function get_notes( $length = 20, $paged = 1 ) { // Number $length = is_numeric( $length ) ? absint( $length ) : 20; // Offset $offset = is_numeric( $paged ) && ( 1 !== $paged ) ? ( ( absint( $paged ) - 1 ) * $length ) : 0; // Return the paginated notes for back-compat return edd_get_notes( array( 'object_id' => $this->id, 'object_type' => 'customer', 'number' => $length, 'offset' => $offset, 'order' => 'desc', ) ); } /** * Get the total number of notes we have after parsing. * * @since 2.3 * @since 3.0 Use the new Notes component & API. * * @return int The number of notes for the customer. */ public function get_notes_count() { return edd_count_notes( array( 'object_id' => $this->id, 'object_type' => 'customer', ) ); } /** * Add a customer note. * * @since 2.3 * @since 3.0 Use the new Notes component & API * * @param string $note The note to add * @return string|boolean The new note if added successfully, false otherwise */ public function add_note( $note = '' ) { // Bail if note content is empty $note = trim( $note ); if ( empty( $note ) ) { return false; } /** * Filter the note of a customer before it's added * * @since 2.3 * @since 3.0 No longer includes the datetime stamp * * @param string $note The content of the note to add * @return string */ $note = apply_filters( 'edd_customer_add_note_string', $note ); /** * Allow actions before a note is added * * @since 2.3 */ do_action( 'edd_customer_pre_add_note', $note, $this->id, $this ); // Sanitize note $note = trim( wp_kses( stripslashes( $note ), edd_get_allowed_tags() ) ); // Try to add the note edd_add_note( array( 'user_id' => 0, // Authored by System/Bot 'object_id' => $this->id, 'object_type' => 'customer', 'content' => $note, ) ); /** * Allow actions after a note is added * * @since 3.0 Changed to an empty string since notes were moved out */ do_action( 'edd_customer_post_add_note', '', $note, $this->id, $this ); // Return the formatted note, so we can test, as well as update any displays return $note; } /** Meta ******************************************************************/ /** * Retrieve customer meta field for a customer. * * @since 2.6 * * @param string $key Optional. The meta key to retrieve. By default, returns data for all keys. Default empty. * @param bool $single Optional, default is false. If true, return only the first value of the specified meta_key. * This parameter has no effect if meta_key is not specified. * * @return mixed Will be an array if $single is false. Will be value of meta data field if $single is true. */ public function get_meta( $key = '', $single = true ) { return edd_get_customer_meta( $this->id, $key, $single ); } /** * Add meta data field to a customer. * * @since 2.6 * * @param string $meta_key Meta data name. * @param mixed $meta_value Meta data value. Must be serializable if non-scalar. * @param bool $unique Optional. Whether the same key should not be added. Default false. * * @return int|false Meta ID on success, false on failure. */ public function add_meta( $meta_key = '', $meta_value = '', $unique = false ) { return edd_add_customer_meta( $this->id, $meta_key, $meta_value, $unique ); } /** * Update customer meta field based on customer ID. * * Use the $prev_value parameter to differentiate between meta fields with the * same key and order ID. * * If the meta field for the order does not exist, it will be added. * * @since 2.6 * * @param string $meta_key Meta data key. * @param mixed $meta_value Meta data value. Must be serializable if non-scalar. * @param mixed $prev_value Optional. Previous value to check before removing. Default empty. * * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure. */ public function update_meta( $meta_key = '', $meta_value = '', $prev_value = '' ) { return edd_update_customer_meta( $this->id, $meta_key, $meta_value, $prev_value ); } /** * Remove meta data matching criteria from a customer. * * You can match based on the key, or key and value. Removing based on key and value, will keep from removing duplicate * meta data with the same key. It also allows removing all meta data matching key, if needed. * * @since 2.6 * * @param string $meta_key Meta data name. * @param mixed $meta_value Optional. Meta data value. Must be serializable if non-scalar. Default empty. * * @return bool True on success, false on failure. */ public function delete_meta( $meta_key = '', $meta_value = '' ) { return edd_delete_customer_meta( $this->id, $meta_key, $meta_value ); } /** Private ***************************************************************/ /** * Sanitize the data for update/create. * * @since 2.3 * * @param array $data The data to sanitize. * @return array The sanitized data, based off column defaults. */ private function sanitize_columns( $data = array() ) { $default_values = array(); foreach ( $data as $key => $type ) { // Only sanitize data that we were provided if ( ! array_key_exists( $key, $data ) ) { continue; } switch ( $type ) { case '%s': if ( 'email' === $key ) { $data[ $key ] = sanitize_email( $data[ $key ] ); } else { $data[ $key ] = sanitize_text_field( $data[ $key ] ); } break; case '%d': if ( ! is_numeric( $data[ $key ] ) || absint( $data[ $key ] ) !== (int) $data[ $key ] ) { $data[ $key ] = $default_values[ $key ]; } else { $data[ $key ] = absint( $data[ $key ] ); } break; case '%f': // Convert what was given to a float $value = floatval( $data[ $key ] ); if ( ! is_float( $value ) ) { $data[ $key ] = $default_values[ $key ]; } else { $data[ $key ] = $value; } break; default: $data[ $key ] = sanitize_text_field( $data[ $key ] ); break; } } return $data; } /** Helpers ***************************************************************/ /** * Retrieve all of the IP addresses used by the customer. * * @since 3.0 * * @return array Array of objects containing IP address. */ public function get_ips() { return edd_get_orders( array( 'customer_id' => $this->id, 'fields' => 'ip', 'groupby' => 'ip', ) ); } /** * Retrieve all the email addresses associated with this customer. * * @since 3.0 * * @return array */ public function get_emails() { // Add primary email. $retval = array( $this->email ); // Fetch email addresses from the database. $emails = edd_get_customer_email_addresses( array( 'customer_id' => $this->id ) ); // Pluck addresses and merg them if ( ! empty( $emails ) ) { // We only want the email addresses $emails = wp_list_pluck( $emails, 'email' ); // Merge with primary email $retval = array_merge( $retval, $emails ); } // Return unique results (to avoid duplicates) return array_unique( $retval ); } /** * Retrieve an address. * * @since 3.0 * * @param boolean $is_primary Whether the address is the primary address. Default true. * * @return array|\EDD\Customers\Customer_Address|null Object if primary address requested, array otherwise. Null if no result for primary address. */ public function get_address( $is_primary = true ) { $args = array( 'customer_id' => $this->id, 'is_primary' => $is_primary, ); if ( $is_primary ) { $args['number'] = 1; $args['orderby'] = 'date_created'; $args['order'] = 'desc'; } $address = edd_get_customer_addresses( $args ); if ( ! $is_primary ) { return $address; } if ( is_array( $address ) && ! empty( $address[0] ) ) { return $address[0]; } return null; } /** * Retrieve all addresses. * * @since 3.0 * * @param string $type Address type. Default empty. * * @return \EDD\Customers\Customer_Address[] Array of addresses. */ public function get_addresses( $type = '' ) { $addresses = edd_get_customer_addresses( array( 'customer_id' => $this->id, ) ); if ( ! empty( $type ) ) { $addresses = wp_filter_object_list( $addresses, array( 'type' => $type ) ); } return $addresses; } /** Deprecated ************************************************************/ /** * Increase the purchase count of a customer. * * @since 2.3 * @deprecated 3.0 Use recalculate_stats() * * @param int $count The number to increment purchase count by. Default 1. * @return int New purchase count. */ public function increase_purchase_count( $count = 1 ) { _edd_deprecated_function( __METHOD__, '3.0', 'EDD_Customer::recalculate_stats()' ); // Make sure it's numeric and not negative if ( ! is_numeric( $count ) || absint( $count ) !== $count ) { return false; } $new_total = (int) $this->purchase_count + (int) $count; do_action( 'edd_customer_pre_increase_purchase_count', $count, $this->id, $this ); if ( $this->update( array( 'purchase_count' => $new_total ) ) ) { $this->purchase_count = $new_total; } do_action( 'edd_customer_post_increase_purchase_count', $this->purchase_count, $count, $this->id, $this ); return $this->purchase_count; } /** * Decrease the customer's purchase count. * * @since 2.3 * @deprecated 3.0 Use recalculate_stats() * * @param int $count The number to decrement purchase count by. Default 1. * @return mixed New purchase count if successful, false otherwise. */ public function decrease_purchase_count( $count = 1 ) { _edd_deprecated_function( __METHOD__, '3.0', 'EDD_Customer::recalculate_stats()' ); // Make sure it's numeric and not negative if ( ! is_numeric( $count ) || absint( $count ) !== $count ) { return false; } $new_total = (int) $this->purchase_count - (int) $count; if ( $new_total < 0 ) { $new_total = 0; } do_action( 'edd_customer_pre_decrease_purchase_count', $count, $this->id, $this ); if ( $this->update( array( 'purchase_count' => $new_total ) ) ) { $this->purchase_count = $new_total; } do_action( 'edd_customer_post_decrease_purchase_count', $this->purchase_count, $count, $this->id, $this ); return $this->purchase_count; } /** * Increase the customer's lifetime value. * * @since 2.3 * @deprecated 3.0 Use recalculate_stats() * * @param float $value The value to increase by. * @return mixed New lifetime value if successful, false otherwise. */ public function increase_value( $value = 0.00 ) { _edd_deprecated_function( __METHOD__, '3.0', 'EDD_Customer::recalculate_stats()' ); $value = floatval( apply_filters( 'edd_customer_increase_value', $value, $this ) ); $new_value = floatval( $this->purchase_value ) + $value; do_action( 'edd_customer_pre_increase_value', $value, $this->id, $this ); if ( $this->update( array( 'purchase_value' => $new_value ) ) ) { $this->purchase_value = $new_value; } do_action( 'edd_customer_post_increase_value', $this->purchase_value, $value, $this->id, $this ); return $this->purchase_value; } /** * Decrease a customer's lifetime value. * * @since 2.3 * @deprecated 3.0 Use recalculate_stats() * * @param float $value The value to decrease by. * @return mixed New lifetime value if successful, false otherwise. */ public function decrease_value( $value = 0.00 ) { _edd_deprecated_function( __METHOD__, '3.0', 'EDD_Customer::recalculate_stats()' ); $value = floatval( apply_filters( 'edd_customer_decrease_value', $value, $this ) ); $new_value = floatval( $this->purchase_value ) - $value; if ( $new_value < 0 ) { $new_value = 0.00; } do_action( 'edd_customer_pre_decrease_value', $value, $this->id, $this ); if ( $this->update( array( 'purchase_value' => $new_value ) ) ) { $this->purchase_value = $new_value; } do_action( 'edd_customer_post_decrease_value', $this->purchase_value, $value, $this->id, $this ); return $this->purchase_value; } }