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

Vendra User Profile Laravel Package

misaf/vendra-user-profile

misaf/vendra-user-profile is a Laravel package for adding user profile management to Vendra applications. It helps organize profile data and integrate user profile features with minimal setup, giving developers a convenient foundation for profile-related workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and boot the module

composer require misaf/vendra-user-profile
php artisan vendor:publish --tag=vendra-user-profile-config
php artisan vendor:publish --tag=vendra-user-profile-migrations
php artisan migrate
php artisan storage:link   # only if you use avatar/profile images on a public disk
php artisan route:list | grep -i profile

The service provider does the wiring for you: it registers the profiles / userProfiles relations on App\Models\User via User::resolveRelationUsing(...), registers the table with TenantTableRegistry, and exposes the Filament plugin. Do not add a HasProfile trait or a hard-coded profile() method to App\Models\User — if a relation looks missing, the provider didn't load or the config was cached stale (php artisan config:clear).

First use case — attach a profile at registration

// inside your RegisterController / onboarding action
$user = User::create($validated);

$profile = $user->userProfiles()->create([
    'display_name' => $validated['name'],
]);

// let the observer own is_default — don't set it manually

Read it back with $user->userProfiles (or the singular accessor the package exposes), and eager load on any listing: User::with('userProfiles')->paginate().

Where to look first, in order

  1. config/vendra-user-profile.php — module_enabled, features_enabled, features_discover, navigation labels/group, route prefix and middleware.
  2. database/migrations — confirm the table is user_profiles, the default_user_guard unique index, and the TenantSchema::addTenantColumn() usage.
  3. src/Models/UserProfile.php — traits, $fillable, $translatable, casts, relationships.
  4. src/Observers/UserProfileObserver.php — the single-default invariant.
  5. src/Filament/ — Plugin, Clusters/Resources/UserProfileResource, Schemas/*Form.php, Tables/*Table.php.
  6. src/Policies + the permission enum + PermissionPolicySeeder.
  7. lang/{en,de,fa} — vendra-user-profile::attributes, ::navigation.
  8. skills/vendra-user-profile-development/SKILL.md in the parent misaf/vendra repo — it is the real contract.

Implementation Patterns

Keep the domain in the module, keep the host app thin. The host app should only integrate: call the model, hit the routes, render the resource. Profile columns, factories, policies, observers, Filament classes, translations and tests live in packages/vendra-user-profile and use namespace Misaf\VendraUserProfile. If you find yourself writing profile business logic in app/, stop and move it.

Create vs. update is a different code path.

// onboarding / registration — idempotent, safe for pre-existing users
$user->userProfiles()->firstOrCreate(
    ['is_default' => true],
    ['display_name' => $request->display_name],
);

// settings screen — explicit update behind a policy
$profile = $user->userProfiles()->where('is_default', true)->firstOrFail();
$this->authorize('update', $profile);
$profile->update($request->validated());

firstOrCreate keyed on the default flag avoids duplicate-profile errors and backfills users created before the module was installed. Use form requests for validation and a small action class so the same logic serves registration, onboarding, and settings.

Never manage is_default yourself. The observer uses MaintainsSingleFlagPerOwner and the default_user_guard unique index enforces it at the DB. Setting the flag in a factory, seeder, or controller will race the observer and surface as a QueryException. If you legitimately need to switch defaults, do it as a transaction and expect a unique-constraint retry path.

Filament is the day-to-day surface. Register the module Plugin on the panel — don't manually wire the resource in an unrelated panel provider. UserProfileResource is ungrouped inside CustomersCluster and sits right after Users via NavigationPriority::UserProfiles; keep it there. Keep the resource class thin (schema in Schemas/UserProfileForm.php, table in Tables/UserProfilesTable.php) and use Filament v5 namespaces (Filament\Forms\Components, Filament\Schemas\Components, Filament\Tables\Columns, Filament\Actions, Filament\Support\Icons\Heroicon).

Extend through the registry, not through edits. UserProfileRelationManagers is the generic extension point — optional packages (Address, Phone, Document, Verification) contribute relation managers there. User Profile must never import or require them; that's a reverse dependency. Add your own contributors the same way.

Permissions come from the policy enum. Every panel action needs an enum case and a policy method with the matching name (viewAny, view, create, update, delete, deleteAny, restore, restoreAny, forceDelete, forceDeleteAny, replicate, reorder). Update PermissionPolicySeeder in the same commit, or the action appears and 403s.

Gate with Features\ModuleEnabled, not config(). Check access through the class — its Pennant before() hook applies the global switches (module_enabled, features_enabled, features_discover) before persisted tenant values. Keep those three keys flat.

Tenancy is derived, never declared. Use the bound TenantResolver from misaf/vendra-support (TenantAwareness, BelongsToTenant, TenantSchema, RequiresCurrentTenant). BelongsToTenant assigns tenant_id on creating — so factories and seeders must not set tenant_id and must work with tenancy on or off. Hide tenant_id from serialization. If the table was migrated before tenancy existed, run php artisan vendra-tenant:enable {tenant} to backfill rather than hand-writing data.

Translatable fields are an explicit decision. Every field in $translatable must be a JSON column with translation-aware casts, factories, validation, Filament locale UI, API serialization, and tests. Every field not in $translatable must be scalar and must not use Spatie Translatable, translatable slugs, locale switchers, or translation-shaped arrays.

APIs: return a UserResource::make($user->load('userProfiles')), and use API-Resource conditional loading rather than shipping profiles on every endpoint.

Tests are cheap here. RefreshDatabase, package factories, assert the observer keeps one default, assert translation key parity. Run the module suite and static analysis:

php artisan test --compact --testsuite=vendra-user-profile
composer stan

Gotchas and Tips

  • This repo is read-only. misaf/vendra-user-profile is a subtree split. PRs and issues go to misaf/vendra; changes here get overwritten on the next split. Keep local patches out of vendor/ and out of the split.
  • The v1.12.3 release notes are just a compare link (v1.11.0...v1.12.3). Before upgrading, diff that range for migration and config changes, then run your suite. It's a patch, so breakage is unlikely — but the split repo gives you no changelog prose.
  • Name drift is the #1 confusion. Public docs and older write-ups mention a profiles table with display_name/bio and a HasProfile trait; the current module ships user_profiles, UserProfile, and resolveRelationUsing-registered relations. Always confirm against the installed migrations and UserProfileServiceProvider before writing queries or factories.
  • Don't hard-code relations on User. If you define your own profile() method, you'll shadow the provider's resolveRelationUsing registration and break withCount/eager loading across the module.
  • Never reference a concrete tenant provider (Misaf\VendraTenant) anywhere — models, migrations, factories, seeders, fixtures, tests. The module must build and run in apps with no tenancy installed. There is no tenant_aware config toggle; adding one is a design regression.
  • Flat feature config only. features_enabled, features_discover, module_enabled. Nested features.* keys aren't read and will silently no-op.
  • A silent 403 in Filament usually means the feature flag or the policy, not the middleware. Check ModuleEnabled first, then the enum case, then PermissionPolicySeeder.
  • A silently dropped field means $fillable. When you add a custom column, ship your own migration (never edit the vendor one) and update fillable/casts and the form schema and validation. Four places, or it disappears.
  • N+1 in Blade and Filament tables. $user->userProfiles inside a loop is a query per row. Eager load, use withCount, or build computed state on loaded relations.
  • Unique indexes need app-level validation too. Usernames/emails and the default-profile flag both race; catch QueryException and return a 422 rather than a 500.
  • Translations are part of the contract. Update en, de, fa together, keep keys sorted, keep model labels singular and navigation/plural labels using the plural key, and keep navigation labels ≤ 24 characters. There's a parity test — don't fight it.
  • Published routes can collide with your existing auth/settings routes. Check php artisan route:list and change the prefix/middleware in config/vendra-user-profile.php rather than forking controllers.
  • Avatar uploads: confirm the expected disk and column name, set the disk in config/filesystems.php, and remember storage:link. Use public visibility only when public access is genuinely required.
  • Debug ladder for "the update didn't save": $fillable → form request rules → feature flag → policy authorization → tenant scope → route model binding. Add Telescope or DB::listen to confirm no query ran at all.
  • Config is cached in prod. php artisan config:clear after any publish or config edit — this resolves the majority of "the module vanished" reports.
  • Keep published config, migrations, and views in VCS and diff them after each upgrade so local overrides don't silently drift from the split.
  • Extend via service-provider binding, not vendor edits, when you need a custom profile model — but verify the package actually exposes a binding point first; not every model is rebound.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle