Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Groups Laravel Package

musonza/groups

A Laravel package for building group functionality into your app: create and manage groups, add members, send invitations, handle join requests, and control access with permissions. It provides Eloquent models, migrations, and relationships to integrate groups, members, and roles quickly.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish the package assets:

composer require musonza/groups

php artisan vendor:publish --provider="Musonza\Groups\GroupsServiceProvider"
# if the provider tag is versioned differently, list tags first:
# php artisan vendor:publish --list

php artisan migrate

Then add the Groupable trait to whichever model represents an authenticated user:

use Musonza\Groups\Traits\Groupable;

class User extends Authenticatable
{
    use Groupable;
}

Where to look first

  1. config/groups.php — this is the most important file. It maps the package's models (Group, Membership, Invitation, Post, request model) to your own classes. Overriding these before you have production data saves a painful migration later.
  2. The published migrations — confirm actual column names (name, description, privacy, user_id/group_id) and, importantly, that there are no unique indexes you were hoping for.
  3. Musonza\Groups\Traits\Groupable and the models/Group.php class — the public API is small and the README is thinner than the source, so read the methods you intend to call.
  4. The privacy values (open, closed, private) and how each one gates join/request/invite flows.

First use case — a closed group with membership and a post:

$group = $user->createGroup([
    'name'        => 'Laravel Kenya',
    'description' => 'Meetups and jobs',
    'privacy'     => 'closed',
]);

$group->addMembers([$alice, $bob]);

$user->publish($group, ['body' => 'First post']);

$group->members;          // Collection of users
$user->groups;            // Collection of groups the user belongs to
$group->posts;

That covers the 80% path: create, join, list members, publish. Requests, invitations, and leaving are the remaining verbs and are worth wiring up in the same pass so the flow is coherent.

Implementation Patterns

Keep controllers thin; put group operations behind actions. The package is a persistence layer, not an application. Wrap each operation so you can add authorization, notifications, and side effects in one place:

class JoinGroup
{
    public function __invoke(User $user, Group $group): void
    {
        DB::transaction(function () use ($user, $group) {
            if ($group->privacy === 'open') {
                $group->addMembers($user);
                // dispatch notification / increment counters here
            } else {
                $user->requestJoin($group);
            }
        });
    }
}

Authorize with Laravel policies, not the package. Nothing in the package knows about your authorization model. Create a GroupPolicy with view, join, post, invite, moderate abilities, and check both membership and privacy. Every controller method that touches a group should call $this->authorize(...) before calling the package.

Override the models via config early. Point config/groups.php at subclasses so you can add scopes, accessors, route keys, and relationships without patching vendor code:

// config/groups.php
'models' => [
    'group'      => App\Models\Group::class,
    'membership' => App\Models\Membership::class,
    'post'       => App\Models\GroupPost::class,
    // ...
],
class Group extends \Musonza\Groups\Models\Group
{
    public function getRouteKeyName(): string { return 'slug'; }

    public function scopeOpen(Builder $q): Builder
    {
        return $q->where('privacy', 'open');
    }
}

This is also where you add HasFactory, casts, and $fillable guardrails.

Eager load aggressively. Group pages naturally produce N+1s:

$group = Group::withCount('members')
    ->with(['posts' => fn ($q) => $q->latest()->limit(20), 'posts.user'])
    ->findOrFail($id);

Use withCount('members') for "12 members" in listings instead of loading the relation, and paginate members on a dedicated tab.

Normalize the three join paths into one UI concept. Open join, request approval, and invitation each have different package methods but should feel the same to a user. Model a small state machine (none → requested → member, none → invited → member) and map each package call onto it. Handle "already a member", "already requested", and "already invited" explicitly rather than relying on the package to dedupe.

Add events/observers yourself. Since the package does not dispatch rich domain events, hook Membership and Invitation creation via model observers (created, deleted) to trigger mail, Slack, or activity-log entries. This keeps notifications out of controllers and survives package upgrades.

Compose for feeds and directories. For a "my groups" page: $user->groups()->latest('memberships.created_at')->paginate(). For a discovery page: Group::open()->withCount('members')->latest()->paginate(). For an admin dashboard: Membership::where('group_id', $id)->with('user')->get().

Seed and test with your own factories. The package predates HasFactory conventions, so write GroupFactory, MembershipFactory helpers in your app and use RefreshDatabase. Test the privacy matrix explicitly: anonymous, non-member, member, group owner, for each of open|closed|private.

Gotchas and Tips

It is a Laravel 5-era package (last release Feb 2020). Expect friction on Laravel 8+: old-style $dates, possibly array casts, missing return types, and composer.json constraints that will refuse to install on modern Laravel. Options: composer require musonza/groups --ignore-platform-reqs (only if the code actually runs), fork it into your packages/ directory with a path repository, or vendor the models you need and drop the dependency. Because the codebase is small, forking is often cheaper than fighting constraints.

Migrations ship with no business constraints. Verify before relying on them: there may be no unique index on (group_id, user_id) for memberships or on pending requests/invitations. Add your own migration:

Schema::table('group_memberships', function (Blueprint $table) {
    $table->unique(['group_id', 'user_id']);
});

Also add foreign keys with onDelete('cascade') or you will accumulate orphaned memberships and posts when a group or user is deleted. If FKs aren't an option, delete children in a deleting model observer.

privacy is a free-text column. Nothing stops 'Closed' or 'public' from being written. Validate with Rule::in(['open', 'closed', 'private']) in form requests and consider a PHP enum/constant class, plus a DB check constraint if your engine supports it.

Swapping model classes is mostly safe, but config caching bites. If you change the models mapping, run php artisan config:clear. Any queued jobs or serialized models referencing old class names will fail to unserialize — drain or version queues around the switch. Do the swap before populating production data.

Check what addMembers accepts and returns before looping. It typically accepts a user or a collection/array, but verify behavior for duplicates and for already-members. Prefer a wrapped, idempotent call:

if (! $group->members()->whereKey($user->id)->exists()) {
    $group->addMembers($user);
}

Invitations and requests have no rate limiting or expiry. A user can spam invites; stale pending requests linger forever. Add throttling middleware, a expires_at column via your own migration, and a scheduled command to prune old rows. Also decide whether an invitation implies membership on accept (usually yes) and enforce that in one place.

Deleting a group is not a domain operation. Call $group->delete() inside a transaction and explicitly delete memberships, posts, requests, and invitations (or rely on FKs). Clear caches keyed by group ID at the same time.

No built-in notifications, activity feed, or moderation queue. Plan for them explicitly. Observers on Membership, Invitation, and the request model are the cleanest hooks; keep mailables queued and use ShouldQueue on listeners so group operations stay fast.

Debugging tip: when a relation returns empty unexpectedly, check the config mapping first — a mismatched model class in config/groups.php results in queries against the wrong table and silent-empty collections rather than exceptions. DB::listen() or Laravel Telescope will show you the actual table being hit.

Exit strategy. Given the package's age and low maintenance signal, keep your group domain code behind your own actions/services and policies, with the package models confined to the persistence layer. If you later replace it with a hand-rolled schema, only the config, migrations, and a handful of action classes should need to change.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource