genealabs/phpgmaps
PHPGmaps is a Laravel package that integrates Google Maps into your applications, offering helpers for maps, markers, geocoding, and directions with a simple, fluent API.
Pin, don't float. This package is archived — last release 0.4.3 on 2017-10-14, 27 stars, zero dependents. Install with an exact constraint so a later composer update can't drift you into an untested state:
composer require "genealabs/phpgmaps:0.4.3"
0.4.3 is a hard floor, not a preference: earlier releases had a broken lookup URL that mangled non-Latin alphabet addresses. If any international address is in scope, anything older is a bug you'll inherit.
Register the provider and facade. Check auto-discovery first — look for extra.laravel for the package in vendor/composer/installed.json. If it's absent (likely for a 2017-era package), wire it manually in config/app.php:
'providers' => [
GeneaLabs\Phpgmaps\PhpgmapsServiceProvider::class,
],
'aliases' => [
'Phpgmaps' => GeneaLabs\Phpgmaps\Facades\Phpgmaps::class,
],
Publish config and set one value. The only thing that matters day to day is the API key:
php artisan vendor:publish --provider="GeneaLabs\Phpgmaps\PhpgmapsServiceProvider"
// config/phpgmaps.php
return [
'key' => env('GOOGLE_MAPS_KEY'),
];
Add GOOGLE_MAPS_KEY to .env.example and your secret store in the same commit. Never read the config inline from a controller — bind the key once at the boundary.
Where to look first, in order:
vendor/genealabs/phpgmaps/src/ — the README is thin and the wrapper is small. The source is the documentation. Read the main class top to bottom before writing your first call site; the method signatures and config-array keys are what you actually program against.create_map() semantics map nearly 1:1. When the Laravel wrapper is ambiguous, the CodeIgniter docs are the authoritative reference.grep -rn "Phpgmaps\|phpgmaps" app/ resources/views/ routes/ — inventory existing call sites before adding one. Every new \Phpgmaps:: invocation is a liability you're choosing to add to an unmaintained dependency.First use case — geocode a single address in a back-office form or an artisan command.
$result = \Phpgmaps::geocode([
'address' => '1600 Amphitheatre Pkwy, Mountain View, CA',
]);
That's the shape of the package's sweet spot: server-side, low-traffic, read-only, no customer-facing SLA. Verify the exact signature against vendor/ before trusting it — the response is a decoded Google payload, not a domain object, so you read $result->results[0]->geometry->location->lat style accessors. Don't build a feature on the first successful response; build the adapter (below) and let it absorb the shape.
Pattern 1 — put an anti-corruption layer in front of it from day one. Do not call the facade from controllers, jobs, or Blade templates. Define a contract that expresses what the product needs, and make the package one implementation of it:
namespace App\Contracts;
interface Geocoder
{
/** @return array{lat: float, lng: float, formatted: string}|null */
public function lookup(string $address): ?array;
/** @return array{distance_meters: int, duration_seconds: int}|null */
public function route(string $from, string $to): ?array;
}
namespace App\Services\Maps;
use App\Contracts\Geocoder;
use Illuminate\Support\Facades\Log;
class PhpgmapsGeocoder implements Geocoder
{
public function lookup(string $address): ?array
{
try {
$raw = \Phpgmaps::geocode(['address' => $address]);
} catch (\Throwable $e) {
Log::warning('phpgmaps.geocode.transport', ['message' => $e->getMessage()]);
return null;
}
if (($raw->status ?? null) !== 'OK' || empty($raw->results)) {
Log::warning('phpgmaps.geocode.status', [
'status' => $raw->status ?? 'MISSING',
'message' => $raw->error_message ?? null,
]);
return null;
}
return [
'lat' => $raw->results[0]->geometry->location->lat,
'lng' => $raw->results[0]->geometry->location->lng,
'formatted' => $raw->results[0]->formatted_address,
];
}
}
Bind it once:
$this->app->bind(\App\Contracts\Geocoder::class, \App\Services\Maps\PhpgmapsGeocoder::class);
This is the whole point of using the package at all. When you replace it — and with an archived dependency you will — it's a one-class change plus a one-line binding change, not a grep-and-refactor across ten controllers.
Pattern 2 — cache on the input hash, and cache in your own table. Geocoding is deterministic for an address string plus locale/region. Start with the cache, then graduate to a table:
$coords = Cache::remember(
'geo:'.sha1($address),
now()->addDays(90),
fn () => $this->geocoder->lookup($address)
);
A persistent geocodes table (address_hash, lat, lng, formatted, provider, fetched_at) is better still: it survives the package replacement untouched, it caps quota exposure, and it gives you an audit trail of what address data left your system. Note the nuance — cache null results too, but with a much shorter TTL, or a transient OVER_QUERY_LIMIT becomes a permanently missing coordinate.
Pattern 3 — batch work goes to console or the queue, never the request cycle. Backfilling coordinates for 500 addresses in a controller blows the request timeout and hammers quota. Use an artisan command or a queued job, chunked, with a pause between batches, and inspect status per response rather than assuming success:
Address::whereNull('lat')->chunkById(50, function ($addresses) {
foreach ($addresses as $address) {
$result = $this->geocoder->lookup($address->line1);
if ($result) {
$address->update($result + ['geocoded_at' => now()]);
}
usleep(200_000); // don't hammer quota
}
});
Pattern 4 — map markup, if you must emit it. The BIOSTALL-derived map-generation side returns a ['js' => ..., 'html' => ...] pair. Render js before html — the script defines the map constructor the div depends on — and route it through a Blade partial rather than inlining in a controller:
// Controller stays thin.
return view('stores.show', [
'map' => app(\App\Contracts\MapRenderer::class)->render($store),
]);
{{-- resources/views/stores/show.blade.php --}}
{!! $map['js'] !!} {{-- raw script injection point — see Gotchas --}}
{!! $map['html'] !!}
Treat both as untrusted output. If you can't fully control everything interpolated into them, wrap them in a CSP-nonced block or move to the Google Maps JS API directly.
Pattern 5 — a driver flag doubles as a kill switch. Put the adapter behind config (MAPS_DRIVER=phpgmaps|direct) so cutting over to a replacement is an environment flip, not a release. It also lets you disable maps instantly if billing or quota becomes a problem — which for a 2017-era package is a realistic Tuesday.
Pattern 6 — test against a fake, never the network. Bind a stub Geocoder in TestCase::setUp() for the whole suite. Any test that reaches Google is slow, flaky, costs money, and breaks CI the moment the key gets a restriction applied. If you want to test your adapter's payload parsing, keep a handful of captured JSON fixtures in tests/fixtures/ — including the unhappy ones (ZERO_RESULTS, REQUEST_DENIED, OVER_QUERY_LIMIT).
The archive status is the headline gotcha. No CVE response, no compatibility patches, no maintainer to notify. Pin the exact version, and treat the source as frozen vendor code you've audited — not a dependency you can lean on. Add a review-checklist line or a CI grep that blocks new \Phpgmaps:: call sites outside the adapter.
Compatibility is a research task, not an assumption. Verify PHP and Laravel constraints in a throwaway branch before committing to a date. On Laravel 8+/PHP 8+, specifically check for: dynamic-property deprecations on the wrapper class, removed framework helpers (array_get(), str_*()), and whether service-provider auto-discovery actually fires. If it breaks, isolate it behind a legacy service boundary — do not widen your platform support to accommodate a 2017 dependency.
Check status, not truthiness. Google returns HTTP 200 with ZERO_RESULTS, OVER_QUERY_LIMIT, REQUEST_DENIED, OVER_DAILY_LIMIT, or INVALID_REQUEST inside the payload. A wrapper that appears to "work" on happy paths returns silently empty data on a restricted key or a blown quota. Log status explicitly in the adapter and alert on OVER_QUERY_LIMIT (quota) and REQUEST_DENIED (usually a key with the wrong restriction type).
Key restrictions versus the 2017 assumption. The wrapper predates Google's current key-restriction and billing model. Server-side geocoding needs an IP-restricted (or unrestricted) key; browser-emitted maps need an HTTP-referrer-restricted key. Using one key for both is a problem — use two keys, wire them through separate config values, and confirm the Cloud project has billing enabled. A disabled billing account returns REQUEST_DENIED, which looks identical to a typo'd key.
XSS from dynamic marker/label content. This generation of wrapper interpolates titles, popup content, and address strings straight into generated JavaScript. Any user-influenced string reaching a marker title, info-window body, or map config is an injection point. Escape at the boundary — json_encode($v, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT) — and prefer passing data via data-* attributes over embedding it in a script body.
CSP will bite, usually at deploy. Inline <script> output fails under any policy without 'unsafe-inline', and the package has no concept of a nonce. This is frequently the trigger that forces the replacement conversation, so surface it early — in planning, not in a failed deploy.
No retry, no backoff, no timeout handling. The wrapper is a thin request builder. Wrap every call with a timeout and a try/catch in your adapter. Add a bounded retry only for transport errors — never for OVER_QUERY_LIMIT, which just makes the throttling worse.
Debugging is on you. APP_DEBUG tells you nothing here. When a call misbehaves, log the constructed request URL (minus the key), the HTTP status, and the raw decoded payload before your adapter transforms it. If your contract returns null, the answer is almost always in status or error_message on the raw response.
Extend the adapter, never the package. Don't subclass the wrapper and don't patch vendor/. Everything you actually need — caching, retries, escaping, status normalization, locale defaults like region and language — belongs in your implementation. That keeps the replacement surface small and keeps the frozen code frozen.
Fork only for a security fix. MIT permits it, but forking converts abandonment risk into ownership cost without removing Google's API-churn risk. If you must, fork a single CVE-class fix into a private VCS repo and record the exit plan and an owner next to it.
Have the exit conversation before the next framework upgrade, not during it. This dependency breaks first during a Laravel or PHP bump — mid-upgrade, when nobody has slack. Size the replacement (Geocoder / RouteEstimator / MapRenderer against Google's web services directly, plus the DB-backed geocode cache you already built) as a prerequisite ticket inside the modernization epic. For internal-only tools, the cheapest exit is often a static map image URL or a plain outbound Google Maps link — removing the dependency entirely in an afternoon.
How can I help you explore Laravel packages today?