---
title: "Geo-targeting — Advertisements for Laravel | Roundly"
description: "Restrict ads to allowed or blocked countries or a radius around a point, and serve each viewer only the ads their location matches."
url: https://roundly-consulting.com/open-source/docs/advertisements-for-laravel/geo-targeting
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Advertisements for Laravel](https://roundly-consulting.com/open-source/docs/advertisements-for-laravel.md)

# Geo-targeting

Ads declare targeting through the Targeting value object — allowed and blocked countries and/or a radius around a point. At serve time, resolve the active ads for a placement that match the viewer’s location:

```php
use RoundlyConsulting\Advertisements\Facades\Advertisements;
use RoundlyConsulting\Advertisements\ValueObjects\Targeting;
use RoundlyConsulting\Geolocation\DataTransferObjects\Coordinates;

$ad->targeting = new Targeting(
    countries: ['SK', 'CZ'],                 // allow list (empty = any)
    excludeCountries: ['RU'],                // deny list
    center: new Coordinates(48.1486, 17.1077),
    radiusKm: 50.0,
);
$ad->save();

// Resolve the viewer from the request IP (geolocation), or pass a Location / null:
$ads = Advertisements::targetedIn('sidebar', request())->get();

// Or the model scope directly:
Advertisement::query()->active()->forPlacement('sidebar')->targetedAt($location)->get();
```

targetedIn() resolves a Request — or the current request when you pass nothing — to a location through geolocation-for-laravel, or takes an already-resolved Location. A lookup that throws is reported and treated as an unknown viewer, so geolocation never takes serving down. With geo.targeting\_enabled off, it’s identical to in().

## Matching rules

- excludeCountries — deny list of ISO codes, checked first.
- countries — allow list; empty means any country.
- center + radiusKm — the viewer must be within radiusKm of the centre (exact Haversine). Applies only when both are set.
- An ad matches when the viewer satisfies every rule it sets: the allow/deny lists check the viewer’s country, the radius checks the viewer’s coordinates.
- An ad with no targeting (null, or an empty new Targeting(), which is stored as no targeting) matches every viewer while geo.untargeted\_match is true.

## Unknown viewers and missing facts

The viewer can be a Location, bare Coordinates, an ISO country code, a Request (resolved from its IP) or null. What happens when something about the viewer is unknown is decided by geo.match\_when\_unknown:

| The viewer is… | untargeted\_only (default) | all |
| --- | --- | --- |
| unknown — null, an IP geolocation can’t place (or whose lookup throws), an empty country code, a Location with neither country nor coordinates | only untargeted ads | every ad |
| known only by country ('US', a Location at 0,0) | a radius rule fails | a radius rule passes |
| known only by coordinates (Coordinates, a Location without a country) | a country list fails | a country list passes |

So under the default a viewer known only as 'US' is never served an ad restricted to 50 km around Bratislava, and bare New York coordinates never get an SK-only ad.

## The targetedAt() scope

The scope behind targetedIn() accepts four viewer shapes:

```php
use RoundlyConsulting\Geolocation\DataTransferObjects\Coordinates;

Advertisement::query()->active()->targetedAt($location)->get();   // Location — country lists + radius
Advertisement::query()->active()->targetedAt('SK')->get();        // country only — a radius rule can't be checked
Advertisement::query()->active()
    ->targetedAt(new Coordinates(48.1486, 17.1077))->get();       // coordinates only — a country list can't be checked
Advertisement::query()->active()->targetedAt(null)->get();        // unknown viewer — per geo.match_when_unknown
```

Targeting is evaluated precisely in PHP over the candidate set the surrounding query already narrowed, so chain active() and forPlacement() before it.

## Reading and clearing targeting

```php
$targeting = $ad->targeting;         // ?Targeting
$targeting?->isEmpty();              // no country list and no radius (centre + radiusKm)
$targeting?->matches('SK');          // Location, Coordinates or a country code
$targeting?->matches('SK', true);    // a rule the viewer lacks the fact for counts as met
$targeting?->toArray();              // ['countries' => [...], 'exclude_countries' => [...], 'center' => [...], 'radius_km' => 50.0]

$ad->targeting = Targeting::fromArray([   // snake_case payload, e.g. from a form
    'countries' => ['sk', 'cz'],          // normalised to upper case
    'center' => ['latitude' => 48.1486, 'longitude' => 17.1077],
    'radius_km' => 25,
]);

$ad->targeting = null;                    // untargeted again
$ad->save();
```

The cast stores targeting as JSON and copies the centre into the target\_latitude and target\_longitude columns. Assign null (or an empty Targeting) to clear it; any value other than a Targeting or null throws InvalidTargeting. A centre without a radius, or a radius without a centre, isn’t a rule.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/advertisements-for-laravel.md)
