Implementation Patterns Guide
Last updated: 2026-08-14
This guide walks through some key implementation patterns for Filament Address Pro.
Overview
| Pattern | Use Case | Complexity | Time |
|---|---|---|---|
| Pattern 1: Basic (Polymorphic) | Standard address storage | Low | 15 min |
| Pattern 2: Embedded Fields | Single address per entity | Medium | 30 min |
| Pattern 3: Custom Extended Form | Add custom fields to address | Medium | 30 min |
| Pattern 4: Multiple Panels | Multi-tenant or different access levels | Medium | 45 min |
| Pattern 5: Auto-populate Fields from Geocoding | Capture extra components (neighborhood, county) | Medium | 30 min |
Pattern 1: Basic Polymorphic Addresses (Recommended)
Best for: Most use cases - multiple addresses per entity with full package features
Step 1: Install Package
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 — 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_secretStep 3: Add Trait to Models
// app/Models/Customer.php
use Viewflex\FilamentAddress\Concerns\HasAddresses;
class Customer extends Model
{
use HasAddresses;
}Step 4: Add Relation Manager to Resource
// 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:
// 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
// config/addresses.php
'import' => [
'allowed_entity_types' => [
'App\Models\User',
'App\Models\Customer', // Add your entities
'App\Models\Location',
],
],✅ Done! Test It
- Go to Customer resource → Edit → Addresses tab
- Click "New Address"
- Use map search or type address
- 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()
// 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
// 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
// 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
- Create/Edit Company
- Fill address fields or use map search
- 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
// 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
// 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
// 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
// app/Filament/Resources/Companies/Schemas/CompanyForm.php
use App\Filament\Schemas\CustomAddressForm;
Section::make('Company Address')
->schema(CustomAddressForm::schema())
->columns(2),✅ Done! Test It
- Create address with custom form
- Fill standard + custom fields
- 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
// 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 - MUST match your panel configuration
ADDRESS_PANEL_ID=customer
ADDRESS_PANEL_PATH=customerStep 5: Configure Authorization
// config/addresses.php
'authorization' => [
'enabled' => true,
// Optional: use a custom policy class
// 'policy' => \App\Policies\AddressPolicy::class,
],✅ Done! Test It
- Access both panels (/admin and /customer)
- Verify Address resources appear correctly
- 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 → AddressProcessingResultYour 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.
// 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
// 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
// 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
// app/Providers/AppServiceProvider.php
use Viewflex\FilamentAddress\Models\Address;
Address::mergeFillable(['neighborhood', 'county']);// 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:
// 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
- Import a CSV with geocoding enabled —
neighborhoodandcountycolumns populate automatically - 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:
// 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:
// 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
# 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:
- Getting Started
- AUTHORIZATION.md - Permission configuration
- IMPORT-EXPORT.md - Bulk operations
- VERIFICATION.md - Address verification providers
Issues: Contact Support
Next Steps After Implementation
- Test thoroughly - Work through the Quick Test Checklist above
- Configure verification - Choose provider and test
- Set up import whitelist - Add your entity types
- Review authorization - Configure access control
- Customize forms - Add custom fields if needed
- Test internationally - Verify your target countries