NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Events for Laravel

Venues and near-me search

A venue is reusable across events, and its timezone decides how every wall-clock time of its events is read:

$venue = Events::venues()->make(['en' => 'City Park', 'sk' => 'Mestský park'])
    ->address(street: 'Námestie slobody', city: 'Bratislava', region: 'BA', postalCode: '811 06', country: 'SK')
    ->formattedAddress('Námestie slobody, 811 06 Bratislava')   // optional; else built from the parts
    ->coordinates(48.1530, 17.1130)                             // or a Coordinates
    ->timezone('Europe/Bratislava')
    ->capacity(500)
    ->ownedBy($team)                                            // "my venues"
    ->meta(['floor' => 1])                                      // private: never in API output
    ->by($user)                                                 // checks the venue policy
    ->create();

A name is required (per locale), the country is an ISO 3166-1 alpha-2 code (upper-cased), the timezone a valid IANA name (else events.venues.default_timezone, else the app’s), coordinates within range and capacity ≥ 0 — informational, since the event’s own capacity counts seats. Blank address parts are stored as null.

Updating and the other operations

use RoundlyConsulting\Events\DataTransferObjects\AddressData;
use RoundlyConsulting\Events\DataTransferObjects\UpdateVenueData;
use RoundlyConsulting\Events\DataTransferObjects\VenueData;

Events::venues()->by($user)->update($venue, new UpdateVenueData(capacity: 600));

// In PHP an address is one value: it replaces all six address columns, so pass every part you keep.
// (The API's PATCH changes each address field on its own.)
Events::venues()->update($venue, new UpdateVenueData(address: new AddressData(
    street: 'Hlavná 1', city: 'Košice', postalCode: '040 01', country: 'SK',
)));

// A DTO may carry its own actor; on a by() accessor it is that actor (another one is refused).
Events::venues()->create(new VenueData(name: 'Studio', owner: $team, actor: $user));
Events::venues()->by($user)->create(new VenueData(name: 'Studio', owner: $team));   // the same check
$venues = Events::venues()->by($user);   // checks the VenuePolicy on every following write

$venues->geocode($venue);               // look the coordinates up now (only missing ones unless force: true)
$venues->delete($venue);                // soft; refused (venue_in_use) while an unfinished event uses it
$venues->restore($venue);

Events::venues()->find(7);
Events::venues()->ownedBy($team)->get();          // an organizer's own list
Events::venues()->near($here, km: 10)->get();     // venues within 10 km

$venue->coordinates;            // ?Coordinates
$venue->formattedAddress();     // the stored one-line address, else the parts joined
  • update() changes only the fields given. A new name, address or coordinates raise the calendar SEQUENCE of the venue’s announced events that are not over, so subscribed calendars take the new LOCATION and GEO.
  • delete() is a soft delete, refused while a draft, scheduled, postponed or rescheduled event uses the venue. Past events keep their trashed venue ($event->venue includes trashed venues) and the API renders it with deleted: true.

Who may change a venue

Venues are physical places: any organizer may hold an event at any venue, owned or shared. Ownership decides only who edits it:

VenueCreated byUpdated, geocoded, deleted, restored by
Owned (ownedBy($team))Someone the owner allows (allowsEventManagementBy(); for a plain model, the model itself), or an admin.The owner’s approved users and admins.
Shared (no owner)Admins and holders of events.authorization.abilities.shared_venues.The same.

Policies\VenuePolicy has viewAny and view (anyone), create (authenticated, plus events.authorization.abilities.venues when set), update, delete, restore and geocode (the rules above) and manageShared (admins and abilities.shared_venues; null means admins only). Swap it with events.authorization.venue_policy. Over the API a venue created without an owner is the caller’s own — shared: true asks for a shared one. Trusted PHP (no by()) is never checked, so seeders and imports still create shared venues.

Geocoding

Geocoding goes through the address pipeline of Geolocation for Laravel, and only when you ask for it:

  • ->geocode() on the builder, or Events::venues()->geocode($venue) — now.
  • events.venues.geocode_on_create = on — every new venue without coordinates, queued after commit (Jobs\GeocodeVenueJob). A rate limit releases the job for a retry after the provider’s retryAfterSeconds(); a deleted venue is skipped.
  • php artisan events:geocode in bulk, and POST /api/event-venues/{venue}/geocode, which always re-geocodes.
php artisan events:geocode                 # venues without coordinates (--missing, the default)
php artisan events:geocode --venue=7       # one venue (skipped when it has coordinates, unless --force)
php artisan events:geocode --force         # every venue again
php artisan events:geocode --chunk=200     # venues per batch (default 100)

The lookup sends formattedAddress() to geolocation’s locateAddress(); a venue without any address is a failure. Only missing coordinates are filled unless forced, and the timezone is never touched. A failure keeps the venue and fires VenueGeocodingFailed — also when the pipeline returns its trailing default location, which would put every venue on the same spot. Success fires VenueGeocoded.

Near me

use RoundlyConsulting\Geolocation\DataTransferObjects\Coordinates;
use RoundlyConsulting\Geolocation\Facades\Geolocation;

$here = Geolocation::locateRequest($request)?->coordinates()   // the visitor's IP — see Privacy
    ?? new Coordinates(48.1486, 17.1077);

Events::browse($viewer)
    ->upcoming()
    ->near($here, km: 25)        // a bounding box on the venue's indexed coordinates, then the exact distance
    ->withDistance($here)        // adds distance_km (null for an event without a venue)
    ->orderByDistance($here)     // nearest first; events without a venue last in either direction
    ->paginate(20);
  • km must be in (0, events.geo.max_radius_km] (500); omitted means events.geo.default_radius_km (25). Online-only events never match near().
  • withDistance() keeps the existing select (events.* when there is none); its bindings sit in the select bucket and the sort in the order bucket, so pagination counts stay correct on every engine. The scopes take named arguments — near($here, km: 25).
  • The distance is the same great-circle formula as geolocation’s Coordinates::distanceTo() (R = 6371.0 km, clamped so antipodal points never error), so SQL and PHP agree within a metre on MySQL and PostgreSQL. SQLite — tests only — uses a registered function, events_distance_km().
  • Geolocation::locateRequest($request) sends the visitor’s IP address to geolocation’s IP providers — mention it in your privacy notice, or ask for the location instead.

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

By donating, you agree to our donation terms.

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.