New in version 0.9.80
The Groups module provides a comprehensive API for managing cohorts, teams, and group-based access. This guide covers the public API functions, hooks, and filters available for extending the Groups functionality.
Group Roles, Relationships, and Target-User Visibility in 1.1.0+
Since version 1.1.0, Groups is also the general relationship layer for member-to-member visibility. The default roles owner, leader, and member still exist, but admins can define custom site-wide group roles with labels, ranks, capabilities, and data scopes. Normal member and invite flows validate role slugs through GroupRoleDefinitionService, and owner is not assigned through regular member/invite UI.
Use GroupMemberRelationshipService for explicit viewer-to-target assignments, and use GroupRolePermissionService or UserDataVisibilityPermissionService before rendering another member’s learning data. Bricks dynamic tags and read-only elements can resolve the target user from Bricks user loops, including User loops filtered by Users: Group Membership, profile pages, then the current user fallback.
Checking Module Status
Before using Groups APIs, verify the module is active:
use BaselMedia\BricksMembers\Core\ModuleRegistry;
if ( ModuleRegistry::is_active( 'groups' ) ) {
// Groups functionality available
}
Helper Functions
GroupContextReader::get_instance()->get_user_active_group_for_post()
Get the user’s active group ID for a specific post context.
/**
* @param int $user_id User ID
* @param int $post_id Post ID
* @return int|null Group ID or null if no active group
*/
$group_id = \BaselMedia\BricksMembers\Modules\Groups\GroupContextReader::get_instance()
->get_user_active_group_for_post( $user_id, $post_id );
brm_get_user_groups()
Get all groups a user belongs to.
/**
* @param int $user_id User ID
* @param array $args Optional: status, role filters
* @return array Array of group arrays
*/
$groups = brm_get_user_groups( $user_id, array( 'status' => 'active' ) );
brm_get_user_groups() is the only public procedural Groups wrapper. For owned internal reads, use GroupService::get_group(), GroupService::is_group_member( $user_id, $group_id ), GroupService::get_user_group_role( $user_id, $group_id ), and GroupContextReader::group_matches_post_required_levels() or get_user_active_group_for_post().
GroupService Class
For more complex operations, use the GroupService singleton:
use BaselMedia\BricksMembers\Modules\Groups\GroupService;
$service = GroupService::get_instance();
Creating Groups
$group_id = $service->create_group( array(
'name' => 'Spring 2026 Cohort',
'owner_user_id' => get_current_user_id(),
'group_type' => 'cohort',
'total_seats' => 25,
'default_levels' => array( 1, 2 ),
'starts_at' => '2026-04-01 00:00:00',
'expires_at' => '2026-07-01 00:00:00',
) );
if ( is_wp_error( $group_id ) ) {
// Handle error
}
Updating Groups
$result = $service->update_group( $group_id, array(
'name' => 'Updated Name',
'total_seats' => 30,
) );
Archiving Groups
$service->archive_group( $group_id );
Member Management
// Add a member
$membership_id = $service->add_member( $group_id, $user_id, 'member' );
// Remove a member
$service->remove_member( $group_id, $user_id );
// Change role
$service->change_member_role( $group_id, $user_id, 'leader' );
// Transfer member to another group
$service->transfer_member( $user_id, $from_group_id, $to_group_id );
Getting Members
// Get all members
$members = $service->get_group_members( $group_id, array(
'status' => 'active',
'role' => 'member', // or any assignable custom role slug
) );
// Get a specific membership
$membership = $service->get_membership( $group_id, $user_id );
InvitationService Class
use BaselMedia\BricksMembers\Modules\Groups\InvitationService;
$invitation_service = InvitationService::get_instance();
Creating Invitations
// Email invitation (single-use, specific recipient)
$invitation_id = $invitation_service->create_email_invitation(
$group_id,
'student@example.com',
get_current_user_id(),
array(
'role' => 'member',
'expires_at' => '2026-04-01 00:00:00',
)
);
// Initial delivery is automatic through the Group Invitation Created
// automation when that workflow exists. Without that workflow, call the
// plaintext fallback explicitly.
if ( ! $invitation_service->is_invitation_email_sent_by_creation_automation() ) {
$invitation_service->send_invitation_email( $invitation_id );
}
// Join link (reusable, public)
$link_id = $invitation_service->create_join_link(
$group_id,
get_current_user_id(),
array(
'role' => 'member',
// For limited groups: must be <= current available seats.
// If omitted, defaults to available seats.
'max_uses' => 10,
'expires_at' => '2026-04-01 00:00:00',
)
);
// Get the shareable URL
$invitation = $invitation_service->get_invitation( $link_id );
$url = $invitation_service->get_claim_url( $invitation['token'] );
Validating and Claiming
// Validate before claiming
$validation = $invitation_service->validate_invitation( $token, $user_id );
if ( is_wp_error( $validation ) ) {
echo $validation->get_error_message();
return;
}
// Claim the invitation
$membership_id = $invitation_service->claim_invitation( $token, $user_id );
Managing Invitations
// Get by token
$invitation = $invitation_service->get_invitation_by_token( $token );
// Get all for a group
$invitations = $invitation_service->get_group_invitations( $group_id, array(
'status' => 'pending',
'type' => 'email', // or 'link'
) );
// Delete permanently
$invitation_service->delete_invitation( $invitation_id );
// Resend email. When an automation owns delivery, this re-dispatches the
// Group Invitation Created trigger instead of sending plaintext fallback.
$invitation_service->send_invitation_email( $invitation_id, true );
SeatRuleService Class
For purchase matching from native Payments, WooCommerce, FluentCart, SureCart, or generic webhooks:
use BaselMedia\BricksMembers\Modules\Groups\SeatRuleService;
$seat_service = SeatRuleService::get_instance();
// Create a rule
$rule_id = $seat_service->create_rule( array(
'product_ref' => 'team-license-pro',
'provider' => 'webhook',
'group_type' => 'team',
'levels_to_grant' => array( 1, 2 ),
'total_seats' => 5,
'create_mode' => 'per_purchase',
'fulfillment_mode' => 'auto_create_group',
) );
// Process a purchase. This records a seat entitlement and creates or tops up
// the buyer's group according to the rule fulfillment mode.
$result = $seat_service->process_purchase(
'team-license-pro', // product_ref
'', // variation_ref
$owner_user_id, // who purchased
'order-123', // unique reference
'', // optional initial group name
'', // optional group start date
'webhook', // source
1 // purchase quantity
);
// $result contains: entitlement_id, group_id, seats_granted, fulfillment_mode
GroupSeatPoolService Class
GroupSeatPoolService is the canonical owner for purchase-backed group seat entitlements and post-purchase allocation. Seat rules decide whether a purchase matches; this service records the idempotent purchase ledger in wp_brm_group_seat_entitlements, creates or tops up the buyer’s group, reads owner pool summaries, and moves seats between owned groups.
use BaselMedia\BricksMembers\Modules\Groups\GroupSeatPoolService;
$pool_service = GroupSeatPoolService::get_instance();
$summary = $pool_service->get_pool_summary( $owner_user_id, $seat_rule_id );
$new_group_id = $pool_service->create_group_from_pool(
$owner_user_id,
$seat_rule_id,
'Acme Sales Team',
5,
$source_group_id // optional: split movable seats from an existing owned group
);
$pool_service->move_group_seats( $owner_user_id, $source_group_id, $target_group_id, 2 );
Allocation methods require the current owner relationship, keep groups within the same seat_rule_id, and block any reduction below used_seats + reserved_seats. Purchase fulfillment and allocation mutations use a per-buyer/per-rule concurrency lock.
Member Chat Integration
When the Member Chat module is enabled, Groups becomes the lifecycle owner for primary group rooms.
brm_event_group_createdis used to create or resolve the primary group roombrm_event_group_member_addedsynchronizes the new user into the roombrm_event_group_member_removedremoves the user from the group’s chat roomsbrm_event_group_archivedarchives the group’s related chat rooms
The current primary-room contract is one room per group with scope = group and room_kind = primary_group in the Member Chat tables.
Actions (Hooks)
Group Lifecycle
// After group is created
add_action( 'brm_event_group_created', function( \BaselMedia\BricksMembers\Core\Event $event ) {
$context = $event->get_context();
}, 10, 1 );
// After group is archived
add_action( 'brm_event_group_archived', function( \BaselMedia\BricksMembers\Core\Event $event ) {
$group_id = $event->get_resource_id();
}, 10, 1 );
Member Lifecycle
// After member joins
add_action( 'brm_event_group_member_added', function( \BaselMedia\BricksMembers\Core\Event $event ) {
$user_id = $event->get_user_id();
$group_id = $event->get_resource_id();
$role = $event->get_context()['role'] ?? '';
}, 10, 1 );
// After member leaves/removed
add_action( 'brm_event_group_member_removed', function( \BaselMedia\BricksMembers\Core\Event $event ) {
$user_id = $event->get_user_id();
$group_id = $event->get_resource_id();
}, 10, 1 );
// After member transfer
add_action( 'brm_event_group_member_transferred', function( \BaselMedia\BricksMembers\Core\Event $event ) {
$context = $event->get_context();
}, 10, 1 );
Invitation Lifecycle
// After invitation created
add_action( 'brm_invitation_created', function( $invitation_id, $group_id, $email, $invited_by ) {
// Custom notifications, logging
}, 10, 4 );
// After invitation claimed
add_action( 'brm_invitation_claimed', function( $invitation_id, $user_id, $group_id, $invitation ) {
// Welcome sequence, analytics
}, 10, 4 );
// After invitation email sent
add_action( 'brm_invitation_email_sent', function( $invitation_id, $invitation ) {
// Track plaintext fallback sends and automation-backed resend dispatches
}, 10, 2 );
Filters
Customizing Invitation Emails
// Custom subject
add_filter( 'brm_invitation_email_subject', function( $subject, $invitation, $group ) {
return sprintf( 'Join %s - Exclusive Invitation', $group['name'] );
}, 10, 3 );
// Custom message body
add_filter( 'brm_invitation_email_message', function( $message, $invitation, $group, $claim_url ) {
return "You've been personally invited to join {$group['name']}.\n\n"
. "Click here to accept: {$claim_url}\n\n"
. "This is a limited opportunity!";
}, 10, 4 );
Customizing Claim Redirects
Since 1.1.0, the global invitation redirect defaults and built-in claim notice toggle are owned by the Groups admin Settings tab, not the general Settings screen.
For normal site setup, configure the default guest redirect page, post-claim redirect page, and built-in claim notice in BricksMembers → Groups → Settings. Use the group detail Settings tab for per-group overrides. The filters below are for custom code that needs to override the resolved destination.
// After successful claim
add_filter( 'brm_invitation_claim_success_redirect', function( $url, $group_id, $user_id ) {
return home_url( '/welcome-to-group/?group=' . $group_id );
}, 10, 3 );
// After failed claim
add_filter( 'brm_invitation_claim_error_redirect', function( $url, $error, $token ) {
return home_url( '/invitation-error/?code=' . $error->get_error_code() );
}, 10, 3 );
Customizing Claim URL Base
add_filter( 'brm_invitation_claim_base_url', function( $url ) {
return home_url( '/join/' ); // Custom join page
} );
Bricks Integration
Custom Conditions
Register custom Bricks conditions based on groups:
add_filter( 'bricks/conditions/options', function( $options ) {
$options[] = array(
'key' => 'brm_custom_group_condition',
'label' => 'Custom Group Check',
'group' => 'brm',
'compare' => array(
'=' => 'equals',
'!=' => 'not equals',
),
'value' => array(
'type' => 'text',
'placeholder' => 'Group type',
),
);
return $options;
}, 15 );
add_filter( 'bricks/conditions/result', function( $result, $key, $condition ) {
if ( 'brm_custom_group_condition' !== $key ) {
return $result;
}
$group_id = \BaselMedia\BricksMembers\Modules\Groups\GroupContextReader::get_instance()
->get_user_active_group_for_post( get_current_user_id(), get_the_ID() );
if ( ! $group_id ) {
return false;
}
$group = \BaselMedia\BricksMembers\Modules\Groups\GroupService::get_instance()
->get_group( (int) $group_id );
$compare_value = $condition['value'] ?? '';
$compare = $condition['compare'] ?? '=';
$matches = $group['group_type'] === $compare_value;
return '=' === $compare ? $matches : ! $matches;
}, 10, 3 );
Group Context and Demo Preview
Group dynamic tags and conditions are not limited to brmUserGroupsQuery rows. They resolve from the current BRM group context, which can come from a group query row, an explicit dashboard link/group request context, invitation claim state, or the current user’s matching active group for the queried post. If the viewer has only one matching active group, tags such as {brm_group:name} can resolve on a normal dashboard page without a visible BRM query loop.
Dashboard demo context is owned by GroupDemoDataService. The URL parameter is brm_group_demo, with modes owner, leader, member, full, and no_seats. Demo mode is ignored unless the current user has RoleCapabilityRegistry::CAP_MANAGE_GROUPS; non-admin frontend users must never receive demo group context.
Custom Dynamic Tags
add_filter( 'bricks/dynamic_tags_list', function( $tags ) {
$tags[] = array(
'name' => '{brm_group:custom_field}',
'label' => 'Group Custom Field',
'group' => 'BricksMembers',
);
return $tags;
} );
add_filter( 'bricks/dynamic_data/render_tag', function( $value, $tag, $post, $context ) {
if ( strpos( $tag, '{brm_group:custom_field}' ) === false ) {
return $value;
}
$group_id = \BaselMedia\BricksMembers\Modules\Groups\GroupContextReader::get_instance()
->get_user_active_group_for_post( get_current_user_id(), get_the_ID() );
if ( ! $group_id ) {
return '';
}
// Return your custom field value
return get_post_meta( $group_id, '_custom_group_field', true );
}, 10, 4 );
Database Schema
The Groups module uses four custom tables:
brm_groups
id– Primary keyname– Group nameowner_user_id– WordPress user ID of ownergroup_type– Type (cohort, team, etc.)total_seats,used_seats– Seat trackingreserved_seats– Pending email invite seat reservationsgrantable_levels,default_levels– JSON arraysinvite_guest_redirect_page_id,invite_post_claim_redirect_page_id– Optional redirect overridesstatus– active, archivedstarts_at,expires_at– Dates
brm_group_members
id– Primary keygroup_id– FK to brm_groupsuser_id– WordPress user IDrole– owner, leader, member, or a custom group role sluggranted_levels– JSON array of level IDsstatus– active, removed, transferredjoined_at,last_active_at– Timestamps
brm_seat_rules
id– Primary keyproduct_ref– Product identifierprovider– webhook, surecart, etc.group_type,total_seats– Group settingslevels_to_grant– JSON arraycreate_mode– per_purchase, shared
brm_group_invitations
id– Primary keygroup_id– FK to brm_groupstoken– Unique claim tokentype– email, linkemail– Recipient email (for email type)role– Role to assignmax_uses,use_count– Usage limitsstatus– pending, claimed, active, exhausted, expired, revoked
Best Practices
- Always check module status before using Groups APIs
- Use helper functions for simple lookups, services for complex operations
- Handle WP_Error returns from service methods
- Use filters to customize behavior rather than overriding
- Clear caches after direct database operations (use MutationContract)