Skip to content

Implementation Patterns Guide

Last updated: 2026-08-14

This guide walks through some key implementation patterns for Filament Address Pro.


Overview

PatternUse CaseComplexityTime
Pattern 1: Basic (Polymorphic)Standard address storageLow15 min
Pattern 2: Embedded FieldsSingle address per entityMedium30 min
Pattern 3: Custom Extended FormAdd custom fields to addressMedium30 min
Pattern 4: Multiple PanelsMulti-tenant or different access levelsMedium45 min
Pattern 5: Auto-populate Fields from GeocodingCapture extra components (neighborhood, county)Medium30 min

Best for: Most use cases - multiple addresses per entity with full package features

Step 1: Install Package

bash
composer require viewflex/filament-address-pro

# Publish config
php artisan vendor:publish --tag="filament-address-config"

# Run migrations
php artisan migrate

# Seed country data (REQUIRED)
php artisan db:seed --class="Viewflex\FilamentAddress\Database\Seeders\CountriesDomainSeeder"

Step 2: Configure Environment

env
# .env — two-key setup recommended for production (see CONFIGURATION-GUIDE.md)
GOOGLE_MAPS_SERVER_KEY=your_server_key    # IP-restricted: Geocoding + Static Maps + Address Validation
GOOGLE_MAPS_BROWSER_KEY=your_browser_key  # Domain-restricted: Maps JavaScript + Places APIs
# GOOGLE_MAPS_API_KEY=your_key           # Single-key fallback if above are not set
ADDRESS_VERIFICATION_PROVIDER=auto  # auto, usps, google, or none
USPS_CONSUMER_KEY=your_key  # Optional - for US verification
USPS_CONSUMER_SECRET=your_secret

Step 3: Add Trait to Models

php
// app/Models/Customer.php
use Viewflex\FilamentAddress\Concerns\HasAddresses;

class Customer extends Model
{
    use HasAddresses;
}

Step 4: Add Relation Manager to Resource

php
// app/Filament/Resources/CustomerResource.php
use Viewflex\FilamentAddress\Filament\RelationManagers\AddressesRelationManager;

public static function getRelations(): array
{
    return [
        AddressesRelationManager::class,
    ];
}

Step 5: Register Package Resources (Optional)

Only if you want the standalone Address manager:

php
// app/Providers/Filament/AdminPanelProvider.php
use Viewflex\FilamentAddress\FilamentAddressServiceProvider;

public function panel(Panel $panel): Panel
{
    return $panel
        // ... other config
        ->resources(FilamentAddressServiceProvider::getResources());
}

Step 6: Configure Import Whitelist

php
// config/addresses.php
'import' => [
    'allowed_entity_types' => [
        'App\Models\User',
        'App\Models\Customer',  // Add your entities
        'App\Models\Location',
    ],
],

✅ Done! Test It

  1. Go to Customer resource → Edit → Addresses tab
  2. Click "New Address"
  3. Use map search or type address
  4. Save and verify

Pattern 2: Embedded Address Fields

Best for: Entities that have exactly ONE address (e.g., User profile, Company HQ)

When to Use

  • Entity always has exactly one address
  • Don't need address history
  • Want simpler UI (no relation manager tab)

Step 1-2: Same as Pattern 1

Follow Steps 1-2 from Pattern 1 (install + configure)

Step 3: Create Migration with Address Fields

⚠️ IMPORTANT: Use ulid() for subdivision IDs, NOT unsignedBigInteger()

php
// database/migrations/xxxx_create_companies_table.php
Schema::create('companies', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->nullable();

    // Address fields
    $table->string('country_code', 2)->nullable();
    $table->string('address_line_1')->nullable();
    $table->string('address_line_2')->nullable();
    $table->string('postal_code')->nullable();

    // ⚠️ CRITICAL: Use ulid() not unsignedBigInteger()
    $table->ulid('administrative_area_id')->nullable();
    $table->string('administrative_area')->nullable();
    $table->ulid('locality_id')->nullable();
    $table->string('locality')->nullable();
    $table->ulid('dependent_locality_id')->nullable();
    $table->string('dependent_locality')->nullable();

    $table->decimal('lat', 10, 7)->nullable();
    $table->decimal('lon', 10, 7)->nullable();

    $table->timestamps();
});

Step 4: Embed AddressForm in Resource

php
// app/Filament/Resources/Companies/Schemas/CompanyForm.php
use Viewflex\FilamentAddress\Filament\Schemas\AddressForm;
use Filament\Forms\Components\Section;

public static function configure(Schema $schema): Schema
{
    return $schema->components([
        TextInput::make('name')->required(),
        TextInput::make('email')->email(),

        Section::make('Company Address')
            ->schema(AddressForm::schema())
            ->columns(2),
    ]);
}

Step 5: Add HandlesMapSelection to Edit Page

php
// app/Filament/Resources/Companies/Pages/EditCompany.php
use Viewflex\FilamentAddress\Concerns\HandlesMapSelection;

class EditCompany extends EditRecord
{
    use HandlesMapSelection;

    protected static string $resource = CompanyResource::class;
}

✅ Done! Test It

  1. Create/Edit Company
  2. Fill address fields or use map search
  3. Save and verify all fields populate

Pattern 3: Extended Address Form with Custom Fields

Best for: Need standard address PLUS custom fields (delivery instructions, gate codes, etc.)

Step 1-3: Same as Pattern 1

Follow Steps 1-3 from Pattern 1 (install + configure + add trait)

Step 4: Create Extended Form Class

php
// app/Filament/Schemas/CustomAddressForm.php
namespace App\Filament\Schemas;

use Viewflex\FilamentAddress\Filament\Schemas\AddressForm as BaseAddressForm;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;

class CustomAddressForm extends BaseAddressForm
{
    public static function schema(): array
    {
        return [
            ...parent::schema(),  // All standard address fields

            // Your custom fields
            TextInput::make('delivery_notes')
                ->label('Delivery Instructions')
                ->maxLength(500)
                ->columnSpanFull(),

            TextInput::make('gate_code')
                ->label('Gate/Access Code')
                ->maxLength(50),

            TextInput::make('parking_info')
                ->label('Parking Information')
                ->maxLength(255),
        ];
    }
}

Step 5: Add Custom Fields to Migration

php
// database/migrations/xxxx_add_custom_fields_to_addresses.php
Schema::table('addresses', function (Blueprint $table) {
    $table->text('delivery_notes')->nullable();
    $table->string('gate_code', 50)->nullable();
    $table->string('parking_info')->nullable();
});

Step 6: Add to Address Model Fillable

php
// In your AppServiceProvider or AddressObserver
use Viewflex\FilamentAddress\Models\Address;

Address::mergeFillable([
    'delivery_notes',
    'gate_code',
    'parking_info',
]);

Step 7: Use Custom Form in Resource

php
// app/Filament/Resources/Companies/Schemas/CompanyForm.php
use App\Filament\Schemas\CustomAddressForm;

Section::make('Company Address')
    ->schema(CustomAddressForm::schema())
    ->columns(2),

✅ Done! Test It

  1. Create address with custom form
  2. Fill standard + custom fields
  3. Save and verify custom fields persist

Pattern 4: Multiple Panels

Best for: Multi-tenant apps or different admin/customer panels

Step 1-2: Same as Pattern 1

Follow Steps 1-2 from Pattern 1 (install + configure)

Step 3: Configure Custom Panel

php
// app/Providers/Filament/CustomerPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        ->id('customer')
        ->path('customer')
        // ... other config
        ->resources(FilamentAddressServiceProvider::getResources());
}

Step 4: Update Package Config

env
# .env - MUST match your panel configuration
ADDRESS_PANEL_ID=customer
ADDRESS_PANEL_PATH=customer

Step 5: Configure Authorization

php
// config/addresses.php
'authorization' => [
    'enabled' => true,

    // Optional: use a custom policy class
    // 'policy' => \App\Policies\AddressPolicy::class,
],

✅ Done! Test It

  1. Access both panels (/admin and /customer)
  2. Verify Address resources appear correctly
  3. Test authorization restrictions

Pattern 5: Auto-populating Custom Fields from Geocoding Data

Best for: Capturing extra geocoding components (neighborhood, county, premise name) without user input

How the Pipeline Works

When Google geocodes an address it returns more than the standard address fields. The extractor pipeline makes all components available to your code:

User input → GeocodingService → extractor.extract() → $components array → AddressProcessingResult

Your custom extractor can return any keys alongside the standard ones. Keys that match a column in addresses and are registered via mergeFillable() are automatically saved during import and bulk verification — no extra code required. Interactive form entry needs one additional step (see Step 5).

Step 1: Implement a Custom Extractor

Extend DefaultAddressExtractor so you inherit all the standard extraction logic and only add what you need.

php
// app/Services/CustomAddressExtractor.php
namespace App\Services;

use Viewflex\FilamentAddress\Services\Geocoding\Extractors\DefaultAddressExtractor;

class CustomAddressExtractor extends DefaultAddressExtractor
{
    public function supports(string $countryCode): bool
    {
        return true; // Apply to all countries; narrow with a specific code if needed
    }

    public function getPriority(): int
    {
        return 100; // Higher than default (0); higher than config-based (50)
    }

    public function extract(array $addressComponents): array
    {
        // Get standard fields from parent
        $components = parent::extract($addressComponents);

        // Extract additional components from Google's raw address_components
        $neighborhood = '';
        $county = '';

        foreach ($addressComponents as $component) {
            $types = $component['types'] ?? [];

            if (in_array('neighborhood', $types)) {
                $neighborhood = $component['long_name'];
            }

            if (in_array('administrative_area_level_2', $types)) {
                $county = $component['long_name'];
            }
        }

        return array_merge($components, [
            'neighborhood' => $neighborhood ?: null,
            'county'       => $county ?: null,
        ]);
    }
}

Step 2: Register the Extractor

php
// app/Providers/AppServiceProvider.php
use App\Services\CustomAddressExtractor;
use Viewflex\FilamentAddress\Services\Geocoding\AddressExtractorRegistry;

public function boot(): void
{
    $this->app->resolving(AddressExtractorRegistry::class, function ($registry) {
        $registry->register(new CustomAddressExtractor);
    });
}

Step 3: Add the Migration

php
// database/migrations/xxxx_add_geocoding_fields_to_addresses.php
Schema::table('addresses', function (Blueprint $table) {
    $table->string('neighborhood')->nullable()->after('address_line_2');
    $table->string('county')->nullable()->after('neighborhood');
});

Step 4: Register as Fillable and Add to Form

php
// app/Providers/AppServiceProvider.php
use Viewflex\FilamentAddress\Models\Address;

Address::mergeFillable(['neighborhood', 'county']);
php
// app/Filament/Schemas/CustomAddressForm.php
use Viewflex\FilamentAddress\Filament\Schemas\AddressForm as BaseAddressForm;
use Filament\Forms\Components\TextInput;

class CustomAddressForm extends BaseAddressForm
{
    public static function schema(): array
    {
        return [
            ...parent::schema(),

            TextInput::make('neighborhood')
                ->label('Neighborhood')
                ->maxLength(100),

            TextInput::make('county')
                ->label('County')
                ->maxLength(100),
        ];
    }
}

Step 5: Populate the Fields After Save (Interactive Form)

Import and bulk verification already work at this point — the extractor keys flow through AddressProcessingResult::getData() and are saved to the model automatically when the fields are in fillable.

For interactive form entry (blur geocoding), the form's geocoding callback writes a fixed set of standard fields and does not call $set() for custom extractor keys. The custom fields will be blank until the record is saved. To fill them on save, override afterCreate() and afterSave() in your relation manager or resource page:

php
// app/Filament/Resources/CustomerResource/RelationManagers/AddressesRelationManager.php
namespace App\Filament\Resources\CustomerResource\RelationManagers;

use Viewflex\FilamentAddress\Filament\RelationManagers\AddressesRelationManager as BaseRelationManager;
use Viewflex\FilamentAddress\Models\Address;
use Viewflex\FilamentAddress\Services\Geocoding\CachedGeocodingService;

class AddressesRelationManager extends BaseRelationManager
{
    protected function afterCreate(): void
    {
        $this->enrichRecord($this->getRecord());
    }

    protected function afterSave(): void
    {
        $this->enrichRecord($this->getRecord());
    }

    private function enrichRecord(Address $address): void
    {
        if (! $address->country_code || ! $address->address_line_1) {
            return;
        }

        $query = implode(', ', array_filter([
            $address->address_line_1,
            $address->locality,
            $address->administrative_area,
            $address->postal_code,
        ]));

        $result = app(CachedGeocodingService::class)->geocode($query, $address->country_code);

        if (! $result['success']) {
            return;
        }

        // The custom extractor runs here; result['components'] contains our extra keys.
        // mergeFillable() in Step 4 is what makes update() accept these fields.
        Address::withoutEvents(fn () => $address->update([
            'neighborhood' => $result['components']['neighborhood'] ?? null,
            'county'       => $result['components']['county'] ?? null,
        ]));
    }
}

Cache behaviour: Geocoding is cached by address string. If the address was geocoded during form entry and the saved address matches the geocoded query, this call returns the cached result immediately — no API call.

If you need enrichment across all paths without subclassing the relation manager, a model observer on Address calling the same enrichRecord logic works identically.

✅ Done! Test It

  1. Import a CSV with geocoding enabled — neighborhood and county columns populate automatically
  2. Create an address via the form using blur geocoding — custom fields populate after save

Common Issues & Solutions

Issue: "Data truncated for column 'administrative_area_id'"

Cause: Used unsignedBigInteger instead of ulid()

Fix:

php
// Migration
$table->ulid('administrative_area_id')->change();
$table->ulid('locality_id')->change();
$table->ulid('dependent_locality_id')->change();

Issue: Import fails with "Failed: X"

Cause: Entity type not in whitelist

Fix:

php
// config/addresses.php
'import' => [
    'allowed_entity_types' => [
        'App\Models\User',
        'App\Models\YourModel',  // Add here
    ],
],

Issue: Verification dialog doesn't appear

This is NORMAL! The dialog only shows when there are differences that need your decision. If blur geocoding + verification work seamlessly, you won't see it. This is good UX.

To verify it's working: Check database for is_verified and verification_provider fields.


Testing Your Implementation

Quick Test Checklist

  • [ ] Create address with map search
  • [ ] Create address by typing (blur geocoding)
  • [ ] Create address manually (no geocoding)
  • [ ] Edit existing address
  • [ ] Set primary address
  • [ ] Delete address
  • [ ] Import addresses from CSV
  • [ ] Export addresses to CSV
  • [ ] Test with US address (USPS)
  • [ ] Test with international address (Google)
  • [ ] Test with invalid/incomplete address

Performance Check

bash
# Check country data loaded
php artisan tinker
>>> \Viewflex\FilamentAddress\Models\Country::count()
# Should be: 256

>>> \Viewflex\FilamentAddress\Models\CountrySubdivision::count()
# Should be: ~17,000+

Getting Help

Documentation:

Issues: Contact Support


Next Steps After Implementation

  1. Test thoroughly - Work through the Quick Test Checklist above
  2. Configure verification - Choose provider and test
  3. Set up import whitelist - Add your entity types
  4. Review authorization - Configure access control
  5. Customize forms - Add custom fields if needed
  6. Test internationally - Verify your target countries

Released under a commercial license.