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

Filament Maps Laravel Package

agelgil/filament-maps

Filament Maps adds interactive map fields to Filament forms, letting admins pick and save coordinates directly in your panel. Easy to install and configure, it brings location selection into your existing forms without extra JavaScript work.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register (5 minutes)

composer require agelgil/filament-maps
php artisan vendor:publish --tag="filament-maps-config"
php artisan filament:assets          # publishes the field's JS/CSS into /public

Register the plugin on the panel(s) that need it:

// app/Providers/Filament/AdminPanelProvider.php
use Agelgil\FilamentMaps\FilamentMapsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(FilamentMapsPlugin::make());
}

Set the defaults once, so you never pass a center per field:

// config/filament-maps.php
return [
    'default_center' => ['lat' => 9.0300, 'lng' => 38.7400],
    'default_zoom'   => 13,
    'height'         => '400px',
];

Read these files first, in this order

  1. config/filament-maps.php — the published config is the real spec: it names the underlying map provider, tile/API key handling, and the state shape the field emits. That state shape is the single thing everything else in your app depends on.
  2. src/Forms/Components/*.php — the field class's setUp() shows the default height, defaultCenter, defaultZoom, and how state is serialized (array ['lat' =>, 'lng' =>] vs. a "lat,lng" string).
  3. The Blade view + JS asset — tells you which map library is underneath (Leaflet/OSM vs. Google Maps) and therefore whether you need a billing-enabled API key before anything renders.

First use case: pin a location on a Store record

$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
use Agelgil\FilamentMaps\Forms\Components\MapPicker;

MapPicker::make('location')
    ->label('Pin location')
    ->columnSpanFull()
    ->required(),

The field is a picker, not a model binding: it edits one composite value, your schema stores two columns. Bridging that is your first real task — see the next section.

Verify before writing code: the package's public class name (MapPicker vs. MapField vs. Map) and its plugin class are inferred from the description, not from published docs. grep -r "class .* extends Field" src/ will settle it in ten seconds. Same for the state shape.

Implementation Patterns

Pattern 1: composite field state → two database columns

Keep the split in one place per resource by overriding the page hooks rather than sprinkling dehydrateStateUsing across fields:

// app/Filament/Resources/StoreResource/Pages/CreateStore.php
protected function mutateFormDataBeforeCreate(array $data): array
{
    [$data['latitude'], $data['longitude']] = $this->split($data['location'] ?? null);
    unset($data['location']);

    return $data;
}

private function split(?array $point): array
{
    return [
        data_get($point, 'lat'),
        data_get($point, 'lng'),
    ];
}

On the Edit page you need the reverse — hydrate the composite state from the two columns:

MapPicker::make('location')
    ->dehydrated(false)        // page hooks own persistence
    ->formatStateUsing(fn (?Store $record) => $record?->latitude
        ? ['lat' => (float) $record->latitude, 'lng' => (float) $record->longitude]
        : null),

This split is worth putting in a HasMapCoordinates trait (coordinates() / setCoordinates() accessors) because you will do it on every geolocated resource, and it's also the seam that lets you swap this package later without touching resources.

Pattern 2: address → coordinates, without leaving the form

The package places points; it does not geocode. Wire it to a geocoder with a suffix action so the map stays the source of truth:

MapPicker::make('location')
    ->suffixAction(
        Action::make('geocode')
            ->icon('heroicon-m-magnifying-glass')
            ->action(function (Get $get, Set $set) {
                $point = app(Geocoder::class)->lookup($get('address'));
                if ($point) {
                    $set('location', ['lat' => $point->lat, 'lng' => $point->lng]);
                }
            })
    ),

Run the geocode call server-side in the action, cache results by address string, and never call the provider from the field's JS — you'd leak the key and lose the ability to rate-limit.

Pattern 3: store locator / delivery zones

Two distinct needs, and the field only covers the first:

  • Origin point per zone/warehouse — the map field is exactly right: one pin per record, plus a radius_m integer.
  • "Nearest to me" queries — the field does nothing for this. Use a raw spatial expression against the two columns:
$results = Store::query()
    ->selectRaw(
        '*, ST_Distance_Sphere(point(longitude, latitude), point(?, ?)) AS distance_m',
        [$lng, $lat]   // note: POINT() is lng, lat
    )
    ->having('distance_m', '<', 5000)
    ->orderBy('distance_m')
    ->get();

Pattern 4: making it optional and testable

->required() on the map is a UX claim you should validate, not assume — the field can dehydrate null or a half-filled array. Add an explicit rule so "one coordinate only" can never reach the database:

->rules([
    fn (): Closure => function (string $attribute, $value, Closure $fail) {
        $lat = data_get($value, 'lat');
        $lng = data_get($value, 'lng');

        if (($lat === null) !== ($lng === null)) {
            $fail('Pick a point on the map.');
        }
        if ($lat !== null && ($lat < -90 || $lat > 90)) {
            $fail('Latitude is out of range.');
        }
        if ($lng !== null && ($lng < -180 || $lng > 180)) {
            $fail('Longitude is out of range.');
        }
    },
])

Because the map is JS-only, Livewire tests never execute it — you set the state directly and assert on the columns:

it('stores the pinned coordinates', function () {
    livewire(CreateStore::class)
        ->fillForm([
            'name'     => 'Bole',
            'location' => ['lat' => 8.9950, 'lng' => 38.7870],
        ])
        ->call('create')
        ->assertHasNoFormErrors();

    $store = Store::first();
    expect((float) $store->latitude)->toBe(8.9950)
        ->and((float) $store->longitude)->toBe(38.7870);
});

That single test is your regression net: it catches a state-shape change on upgrade, which is the most likely breakage.

Gotchas and Tips

Coordinate order will bite you once, in GeoJSON. The field is lat/lng; POINT() and GeoJSON are lng, lat. Write one toGeoJson() helper on your trait and never hand-roll the swap. Every "pin appears in the wrong ocean" bug is this.

Use decimal, not float. decimal(10,7) gives you three integer digits — enough for ±90 / ±180 at ~1 cm precision. FLOAT rounds on storage and makes equality assertions flaky. Remember ->cast('decimal:7') returns a string; (float) it before comparing.

Empty state is ambiguous. Depending on the field's dehydration, an untouched map can be null, ['lat' => null, 'lng' => null], or ''. Normalize in exactly one place (your split() helper) and let the validation rule above be the gate. Check this first when "I didn't touch the map and my coordinates got wiped" shows up in QA.

Map renders blank in four predictable situations:

  1. filament:assets was never run, or public/ isn't writable in the deploy pipeline.
  2. The container has zero height — inside a Tabs/Wizard/Modal that starts hidden, or in a Repeater. Force a CSS height and call the map's invalidateSize() after show if the underlying library exposes it.
  3. A CSP header blocks tile requests or the provider's JS — check the browser console, not the Laravel log.
  4. config:cache was built before you changed default_center; re-run php artisan config:cache on deploy.

Repeaters and repeatable map fields are the weak spot. Instantiating multiple map instances from one JS bundle is where JS-backed form fields usually break. Prototype it before committing to a "multiple locations per record" feature; if it fights you, fall back to a Repeater of read-only coordinate pairs with a single map for the primary point.

A 0-star, last-released-September-2024 package is a pin-and-isolate dependency, not a standard. Two concrete consequences:

  • Pin the version ("agelgil/filament-maps": "1.0.*") and commit the lockfile. With no issue tracker history to read, you cannot predict what a minor bump changes.
  • Keep it behind your own field class. Subclass it as CoordinatesPicker extends MapPicker and use that in resources. If you outgrow it or the field stops working on a future Filament version, you edit one file.

Filament v4 is the real compatibility question. This package targets Filament v3 form schemas (schema() / ->form()), which v4 renamed and restructured. If you're starting a new panel today, confirm v4 support before designing around the field — otherwise plan to vendor and adapt the Blade view, which is small but non-zero work.

No spatial index, no scale. ST_Distance_Sphere over decimal columns is a full table scan. Fine for hundreds of stores; at tens of thousands, migrate to a POINT column with a SPATIAL INDEX (MySQL 8) or PostGIS — an additive migration, and your HasMapCoordinates trait is exactly where you absorb it.

Accessibility fallback. The field is click-and-drag only. If the coordinates are business-critical, expose two small numeric inputs (latitude/longitude) beside it, or a "paste coordinates" action — keyboard users and mobile field staff will need it, and it doubles as your debugging tool when the map won't load.

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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk