Skip to:
Content

BuddyPress.org

Changeset 11888


Ignore:
Timestamp:
03/05/2018 12:57:24 PM (7 years ago)
Author:
boonebgorges
Message:

Move notification-grouping logic to MySQL.

The notification-grouping logic required to show the collapsed Notifications
toolbar - "You have 3 pending friend requests", etc - is currently done in
PHP, in a manner that requires all notifications to be loaded into memory
and processed. On sites where users have large numbers of notifications, this
operation requires extensive resources. By leaving the grouping to MySQL,
we can speed up processing in almost all situations.

This changeset also adds specific caching for grouped notifications.

Props m_uysl.
See #7130.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/bp-notifications/bp-notifications-cache.php

    r11851 r11888  
    4545    wp_cache_delete( 'all_for_user_' . $user_id, 'bp_notifications' );
    4646    wp_cache_delete( $user_id, 'bp_notifications_unread_count' );
     47    wp_cache_delete( $user_id, 'bp_notifications_grouped_notifications' );
    4748}
    4849
  • trunk/src/bp-notifications/bp-notifications-functions.php

    r11851 r11888  
    164164
    165165/**
     166 * Get a user's unread notifications, grouped by component and action.
     167 *
     168 * This function returns a list of notifications collapsed by component + action.
     169 * See BP_Notifications_Notification::get_grouped_notifications_for_user() for
     170 * more details.
     171 *
     172 * @since 3.0.0
     173 *
     174 * @param int $user_id ID of the user whose notifications are being fetched.
     175 * @return array $notifications
     176 */
     177function bp_notifications_get_grouped_notifications_for_user( $user_id = 0 ) {
     178    if ( empty( $user_id ) ) {
     179        $user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
     180    }
     181
     182    $notifications = wp_cache_get( $user_id, 'bp_notifications_grouped_notifications' );
     183    if ( false === $notifications ) {
     184        $notifications = BP_Notifications_Notification::get_grouped_notifications_for_user( $user_id );
     185        wp_cache_set( $user_id, $notifications, 'bp_notifications_grouped_notifications' );
     186    }
     187
     188    return $notifications;
     189}
     190
     191/**
    166192 * Get notifications for a specific user.
    167193 *
     
    174200 */
    175201function bp_notifications_get_notifications_for_user( $user_id, $format = 'string' ) {
    176 
    177     // Setup local variables.
    178202    $bp = buddypress();
    179203
    180     // Get notifications (out of the cache, or query if necessary).
    181     $notifications         = bp_notifications_get_all_notifications_for_user( $user_id );
    182     $grouped_notifications = array(); // Notification groups.
    183     $renderable            = array(); // Renderable notifications.
    184 
    185     // Group notifications by component and component_action and provide totals.
    186     for ( $i = 0, $count = count( $notifications ); $i < $count; ++$i ) {
    187         $notification = $notifications[$i];
    188         $grouped_notifications[$notification->component_name][$notification->component_action][] = $notification;
    189     }
    190 
    191     // Bail if no notification groups.
    192     if ( empty( $grouped_notifications ) ) {
    193         return false;
    194     }
     204    $notifications = bp_notifications_get_grouped_notifications_for_user( $user_id );
    195205
    196206    // Calculate a renderable output for each notification type.
    197     foreach ( $grouped_notifications as $component_name => $action_arrays ) {
    198 
     207    foreach ( $notifications as $notification_item ) {
     208
     209        $component_name = $notification_item->component_name;
    199210        // We prefer that extended profile component-related notifications use
    200211        // the component_name of 'xprofile'. However, the extended profile child
    201212        // object in the $bp object is keyed as 'profile', which is where we need
    202213        // to look for the registered notification callback.
    203         if ( 'xprofile' == $component_name ) {
     214        if ( 'xprofile' == $notification_item->component_name ) {
    204215            $component_name = 'profile';
    205216        }
    206217
    207         // Skip if group is empty.
    208         if ( empty( $action_arrays ) ) {
    209             continue;
    210         }
    211 
    212         // Loop through each actionable item and try to map it to a component.
    213         foreach ( (array) $action_arrays as $component_action_name => $component_action_items ) {
    214 
    215             // Get the number of actionable items.
    216             $action_item_count = count( $component_action_items );
    217 
    218             // Skip if the count is less than 1.
    219             if ( $action_item_count < 1 ) {
    220                 continue;
     218        // Callback function exists.
     219        if ( isset( $bp->{$component_name}->notification_callback ) && is_callable( $bp->{$component_name}->notification_callback ) ) {
     220
     221            // Function should return an object.
     222            if ( 'object' === $format ) {
     223
     224                // Retrieve the content of the notification using the callback.
     225                $content = call_user_func( $bp->{$component_name}->notification_callback, $notification_item->component_action, $notification_item->item_id, $notification_item->secondary_item_id, $notification_item->total_count, 'array', $notification_item->id );
     226
     227                // Create the object to be returned.
     228                $notification_object = $notification_item;
     229
     230                // Minimal backpat with non-compatible notification
     231                // callback functions.
     232                if ( is_string( $content ) ) {
     233                    $notification_object->content = $content;
     234                    $notification_object->href    = bp_loggedin_user_domain();
     235                } else {
     236                    $notification_object->content = $content['text'];
     237                    $notification_object->href    = $content['link'];
     238                }
     239
     240                $renderable[] = $notification_object;
     241
     242                // Return an array of content strings.
     243            } else {
     244                $content      = call_user_func( $bp->{$component_name}->notification_callback, $notification_item->component_action, $notification_item->item_id, $notification_item->secondary_item_id, $notification_item->total_count, 'string', $notification_item->id );
     245                $renderable[] = $content;
    221246            }
    222247
    223             // Callback function exists.
    224             if ( isset( $bp->{$component_name}->notification_callback ) && is_callable( $bp->{$component_name}->notification_callback ) ) {
    225 
    226                 // Function should return an object.
    227                 if ( 'object' === $format ) {
    228 
    229                     // Retrieve the content of the notification using the callback.
    230                     $content = call_user_func(
    231                         $bp->{$component_name}->notification_callback,
    232                         $component_action_name,
    233                         $component_action_items[0]->item_id,
    234                         $component_action_items[0]->secondary_item_id,
    235                         $action_item_count,
    236                         'array',
    237                         $component_action_items[0]->id
    238                     );
    239 
    240                     // Create the object to be returned.
    241                     $notification_object = $component_action_items[0];
    242 
    243                     // Minimal backpat with non-compatible notification
    244                     // callback functions.
    245                     if ( is_string( $content ) ) {
    246                         $notification_object->content = $content;
    247                         $notification_object->href    = bp_loggedin_user_domain();
    248                     } else {
    249                         $notification_object->content = $content['text'];
    250                         $notification_object->href    = $content['link'];
    251                     }
    252 
    253                     $renderable[] = $notification_object;
     248            // @deprecated format_notification_function - 1.5
     249        } elseif ( isset( $bp->{$component_name}->format_notification_function ) && function_exists( $bp->{$component_name}->format_notification_function ) ) {
     250            $renderable[] = call_user_func( $bp->{$component_name}->notification_callback, $notification_item->component_action, $notification_item->item_id, $notification_item->secondary_item_id, $notification_item->total_count );
     251
     252            // Allow non BuddyPress components to hook in.
     253        } else {
     254
     255            // The array to reference with apply_filters_ref_array().
     256            $ref_array = array(
     257                $notification_item->component_action,
     258                $notification_item->item_id,
     259                $notification_item->secondary_item_id,
     260                $notification_item->total_count,
     261                $format,
     262                $notification_item->component_action, // Duplicated so plugins can check the canonical action name.
     263                $component_name,
     264                $notification_item->id,
     265            );
     266
     267            // Function should return an object.
     268            if ( 'object' === $format ) {
     269
     270                /**
     271                 * Filters the notification content for notifications created by plugins.
     272                 * If your plugin extends the {@link BP_Component} class, you should use the
     273                 * 'notification_callback' parameter in your extended
     274                 * {@link BP_Component::setup_globals()} method instead.
     275                 *
     276                 * @since 1.9.0
     277                 * @since 2.6.0 Added $component_action_name, $component_name, $id as parameters.
     278                 *
     279                 * @param string $content               Component action. Deprecated. Do not do checks against this! Use
     280                 *                                      the 6th parameter instead - $component_action_name.
     281                 * @param int    $item_id               Notification item ID.
     282                 * @param int    $secondary_item_id     Notification secondary item ID.
     283                 * @param int    $action_item_count     Number of notifications with the same action.
     284                 * @param string $format                Format of return. Either 'string' or 'object'.
     285                 * @param string $component_action_name Canonical notification action.
     286                 * @param string $component_name        Notification component ID.
     287                 * @param int    $id                    Notification ID.
     288                 *
     289                 * @return string|array If $format is 'string', return a string of the notification content.
     290                 *                      If $format is 'object', return an array formatted like:
     291                 *                      array( 'text' => 'CONTENT', 'link' => 'LINK' )
     292                 */
     293                $content = apply_filters_ref_array( 'bp_notifications_get_notifications_for_user', $ref_array );
     294
     295                // Create the object to be returned.
     296                $notification_object = $notification_item;
     297
     298                // Minimal backpat with non-compatible notification
     299                // callback functions.
     300                if ( is_string( $content ) ) {
     301                    $notification_object->content = $content;
     302                    $notification_object->href    = bp_loggedin_user_domain();
     303                } else {
     304                    $notification_object->content = $content['text'];
     305                    $notification_object->href    = $content['link'];
     306                }
     307
     308                $renderable[] = $notification_object;
    254309
    255310                // Return an array of content strings.
    256                 } else {
    257                     $content      = call_user_func( $bp->{$component_name}->notification_callback, $component_action_name, $component_action_items[0]->item_id, $component_action_items[0]->secondary_item_id, $action_item_count, 'string', $component_action_items[0]->id );
    258                     $renderable[] = $content;
    259                 }
    260 
    261             // @deprecated format_notification_function - 1.5
    262             } elseif ( isset( $bp->{$component_name}->format_notification_function ) && function_exists( $bp->{$component_name}->format_notification_function ) ) {
    263                 $renderable[] = call_user_func( $bp->{$component_name}->format_notification_function, $component_action_name, $component_action_items[0]->item_id, $component_action_items[0]->secondary_item_id, $action_item_count );
    264 
    265             // Allow non BuddyPress components to hook in.
    266311            } else {
    267312
    268                 // The array to reference with apply_filters_ref_array().
    269                 $ref_array = array(
    270                     $component_action_name,
    271                     $component_action_items[0]->item_id,
    272                     $component_action_items[0]->secondary_item_id,
    273                     $action_item_count,
    274                     $format,
    275                     $component_action_name, // Duplicated so plugins can check the canonical action name.
    276                     $component_name,
    277                     $component_action_items[0]->id
    278                 );
    279 
    280                 // Function should return an object.
    281                 if ( 'object' === $format ) {
    282 
    283                     /**
    284                      * Filters the notification content for notifications created by plugins.
    285                      *
    286                      * If your plugin extends the {@link BP_Component} class, you should use the
    287                      * 'notification_callback' parameter in your extended
    288                      * {@link BP_Component::setup_globals()} method instead.
    289                      *
    290                      * @since 1.9.0
    291                      * @since 2.6.0 Added $component_action_name, $component_name, $id as parameters.
    292                      *
    293                      * @param string $content               Component action. Deprecated. Do not do checks against this! Use
    294                      *                                      the 6th parameter instead - $component_action_name.
    295                      * @param int    $item_id               Notification item ID.
    296                      * @param int    $secondary_item_id     Notification secondary item ID.
    297                      * @param int    $action_item_count     Number of notifications with the same action.
    298                      * @param string $format                Format of return. Either 'string' or 'object'.
    299                      * @param string $component_action_name Canonical notification action.
    300                      * @param string $component_name        Notification component ID.
    301                      * @param int    $id                    Notification ID.
    302                      *
    303                      * @return string|array If $format is 'string', return a string of the notification content.
    304                      *                      If $format is 'object', return an array formatted like:
    305                      *                      array( 'text' => 'CONTENT', 'link' => 'LINK' )
    306                      */
    307                     $content = apply_filters_ref_array( 'bp_notifications_get_notifications_for_user', $ref_array );
    308 
    309                     // Create the object to be returned.
    310                     $notification_object = $component_action_items[0];
    311 
    312                     // Minimal backpat with non-compatible notification
    313                     // callback functions.
    314                     if ( is_string( $content ) ) {
    315                         $notification_object->content = $content;
    316                         $notification_object->href    = bp_loggedin_user_domain();
    317                     } else {
    318                         $notification_object->content = $content['text'];
    319                         $notification_object->href    = $content['link'];
    320                     }
    321 
    322                     $renderable[] = $notification_object;
    323 
    324                 // Return an array of content strings.
    325                 } else {
    326 
    327                     /** This filters is documented in bp-notifications/bp-notifications-functions.php */
    328                     $renderable[] = apply_filters_ref_array( 'bp_notifications_get_notifications_for_user', $ref_array );
    329                 }
     313                /** This filters is documented in bp-notifications/bp-notifications-functions.php */
     314                $renderable[] = apply_filters_ref_array( 'bp_notifications_get_notifications_for_user', $ref_array );
    330315            }
    331316        }
  • trunk/src/bp-notifications/classes/class-bp-notifications-notification.php

    r11840 r11888  
    11321132        return self::update( $update_args, $where_args );
    11331133    }
     1134
     1135    /**
     1136     * Get a user's unread notifications, grouped by component and action.
     1137     *
     1138     * Multiple notifications of the same type (those that share the same component_name
     1139     * and component_action) are collapsed for formatting as "You have 5 pending
     1140     * friendship requests", etc. See bp_notifications_get_notifications_for_user().
     1141     * For a full-fidelity list of user notifications, use
     1142     * bp_notifications_get_all_notifications_for_user().
     1143     *
     1144     * @since 3.0.0
     1145     *
     1146     * @param int $user_id ID of the user whose notifications are being fetched.
     1147     * @return array Notifications items for formatting into a list.
     1148     */
     1149    public static function get_grouped_notifications_for_user( $user_id ) {
     1150        global $wpdb;
     1151
     1152        // Load BuddyPress.
     1153        $bp = buddypress();
     1154
     1155        // SELECT.
     1156        $select_sql = "SELECT id, user_id, item_id, secondary_item_id, component_name, component_action, date_notified, is_new, COUNT(id) as total_count ";
     1157
     1158        // FROM.
     1159        $from_sql = "FROM {$bp->notifications->table_name} n ";
     1160
     1161        // WHERE.
     1162        $where_sql = self::get_where_sql( array(
     1163            'user_id'        => $user_id,
     1164            'is_new'         => 1,
     1165            'component_name' => bp_notifications_get_registered_components(),
     1166        ), $select_sql, $from_sql );
     1167
     1168        // GROUP
     1169        $group_sql = "GROUP BY user_id, component_name, component_action";
     1170
     1171        // SORT
     1172        $order_sql = "ORDER BY date_notified desc";
     1173
     1174        // Concatenate query parts.
     1175        $sql = "{$select_sql} {$from_sql} {$where_sql} {$group_sql} {$order_sql}";
     1176
     1177        // Return the queried results.
     1178        return $wpdb->get_results( $sql );
     1179    }
    11341180}
  • trunk/tests/phpunit/testcases/notifications/functions.php

    r11851 r11888  
    404404        $this->n_args = compact( 'action', 'item_id', 'secondary_item_id', 'total_items', 'id', 'format' );
    405405    }
     406
     407    /**
     408     * @group cache
     409     * @ticket BP7130
     410     */
     411    public function test_get_grouped_notifications_for_user_cache_invalidation() {
     412        $u = self::factory()->user->create();
     413
     414        $n1 = self::factory()->notification->create( array(
     415            'component_name'    => 'activity',
     416            'component_action'  => 'new_at_mention',
     417            'item_id'           => 99,
     418            'user_id'           => $u,
     419        ) );
     420
     421        // Prime cache.
     422        $found = bp_notifications_get_grouped_notifications_for_user( $u );
     423        $this->assertEquals( 1, $found[0]->total_count );
     424
     425        $n2 = self::factory()->notification->create( array(
     426            'component_name'    => 'activity',
     427            'component_action'  => 'new_at_mention',
     428            'item_id'           => 100,
     429            'user_id'           => $u,
     430        ) );
     431
     432        $found = bp_notifications_get_grouped_notifications_for_user( $u );
     433        $this->assertEquals( 2, $found[0]->total_count );
     434    }
    406435}
Note: See TracChangeset for help on using the changeset viewer.