rmzamora/google-bundle
rmzamora/google-bundle provides Laravel integration for Google APIs, with configuration, service container bindings, and a streamlined setup for authenticating and calling Google services from your PHP application.
Read this first: rmzamora/google-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no publishes() tag, no facade, and no config/ stub to vendor:publish. The fork is a copy of antimattr/google-bundle, a Symfony 2.x-era bundle whose last meaningful activity predates Laravel 5.4. Treat it as reference source code to port, not a dependency to install.
That said, the scope is genuinely small — roughly three service classes plus a Twig extension — so porting it into Laravel is a half-day job, and the port is more useful than the original.
composer require rmzamora/google-bundle --dry-run
Expect a resolution failure on PHP 8.x and any Symfony/Illuminate conflict. If it does resolve, it will drag in symfony/framework-bundle and twig/twig you don't want. Do not add it to a Laravel app's require.
git clone https://github.com/rmzamora/GoogleBundle /tmp/google-bundle
cat /tmp/google-bundle/DependencyInjection/Configuration.php
Configuration.php is the single most valuable file in the repo: it documents every knob for analytics, adwords, and static maps in one place. Then read, in order:
DependencyInjection/AntimattrGoogleExtension.php — how config maps to service argumentsAnalytics/ — tracker service (renders the GA snippet)Adwords/ — conversion service (renders google_conversion_id / google_conversion_label)Maps/StaticMap.php — a fluent URL builder for maps.googleapis.com/maps/api/staticmapNames in the fork may be prefixed differently; trust the PSR-4 mapping in its composer.json over any README.
Don't port all three at once. Start with the Analytics piece because it's a single Blade component and validates your config/env wiring:
// config/google.php
return [
'analytics' => [
'measurement_id' => env('GOOGLE_ANALYTICS_ID'), // GA4: G-XXXXXXX
],
'maps' => [
'key' => env('GOOGLE_MAPS_KEY'),
'default_size' => '600x400',
'default_zoom' => 13,
],
];
{{-- resources/views/components/google-analytics.blade.php --}}
@if ($id = config('google.analytics.measurement_id'))
<script async src="https://www.googletagmanager.com/gtag/js?id={{ $id }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ $id }}');
</script>
@endif
Include it once in layouts/app.blade.php. Done — that's the whole Analytics feature, and you've already replaced the package's version with something that uses gtag.js (see Gotchas).
The Symfony bundle's Twig extension was typically called in base.html.twig. Mirror that in Blade — a single anonymous component in the layout, guarded by config, so local/test environments emit nothing. Never scatter @include('components.google-analytics') across views; you'll double-count.
For ecommerce, push to dataLayer from a Blade @push('scripts') block on the confirmation page rather than adding a second config surface:
@push('scripts')
<script>
gtag('event', 'purchase', {
transaction_id: '{{ $order->id }}',
value: {{ $order->total }},
currency: '{{ $order->currency }}',
items: @json($order->items->map->only(['sku', 'name', 'price', 'quantity'])),
});
</script>
@endpush
The bundled Adwords service just emits conversion variables + the legacy pagead/conversion.js include. The Laravel-shaped equivalent is: write a flash value on the redirect, read it once on the thank-you page.
// OrderController::store
return redirect()
->route('checkout.thanks', $order)
->with('conversion', [
'send_to' => config('google.adwords.send_to'), // AW-XXXXXXX/label
'value' => $order->total,
'currency' => $order->currency,
'transaction_id' => $order->id,
]);
{{-- checkout/thanks.blade.php --}}
@if ($conversion = session('conversion'))
@push('scripts')
<script>gtag('event', 'conversion', @json($conversion));</script>
@endpush
@endif
Flash data is single-use, which is exactly the semantics you want — refresh on the thank-you page must not re-fire a conversion. This is strictly better than the bundle's approach of rendering on every request where the Twig function is called.
The StaticMap class is the most portable piece and the only one still fully supported by Google. Port it as a plain value object with no framework dependencies:
namespace App\Support;
class StaticMap
{
private array $markers = [];
private array $center = [];
private int $zoom = 13;
private string $size = '600x400';
private string $type = 'roadmap';
public function center(float $lat, float $lng): static { $this->center = [$lat, $lng]; return $this; }
public function zoom(int $z): static { $this->zoom = $z; return $this; }
public function size(string $s): static { $this->size = $s; return $this; }
public function marker(float $lat, float $lng, ?string $color = null): static {
$this->markers[] = collect([$color, $lat, $lng])->filter(fn ($v) => $v !== null)->implode(',');
return $this;
}
public function url(): string
{
return 'https://maps.googleapis.com/maps/api/staticmap?' . http_build_query(array_filter([
'center' => $this->center ? implode(',', $this->center) : null,
'zoom' => $this->zoom,
'size' => $this->size,
'maptype' => $this->type,
'markers' => $this->markers ? implode('|', $this->markers) : null,
'key' => config('google.maps.key'),
]), '', '&', PHP_QUERY_RFC3986);
}
public function __toString(): string { return $this->url(); }
}
Two integration tips that the original bundle doesn't help you with:
// Cache because Static Maps is billed per request and the URL is deterministic.
$url = Cache::remember('map:'.md5($cacheKey), now()->addDay(), fn () => (string) $map);
// Always render with explicit dimensions — the API returns a blank tile on error,
// so a broken key looks like a grey box unless you set width/height.
<img src="{{ $url }}" width="600" height="400" loading="lazy"
alt="Map of {{ $venue->name }}" class="rounded">
config(['google.analytics.measurement_id' => null]) then $this->get('/')->assertDontSee('googletagmanager')..env.example so a new environment doesn't ship with prod analytics IDs.The Analytics half of this package is a history lesson. It emits Universal Analytics (ga.js / analytics.js) with UA-XXXXX-X IDs. Universal Analytics stopped processing hits in July 2023. If you port the tracker class verbatim you will ship dead code that looks like it works in devtools. Port the shape, but emit gtag.js and GA4 G- IDs.
Adwords conversion.js is likewise deprecated. The bundle injects //www.googleadservices.com/pagead/conversion.js and sets google_conversion_id / google_conversion_label globals. Google's current mechanism is gtag('event', 'conversion', { send_to: 'AW-.../label' }). Don't port the script include; port the configuration (a send_to string is simpler than two separate values, which is a small improvement worth making).
Static Maps now requires an API key and a billing account. The bundle's config exposes a sensor parameter (it was mandatory in 2011, then optional, then removed) and may default to a keyless request. A keyless or unbilled request returns a 403 image, which renders as a broken image rather than an exception — the failure is silent unless you check width/height in the markup and monitor for it.
URL length limits. Static Maps caps the total request URL around 8 KB. A dozen labeled markers with custom colors will get you there quickly and you'll get a 414 with no useful body. Cap marker count in your builder, or switch to a JS map for dense datasets.
CSP will break the inline snippets. Both the analytics and conversion snippets are raw inline <script>. If you run a strict Content-Security-Policy, add a nonce:
<script nonce="{{ csp_nonce() }}">…</script>
and allow https://www.googletagmanager.com in script-src plus https://www.google-analytics.com in connect-src. Symfony bundles of this vintage never anticipated CSP.
Twig/DI compatibility landmines if you do try to install it. Twig 1.x extensions extend \Twig_Extension, which is a fatal error on Twig 2/3 (renamed to AbstractExtension). Service IDs are namespaced antimattr_google.* while the fork may use a different alias — check both Configuration.php root name and the extension's getAlias(). And PHP 8 will surface bindParam/implicit-nullable deprecations throughout.
Zero dependents, no tests, no stars. There's no signal that anyone has run this on a modern stack. Audit the source before copying, particularly around how the API key is interpolated into URLs (the original builds query strings by concatenation in places — use http_build_query as above to avoid unescaped-key surprises).
Extension points: there are none for Laravel. The bundle's extension hooks (services.* overrides in Symfony config) don't translate. The right move is to keep the ported classes as plain App\Support\* value objects with constructor/config() injection, so you can swap in a GA4 wrapper or a StaticMap implementation backed by a different provider without touching views. Bind them in AppServiceProvider only if you actually need interfaces; otherwise static factories are fine and testable.
License and provenance. The fork inherits the upstream license; if you copy code rather than reimplement, preserve the header and record it. Given how little unique logic exists (a URL builder and two script templates), reimplementing from the config schema is usually cleaner than copying — and sidesteps the question entirely.
How can I help you explore Laravel packages today?