Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,80 @@ You can disable the redirect when including the Form/Show component inside of an
@livewire('tapp.filament-form-builder.livewire.filament-form.show', ['form' => $test->form, 'blockRedirect' => true])
```

## Multi-Tenancy Support

This plugin includes comprehensive support for multi-tenancy, allowing you to scope forms, form fields, and entries to specific tenants (e.g., Teams, Organizations, Companies).

### ⚠️ Important: Configure Before Migration

**You MUST enable and configure tenancy BEFORE running migrations!** The migrations check the tenancy configuration to determine whether to add tenant columns to the database tables. Enabling tenancy after running migrations will require manual database modifications.

### Configuration

Update your `config/filament-form-builder.php` configuration file:

```php
'tenancy' => [
// Enable tenancy support
'enabled' => true,

// The Tenant model class
'model' => \App\Models\Team::class,

// Optional: Override the tenant relationship name
// (defaults to snake_case of tenant model class name: Team -> 'team')
'relationship_name' => null,

// Optional: Override the tenant foreign key column name
// (defaults to relationship_name + '_id': 'team' -> 'team_id')
'column' => null,
],
```

### Setup Steps

1. **Configure tenancy** in `config/filament-form-builder.php` (set `enabled` to `true` and specify your tenant model)
2. **Publish migrations**: `php artisan vendor:publish --tag="filament-form-builder-migrations"`
3. **Run migrations**: `php artisan migrate`
4. **Configure your Filament Panel** with tenancy:

```php
use Filament\Panel;
use App\Models\Team;
use Tapp\FilamentFormBuilder\FilamentFormBuilderPlugin;

public function panel(Panel $panel): Panel
{
return $panel
->tenant(Team::class)
->plugins([
FilamentFormBuilderPlugin::make(),
]);
}
```

### How It Works

When tenancy is enabled:

- **Automatic Scoping**: All queries within Filament panels are automatically scoped to the current tenant
- **URL Structure**: Forms are accessed via tenant-specific URLs: `/admin/{tenant-slug}/filament-forms`
- **Data Isolation**: Each tenant can only access their own forms, fields, and entries
- **Cascade Deletion**: Deleting a tenant automatically removes all associated form data

### Disabling Tenancy

To disable tenancy, set `enabled` to `false` in your configuration:

```php
'tenancy' => [
'enabled' => false,
'model' => null,
],
```

### Events

#### Livewire
The FilamentForm/Show component emits an 'entrySaved' event when a form entry is saved. You can handle this event in a parent component to as follows.
```
Expand Down
19 changes: 19 additions & 0 deletions config/filament-form-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,23 @@
'admin-panel-filament-form-field-name-plural' => 'Fields',

'preview-route' => 'filament-form-builder.show',

// Multi-Tenancy configuration
'tenancy' => [
// Enable tenancy support
'enabled' => false,

// The Tenant model class (e.g., App\Models\Team::class, App\Models\Organization::class)
'model' => null,

// The tenant relationship name (defaults to snake_case of tenant model class name)
// For example: Team::class -> 'team', Organization::class -> 'organization'
// This should match what you configure in your Filament Panel:
// ->tenantOwnershipRelationshipName('team')
'relationship_name' => null,

// The tenant column name (defaults to snake_case of tenant model class name + '_id')
// You can override this if needed
'column' => null,
],
];
36 changes: 36 additions & 0 deletions database/migrations/create_dynamic_filament_form_tables.php.stub
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ return new class extends Migration
{
Schema::create('filament_forms', function (Blueprint $table) {
$table->id();

// Add tenant foreign key if tenancy is enabled
if (config('filament-form-builder.tenancy.enabled')) {
$tenantModel = config('filament-form-builder.tenancy.model');
if (! $tenantModel) {
throw new \Exception('Tenant model must be configured when tenancy is enabled.');
}
$table->foreignIdFor($tenantModel)
->constrained()
->cascadeOnDelete();
}

$table->timestamps();
$table->string('name');
$table->text('description')->nullable();
Expand All @@ -23,6 +35,18 @@ return new class extends Migration

Schema::create('filament_form_fields', function (Blueprint $table) {
$table->id();

// Add tenant foreign key if tenancy is enabled
if (config('filament-form-builder.tenancy.enabled')) {
$tenantModel = config('filament-form-builder.tenancy.model');
if (! $tenantModel) {
throw new \Exception('Tenant model must be configured when tenancy is enabled.');
}
$table->foreignIdFor($tenantModel)
->constrained()
->cascadeOnDelete();
}

$table->timestamps();
$table->foreignId('filament_form_id')->constrained()->cascadeOnDelete();
$table->integer('order');
Expand All @@ -36,6 +60,18 @@ return new class extends Migration

Schema::create('filament_form_user', function (Blueprint $table) {
$table->id();

// Add tenant foreign key if tenancy is enabled
if (config('filament-form-builder.tenancy.enabled')) {
$tenantModel = config('filament-form-builder.tenancy.model');
if (! $tenantModel) {
throw new \Exception('Tenant model must be configured when tenancy is enabled.');
}
$table->foreignIdFor($tenantModel)
->constrained()
->cascadeOnDelete();
}

$table->timestamps();
$table->foreignId('filament_form_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
Expand Down
20 changes: 20 additions & 0 deletions src/Filament/Resources/FilamentFormResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ class FilamentFormResource extends Resource

protected static ?int $navigationSort = 99;

/**
* Check if this resource should be scoped to a tenant.
*/
public static function isScopedToTenant(): bool
{
return config('filament-form-builder.tenancy.enabled', false);
}

/**
* Get the tenant ownership relationship name.
*/
public static function getTenantOwnershipRelationshipName(): string
{
if (! config('filament-form-builder.tenancy.enabled')) {
return 'tenant';
}

return FilamentForm::getTenantRelationshipName();
}

public static function getBreadcrumb(): string
{
return config('filament-form-builder.admin-panel-resource-name-plural');
Expand Down
2 changes: 2 additions & 0 deletions src/Models/FilamentForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tapp\FilamentFormBuilder\Models\Traits\BelongsToTenant;

/**
* @property int $id
Expand All @@ -16,6 +17,7 @@
*/
class FilamentForm extends Model
{
use BelongsToTenant;
use HasFactory;

protected $guarded = [];
Expand Down
2 changes: 2 additions & 0 deletions src/Models/FilamentFormField.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;
use Tapp\FilamentFormBuilder\Enums\FilamentFieldTypeEnum;
use Tapp\FilamentFormBuilder\Models\Traits\BelongsToTenant;

/**
* @property int $id
Expand All @@ -20,6 +21,7 @@
*/
class FilamentFormField extends Model implements Sortable
{
use BelongsToTenant;
use HasFactory;
use SortableTrait;

Expand Down
2 changes: 2 additions & 0 deletions src/Models/FilamentFormUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Tapp\FilamentFormBuilder\Models\Traits\BelongsToTenant;

/**
* @property array $entry
Expand All @@ -16,6 +17,7 @@
*/
class FilamentFormUser extends Model implements HasMedia
{
use BelongsToTenant;
use HasFactory;
use InteractsWithMedia;

Expand Down
120 changes: 120 additions & 0 deletions src/Models/Traits/BelongsToTenant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace Tapp\FilamentFormBuilder\Models\Traits;

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;

trait BelongsToTenant
{
/**
* Boot the trait and register the dynamic tenant relationship.
*/
public static function bootBelongsToTenant(): void
{
if (! config('filament-form-builder.tenancy.enabled')) {
return;
}

// Register the dynamic relationship
static::resolveRelationUsing(
static::getTenantRelationshipName(),
function ($model) {
return $model->belongsTo(config('filament-form-builder.tenancy.model'), static::getTenantColumnName());
}
);

// Automatically set tenant_id when creating a new model
static::creating(function ($model) {
$tenantColumnName = static::getTenantColumnName();

// Skip if tenant foreign key is already set (e.g., by Filament's observer)
if (! empty($model->{$tenantColumnName})) {
return;
}

$tenantRelationshipName = static::getTenantRelationshipName();

// Try to get tenant from Filament context (Filament's standard method)
// This handles top-level resources created outside Filament's Resource observers
if (class_exists(\Filament\Facades\Filament::class)) {
$tenant = \Filament\Facades\Filament::getTenant();
if ($tenant) {
$model->{$tenantRelationshipName}()->associate($tenant);

return;
}
}

if (method_exists($model, 'filamentForm') && isset($model->filament_form_id)) {
$parentFormId = $model->filament_form_id;
$parentFormClass = get_class($model->filamentForm()->getRelated());
$parentForm = $parentFormClass::find($parentFormId);

if ($parentForm) {
$parentTenant = $parentForm->{$tenantRelationshipName};
if ($parentTenant) {
$model->{$tenantRelationshipName}()->associate($parentTenant);
}
}
}
});
}

/**
* Get the tenant relationship name.
*/
public static function getTenantRelationshipName(): string
{
// Use configured relationship name if provided
if ($relationshipName = config('filament-form-builder.tenancy.relationship_name')) {
return $relationshipName;
}

// Auto-detect from tenant model class name
$tenantModel = config('filament-form-builder.tenancy.model');

if (! $tenantModel) {
if (config('filament-form-builder.tenancy.enabled')) {
throw new \Exception('Tenant model not configured in filament-form-builder.tenancy.model');
}

return 'tenant'; // Return a default value when tenancy is disabled
}

return Str::snake(class_basename($tenantModel));
}

/**
* Get the tenant column name.
*/
public static function getTenantColumnName(): string
{
// Use configured column name if provided
if ($columnName = config('filament-form-builder.tenancy.column')) {
return $columnName;
}

// Auto-detect from tenant model class name
return static::getTenantRelationshipName().'_id';
}

/**
* Get the tenant relationship instance.
* This provides a typed method for IDEs and static analysis.
*/
public function tenant(): ?BelongsTo
{
if (! config('filament-form-builder.tenancy.enabled')) {
return null;
}

$tenantModel = config('filament-form-builder.tenancy.model');

if (! $tenantModel) {
throw new \Exception('Tenant model not configured in filament-form-builder.tenancy.model');
}

return $this->belongsTo($tenantModel, static::getTenantColumnName());
}
}