liberusoftware/profiles
A Laravel package for managing user profiles, providing a clean, configurable foundation for profile data, relationships, and common profile workflows. It integrates with Eloquent models and is designed to drop into existing applications with minimal setup.
Install the module.
composer require liberusoftware/profiles
If Packagist resolution fails (common for single-maintainer Liberu modules), add the GitHub repo as a VCS repository in composer.json and require dev-main — then pin to a commit SHA.
Publish and migrate.
php artisan vendor:publish --provider="Liberu\Profiles\ProfilesServiceProvider"
php artisan migrate
Check config/profiles.php before migrating — table names and the user foreign key are usually configurable, and getting them wrong means a rollback and re-migrate.
Read these files first, in this order:
src/ProfilesServiceProvider.php — what's registered, what's publishable, whether routes/views/migrations are loaded conditionally.config/profiles.php — table names, model bindings, upload disk, approval toggles.database/migrations/* — the actual schema is the source of truth for what "controlled updates" means here (look for a status/approved_at/reviewed_by column).src/Models/ and src/Http/Controllers/ — the real public API surface.First use case: attach the package's profile trait (or relation) to App\Models\User, then let a user edit display name, avatar, timezone, and locale through the package's routes. If an approval column exists in the migration, you have the second use case for free: submissions land as pending and an admin approves them.
Smoke test before writing code:
php artisan route:list --name=profiles
php artisan migrate:status
If no routes appear, the module isn't auto-discovered — see Gotchas.
Attach via trait, don't subclass. The typical Liberu module ships a HasProfile trait plus a Profile model with a user() belongsTo. Add the trait to User rather than swapping the model, so module-users, module-teams, and auth keep working.
Read preferences through a single accessor. Presentation preferences (theme, locale, timezone, avatar, notification channels) are usually stored as a JSON column or discrete columns. Wrap reads in one helper/service method ($user->preference('timezone', config('app.timezone'))) so you have one place to add caching and one place to change storage later. Never read the raw JSON column in Blade.
Controlled updates as a two-state workflow. The package's core value is gating profile changes. The pattern that scales:
pending status + snapshot of proposed values.approved_by/approved_at.Drive the transitions through the package's service/action class if one exists; if not, wrap them in your own action class so events fire consistently.
Hear the events, don't poll. Look for ProfileUpdated, ProfileUpdateRequested, or ProfileApproved in src/Events/. Register listeners to fan out to notifications, audit logs, and search indexing. If events are missing, that's your first fork/PR candidate — it's the cleanest extension point.
Pair with policies. Register a ProfilePolicy (update, approve) and let the package's controllers authorize through it. This is how you get "users edit their own, admins edit anyone" without touching vendor code.
Presentation preferences belong at render time. Resolve theme/locale in a middleware or view composer, cache per-user for the request (once() or a request-scoped singleton), and let the frontend read from a single $user->presentation() array. Keeps controllers thin and makes A/B or per-tenant defaults easy later.
Testing. Use RefreshDatabase. If the package ships factories, use them. For approval flows, freeze time and assert the audit columns — this is the part reviewers care about most.
src/ into your app if you need a fix. Budget a day for reading before you commit to it.git ls-remote --tags) before trusting composer require to resolve anything stable.nwidart/laravel-modules or a modules_statuses.json flag, you must enable it there or manually register the provider in bootstrap/providers.php. Symptom: commands succeed, routes 404.user_id vs users_id, UUID vs bigint). Read the migration before running it on an existing DB; on a fresh install it's fine, on an existing one it can cascade-delete.Profile::where(...)->update([...]) bypasses model events, so approval listeners silently never fire. Use fill()->save() or the package's action class.DB::transaction with lockForUpdate(), and make the status update conditional (where('status','pending')) so the second one no-ops.FILESYSTEM_DISK after the fact leaves orphaned URLs. Store the path, resolve the URL at render time.ProfileUpdated — otherwise theme changes appear "saved but not applied" and you'll burn an hour on it.null timezones in production.php artisan route:list --name=profiles, check storage/logs/laravel.log for provider boot errors, php artisan about to confirm the provider loaded, and DB::listen() around a save to see whether the write is hitting the expected table.config/profiles.php (swap Profile for your own subclass), the published views (override theme without touching src/), and the event/listener pair. Avoid editing vendor files directly — with no release cadence you'll never get clean merges back.How can I help you explore Laravel packages today?