LLMS_Engagements
Engagements Class
Contents
Source Source
File: includes/class.llms.engagements.php
class LLMS_Engagements { use LLMS_Trait_Singleton; /** * Enable debug logging * * @since 2.7.9 * @var boolean */ private $debug = false; /** * Constructor * * Adds actions to events that trigger engagements. * * @since 2.3.0 * @since 6.0.0 Added deprecation warning when using constant `LLMS_ENGAGEMENT_DEBUG`. * Don't call deprecated `init()` method. * * @return void */ private function __construct() { if ( defined( 'LLMS_ENGAGEMENT_DEBUG' ) && LLMS_ENGAGEMENT_DEBUG ) { _deprecated_function( 'Constant: LLMS_ENGAGEMENT_DEBUG', '6.0.0' ); $this->debug = true; } $this->add_actions(); } /** * Register all actions that trigger engagements * * @since 2.3.0 * @since 3.11.0 Unknown. * @since 3.39.0 Added `llms_rest_student_registered` as action hook. * @since 6.0.0 Moved the list of hooks to the `get_trigger_hooks()` method. * * @return void */ private function add_actions() { foreach ( $this->get_trigger_hooks() as $action ) { add_action( $action, array( $this, 'maybe_trigger_engagement' ), 777, 3 ); } // Handlers are in charge of processing (awarding/sending) the email/cert/achievement. $handlers = array( 'lifterlms_engagement_send_email' => 'handle_email', 'lifterlms_engagement_award_achievement' => 'handle_achievement', 'lifterlms_engagement_award_certificate' => 'handle_certificate', ); foreach ( $handlers as $action => $method ) { /** * Adds an action for the deprecated method so that `remove_action()` calls * on the old method will continue to remove the new method. * * When we *remove* the deprecated methods we can remove this logic. */ add_action( $action, array( $this, $method ) ); // If the above action has been completely removed this will be false and we won't add the new method callback. $priority = has_action( $action, array( $this, $method ) ); if ( false !== $priority ) { // Remove the deprecated action. remove_action( $action, array( $this, $method ) ); // Call the new action at the specified priority. If the old action was restored at a different priority this will retain that customization. add_action( $action, array( 'LLMS_Engagement_Handler', $method ), $priority ); } } add_action( 'deleted_post', array( $this, 'unschedule_delayed_engagements' ), 20, 2 ); } /** * Retrieve a group id used when scheduling delayed engagement action triggers. * * @since 6.0.0 * * @param int $engagement_id WP_Post ID of the `llms_engagement` post type. * @return string */ private function get_delayed_group_id( $engagement_id ) { return sprintf( 'llms_engagement_%d', $engagement_id ); } /** * Retrieve engagements based on the trigger type * * Joins rather than nested loops and sub queries ftw. * * @since 2.3.0 * @since 3.13.1 Unknown. * @since 6.0.0 Removed engagement debug logging & moved filter onto the return instead of calling in `maybe_trigger_engagement()`. * * @param string $trigger_type Name of the trigger to look for. * @param int|string $related_post_id The WP_Post ID of the related post or an empty string. * @return object[] { * Array of objects from the database. * * @type int $engagement_id WP_Post ID of the engagement post (email, certificate, achievement). * @type int $trigger_id WP_Post ID of the llms_engagement post. * @type string $trigger_event The triggering action (user_registration, course_completed, etc...). * @type string $event_type The engagement event action (certificate, achievement, email). * @type int $delay The engagement send delay (in days). * } */ private function get_engagements( $trigger_type, $related_post_id = '' ) { global $wpdb; $related_select = ''; $related_join = ''; $related_where = ''; if ( $related_post_id ) { $related_select = ', relation_meta.meta_value AS related_post_id'; $related_join = "LEFT JOIN $wpdb->postmeta AS relation_meta ON triggers.ID = relation_meta.post_id"; $related_where = $wpdb->prepare( "AND relation_meta.meta_key = '_llms_engagement_trigger_post' AND relation_meta.meta_value = %d", $related_post_id ); } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $results = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT triggers.ID AS trigger_id , triggers_meta.meta_value AS engagement_id , engagements_meta.meta_value AS trigger_event , event_meta.meta_value AS event_type , delay.meta_value AS delay $related_select FROM $wpdb->postmeta AS engagements_meta LEFT JOIN $wpdb->posts AS triggers ON triggers.ID = engagements_meta.post_id LEFT JOIN $wpdb->postmeta AS triggers_meta ON triggers.ID = triggers_meta.post_id LEFT JOIN $wpdb->posts AS engagements ON engagements.ID = triggers_meta.meta_value LEFT JOIN $wpdb->postmeta AS event_meta ON triggers.ID = event_meta.post_id LEFT JOIN $wpdb->postmeta AS delay ON triggers.ID = delay.post_id $related_join WHERE triggers.post_type = 'llms_engagement' AND triggers.post_status = 'publish' AND triggers_meta.meta_key = '_llms_engagement' AND engagements_meta.meta_key = '_llms_trigger_type' AND engagements_meta.meta_value = %s AND engagements.post_status = 'publish' AND event_meta.meta_key = '_llms_engagement_type' AND delay.meta_key = '_llms_engagement_delay' $related_where ", // Prepare variables. $trigger_type ), OBJECT ); // no-cache ok. // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared /** * Filters the list of engagements to be triggered for a given trigger type and related post. * * @since 6.0.0 * * @param object[] $results Array of engagement objects. * @param string $trigger_type Name of the engagement trigger. * @param int $related_post_id WP_Post ID of the related post. */ return apply_filters( 'lifterlms_get_engagements', $results, $trigger_type, $related_post_id ); } /** * Retrieve a list of hooks that trigger engagements to be awarded. * * @since 6.0.0 * * @return string[] */ protected function get_trigger_hooks() { $hooks = array( 'lifterlms_access_plan_purchased', 'lifterlms_course_completed', 'lifterlms_course_track_completed', 'lifterlms_lesson_completed', 'lifterlms_product_purchased', 'lifterlms_quiz_completed', 'lifterlms_quiz_failed', 'lifterlms_quiz_passed', 'lifterlms_section_completed', 'lifterlms_user_registered', 'llms_rest_student_registered', 'llms_user_added_to_membership_level', 'llms_user_enrolled_in_course', ); // If there are any actions registered to this deprecated hook, add it to the list. if ( has_action( 'lifterlms_created_person' ) ) { $hooks[] = 'lifterlms_created_person'; } /** * Filters the list of hooks which can trigger engagements to be sent/awarded. * * @since 2.3.0 * * @param string[] $hooks List of hook names. */ return apply_filters( 'lifterlms_engagement_actions', $hooks ); } /** * Include engagement types (excluding email) * * @since Unknown * @deprecated 6.0.0 `LLMS_Engagements::init()` is deprecated with no replacement. * * @return void */ public function init() { _deprecated_function( 'LLMS_Engagements::init()', '6.0.0' ); } /** * Award an achievement * * @since 2.3.0 * @deprecated 6.0.0 `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`. * * @param array $args { * Indexed array of arguments. * * @type int $0 WP_User ID. * @type int $1 WP_Post ID of the achievement template post. * @type int|string $2 WP_Post ID of the related post that triggered the award or an empty string. * @type int $3 WP_Post ID of the engagement post. * } * @return void */ public function handle_achievement( $args ) { _deprecated_function( 'LLMS_Engagements::handle_achievement', '6.0.0', 'LLMS_Engagement_Handler::handle_achievement' ); LLMS_Engagement_Handler::handle_achievement( $args ); } /** * Award a certificate * * @since 2.3.0 * @deprecated 6.0.0 `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`. * * @param array $args { * Indexed array of arguments. * * @type int $0 WP_User ID. * @type int $1 WP_Post ID of the certificate template post. * @type int|string $2 WP_Post ID of the related post that triggered the award or an empty string. * @type int $3 WP_Post ID of the engagement post. * } * @return void */ public function handle_certificate( $args ) { _deprecated_function( 'LLMS_Engagements::handle_certificate', '6.0.0', 'LLMS_Engagement_Handler::handle_certificate' ); LLMS_Engagement_Handler::handle_certificate( $args ); } /** * Send an email engagement * * This is called via do_action() by the 'maybe_trigger_engagement' function in this class. * * @since 2.3.0 * @since 3.8.0 Unknown. * @since 4.4.1 Use postmeta helpers for dupcheck and postmeta insertion. * Add a return value in favor of `void`. * Log successes and failures to the `engagement-emails` log file instead of the main `llms` log. * @since 4.4.3 Fixed different emails triggered by the same related post not sent because of a wrong duplicate check. * Fixed dupcheck log message and error message which reversed the email and person order. * @deprecated 6.0.0 `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`. * * @param mixed[] $args { * An array of arguments from the triggering hook. * * @type int $0 WP_User ID. * @type int $1 WP_Post ID of the email. * @type int|string $2 WP_Post ID of the related triggering post or an empty string for engagements with no related post. * @type int $3 WP_Post ID of the engagement post. * } * @return bool|WP_Error Returns `true` on success, `false` when the email is skipped, and a `WP_Error` when * the email has failed or is prevented. */ public function handle_email( $args ) { _deprecated_function( 'LLMS_Engagements::handle_email', '6.0.0', 'LLMS_Engagement_Handler::handle_email' ); $res = LLMS_Engagement_Handler::handle_email( $args ); if ( true === $res ) { return $res; } // The new handler returns an array of errors in favor of a single error. Retain the initial return type for this deprecated version. return $res[0]; } /** * Parse incoming hook / callback data to determine if an engagement should be triggered from a given hook. * * @since 6.0.0 * @since 6.6.0 Fixed an issue where the `lifterlms_external_engagement_query_arguments` filter * would not trigger if a 3rd party registered a trigger hook. * * @param string $action Action hook name. * @param array $args Array of arguments passed to the callback function. * @return array { * An associative array of parsed data used to trigger the engagement. * * @type string $trigger_type The name of the engagement trigger. See `llms_get_engagement_triggers()` for a list of valid triggers. * @type int $user_id The WP_User ID of the user who the engagement is being awarded or sent to. * @type int $related_post_id The WP_Post ID of a related post. * } */ private function parse_hook( $action, $args ) { $parsed = array( 'trigger_type' => null, 'user_id' => null, 'related_post_id' => null, ); /** * Allows 3rd parties to hook into the core engagement system by parsing data passed to the hook. * * @since 2.3.0 * * @param array $parsed { * An associative array of parsed data used to trigger the engagement. * * @type string $trigger_type (Required) The name of the engagement trigger. See `llms_get_engagement_triggers()` for a list of valid triggers. * @type int $user_id (Required) The WP_User ID of the user who the engagement is being awarded or sent to. * @type int $related_post_id (Optional) The WP_Post ID of a related post. * } * @param string $action The name of the hook which triggered the engagement. * @param array $args The original arguments provided by the triggering hook. */ $filtered_parsed = apply_filters( 'lifterlms_external_engagement_query_arguments', $parsed, $action, $args ); // If valid, return the filtered parsed data. if ( isset( $filtered_parsed['trigger_type'] ) && isset( $filtered_parsed['user_id'] ) ) { return $filtered_parsed; } // Verify that the action is a supported hook. if ( ! in_array( $action, $this->get_trigger_hooks(), true ) ) { return $parsed; } // The user registration action doesn't have a related post id. $related_post_id = isset( $args[1] ) && is_numeric( $args[1] ) ? absint( $args[1] ) : ''; $parsed['user_id'] = absint( $args[0] ); $parsed['trigger_type'] = $this->parse_hook_find_trigger_type( $action, $related_post_id ); $parsed['related_post_id'] = $related_post_id; return $parsed; } /** * Get the engagement trigger type based on the action and related post id * * @since 6.0.0 * * @param string $action Name of the triggering action hook. * @param int|string $related_post_id WP_Post ID of the related post or an empty string. * @return string */ private function parse_hook_find_trigger_type( $action, $related_post_id ) { $trigger_type = ''; switch ( $action ) { case 'llms_rest_student_registered': case 'lifterlms_created_person': case 'lifterlms_user_registered': $trigger_type = 'user_registration'; break; case 'lifterlms_course_completed': case 'lifterlms_course_track_completed': case 'lifterlms_lesson_completed': case 'lifterlms_section_completed': case 'lifterlms_quiz_completed': case 'lifterlms_quiz_passed': case 'lifterlms_quiz_failed': $trigger_type = str_replace( 'lifterlms_', '', $action ); break; case 'llms_user_added_to_membership_level': case 'llms_user_enrolled_in_course': $trigger_type = str_replace( 'llms_', '', get_post_type( $related_post_id ) ) . '_enrollment'; break; case 'lifterlms_access_plan_purchased': case 'lifterlms_product_purchased': $trigger_type = str_replace( 'llms_', '', get_post_type( $related_post_id ) ) . '_purchased'; break; } return $trigger_type; } /** * Handles all actions that could potentially trigger an engagement * * It will fire or schedule the actions after gathering all necessary data. * * @since 2.3.0 * @since 3.11.0 Unknown. * @since 3.39.0 Treat also `llms_rest_student_registered` action. * @since 6.0.0 Major refactor to reduce code complexity. * * @return void */ public function maybe_trigger_engagement() { // Parse incoming hook data. $hook = $this->parse_hook( current_filter(), func_get_args() ); // We need a user and a trigger to proceed, related_post is optional though. if ( ! $hook['user_id'] || ! $hook['trigger_type'] ) { return; } // Gather triggerable engagements matching the supplied criteria. $engagements = $this->get_engagements( $hook['trigger_type'], $hook['related_post_id'] ); // Loop through the retrieved engagements and trigger them. foreach ( $engagements as $engagement ) { $handler = $this->parse_engagement( $engagement, $hook ); $this->trigger_engagement( $handler, $engagement->delay ); } } /** * Parse engagement objects from the DB and return data needed to trigger the engagements. * * @since 6.0.0 * @since 6.6.0 Fixed an issue where the `lifterlms_external_engagement_handler_arguments` filter * would not trigger if a 3rd party registered an engagement type. * * @param object $engagement The engagement object from the `get_engagements()` query. * @param array $trigger_data Parsed hook data from `parse_hook()`. * @return array { * An associative array of parsed data used to trigger the engagement. * * @type string $handler_action Hook name of the action that will handle awarding the sending the engagement. * @type array $handler_args Arguments passed to the `$handler_action` callback. * } */ private function parse_engagement( $engagement, $trigger_data ) { $parsed = array( 'handler_action' => null, 'handler_args' => null, ); /** * Enable 3rd parties to parse custom engagement types. * * @since 2.3.0 * * @param array $parsed { * An associative array of parsed data used to trigger the engagement. * * @type string $handler_action (Required) Hook name of the action that will handle awarding the sending the engagement. * @type array $handler_args (Required) Arguments passed to the `$handler_action` callback. * } * @param object $engagement The engagement object from the `get_engagements()` query. * @param int $user_id WP_User ID who will be awarded the engagement. * @param int $related_post_id WP_Post ID of the related post. * @param string $event_type The type of engagement event. */ $filtered_parsed = apply_filters( 'lifterlms_external_engagement_handler_arguments', $parsed, $engagement, $trigger_data['user_id'], $trigger_data['related_post_id'], $engagement->event_type ); // If valid, return the filtered parsed data. if ( isset( $filtered_parsed['handler_action'] ) && isset( $filtered_parsed['handler_args'] ) ) { return $filtered_parsed; } // Verify that the engagement event type is supported. if ( ! array_key_exists( $engagement->event_type, llms_get_engagement_types() ) ) { return $parsed; } $parsed['handler_args'] = array( $trigger_data['user_id'], $engagement->engagement_id, $trigger_data['related_post_id'], absint( $engagement->trigger_id ), ); /** * @todo Fix this * * If there's no related post id we have to send one anyway for certs to work. * * This would only be for registration events @ version 2.3.0 so we pass the engagement_id twice until we find a better solution. */ if ( 'certificate' === $engagement->event_type && empty( $parsed['handler_args'][2] ) ) { $parsed['handler_args'][2] = $parsed['handler_args'][1]; } $parsed['handler_action'] = sprintf( 'lifterlms_engagement_%1$s_%2$s', 'email' === $engagement->event_type ? 'send' : 'award', $engagement->event_type ); return $parsed; } /** * Triggers or schedules an engagement * * @since 6.0.0 * * @param array $data Handler data from `parse_engagement()`. * @param int $delay The engagement send delay (in days). * @return void */ private function trigger_engagement( $data, $delay ) { // Can't proceed without an action and a handler. if ( empty( $data['handler_action'] ) || empty( $data['handler_args'] ) ) { return; } // If we have a delay, schedule the engagement handler. $delay = absint( $delay ); if ( $delay ) { as_schedule_single_action( time() + ( DAY_IN_SECONDS * $delay ), $data['handler_action'], array( $data['handler_args'] ), ! empty( $data['handler_args'][3] ) ? $this->get_delayed_group_id( $data['handler_args'][3] ) : null ); } else { /** * Skip processing checks for immediate engagements. * * We know the user exists (because they're currently logged in) and we don't have to run * publish/existence checks on all the related posts because the `get_engagement()` query takes care * of that already. */ add_filter( 'llms_skip_engagement_processing_checks', '__return_true' ); do_action( $data['handler_action'], $data['handler_args'] ); remove_filter( 'llms_skip_engagement_processing_checks', '__return_true' ); } } /** * Unschedule all scheduled actions for a delayed engagement * * This is the callback function for deleted engagement posts. * * The `deleted_post` action param `$post` has been added since WordPress 5.5.0. * * @since 6.0.0 * * @param int $post_id WP_Post ID. * @param WP_Post|null $post Post object of the deleted post. * @return void */ public function unschedule_delayed_engagements( $post_id, $post = null ) { // @todo Remove compatibility with WP < 5.5 when bumping the minimum WP required version to 5.5+ $post_type = $post ? $post->post_type : get_post_type( $post_id ); if ( 'llms_engagement' === $post_type ) { as_unschedule_all_actions( '', array(), $this->get_delayed_group_id( $post_id ) ); } } /** * Log debug data to the WordPress debug.log file * * @since 2.7.9 * @since 3.12.0 Unknown. * @deprecated 6.0.0 Engagement debug logging is removed. Use `llms_log()` directly instead. * * @param mixed $log Data to write to the log. * @return void */ public function log( $log ) { _deprecated_function( 'LLMS_Engagements::log()', '6.0.0', 'llms_log()' ); if ( $this->debug ) { llms_log( $log, 'engagements' ); } } }
Expand full source code Collapse full source code View on GitHub
Methods Methods
- __construct — Constructor
- add_actions — Register all actions that trigger engagements
- get_delayed_group_id — Retrieve a group id used when scheduling delayed engagement action triggers.
- get_engagements — Retrieve engagements based on the trigger type
- get_trigger_hooks — Retrieve a list of hooks that trigger engagements to be awarded.
- handle_achievement — Award an achievement — deprecated
- handle_certificate — Award a certificate — deprecated
- handle_email — Send an email engagement — deprecated
- init — Include engagement types (excluding email) — deprecated
- instance — Create instance of class
- log — Log debug data to the WordPress debug.log file — deprecated
- maybe_trigger_engagement — Handles all actions that could potentially trigger an engagement
- parse_engagement — Parse engagement objects from the DB and return data needed to trigger the engagements.
- parse_hook — Parse incoming hook / callback data to determine if an engagement should be triggered from a given hook.
- parse_hook_find_trigger_type — Get the engagement trigger type based on the action and related post id
- trigger_engagement — Triggers or schedules an engagement
- unschedule_delayed_engagements — Unschedule all scheduled actions for a delayed engagement
Changelog Changelog
Version | Description |
---|---|
6.0.0 | Changes:
|
5.3.0 | Replace singleton code with LLMS_Trait_Singleton . |
3.30.3 | Fixed spelling errors. |
2.3.0 | Introduced. |