Skip to content

Store Locator / Maps

Flow ID: CF-28 | Module(s): maps, Map domain, SmartPoints | Complexity: Medium

Business Overview

Interactive store locator system that allows merchants to create multiple maps, each containing geolocated points (stores, branches, pickup locations). Visitors browse a map page by slug, see all location pins rendered on a Google Maps embed, and can search/filter to find the closest point.

What customers experience:

  • Navigate to /maps/{slug} to view a specific map with its locations
  • Google Maps embed renders all active location pins with markers
  • Each pin shows title, address, city, postal code, country, and description in an info window
  • Proximity search sorts pins by distance from a search query using Google Places API
  • Location data is multi-language -- content adapts to the active storefront language

What admins manage (legacy panel):

  • Create/edit maps with per-language titles, slugs, and optional background images
  • Add/edit/delete location points per map with GPS coordinates, country, and per-language address details
  • File uploads for map and location images stored in files/maps/
  • Role-gated: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING, AUTH_ROLE_MEDIA

Key business behaviors:

  • Locations with any empty required field (id, address, title, city, country, lat, lng, zip, description) are silently excluded from storefront rendering
  • Location data is mapped to SmartPointDTO -- the same DTO used for transporter pickup points -- enabling shared Google Maps UI components
  • Map slug is unique per language, auto-generated from title via greek_lower() with incrementing suffix on collision
  • Cache layer (pscache) wraps all model queries on storefront; clearCache('maps') invalidates on admin writes
  • Language links rendered automatically from maps_mui records for SEO hreflang

API Reference

REST Endpoints

MethodPathAuthDescription
GET/rest/map/mapGuestList maps (paginated, filterable)
GET/rest/map/map/{id}GuestGet single map by ID
GET/rest/map/map/itemGuestGet single map by filter
POST/rest/map/mapBackendCreate map (JSON or multipart with image)
POST/rest/map/map/{id}BackendUpdate map (JSON or multipart with image)
DELETE/rest/map/map/{id}BackendDelete map
GET/rest/map/locationGuestList locations (paginated, filterable)
GET/rest/map/location/{id}GuestGet single location by ID
GET/rest/map/location/itemGuestGet single location by filter
POST/rest/map/locationBackendCreate location (JSON)
POST/rest/map/location/{id}BackendUpdate location (JSON)
DELETE/rest/map/location/{id}BackendDelete location

All REST endpoints support locale-prefixed paths: /{locale}/rest/map/...

Map Filters & Sorts

FilterTypeExample
filter[id]Exact (multi-value)filter[id]=1,2,3
filter[active]Exactfilter[active]=1
filter[title.{locale}]Partial (LIKE)filter[title.el]=Athens
filter[slug.{locale}]Exactfilter[slug.en]=our-stores
SortDescription
id, activeRoot entity sorts
slug.{locale}, title.{locale}MUI translation sorts via subquery JOIN

Location Filters & Sorts

FilterTypeExample
filter[id]Exact (multi-value)filter[id]=5
filter[active]Exactfilter[active]=1
filter[country]Exactfilter[country]=GR
filter[city.{locale}]Exactfilter[city.el]=Athens
filter[postalCode.{locale}]Exactfilter[postalCode.el]=10431
SortDescription
id, active, countryRoot entity sorts
city.{locale}, postalCode.{locale}MUI translation sorts

Relations

EndpointRelationType
Maptranslationsone-to-many (maps_mui)
Maplocationsone-to-many (location)
Locationtranslationsone-to-many (location_mui)
Locationmapbelongs-to (maps)

Use ?with=translations,locations to include nested data.

Legacy Storefront

URLController MethodDescription
/maps/{slug}Adv_maps::index($slug)Render map page with all location pins

Legacy Admin Panel

URLController MethodDescription
/map_listAdv_maps_admin::index()List all maps
/map_list/addAdv_maps_admin::add()Create map form
/map_list/edit/{id}Adv_maps_admin::edit($id)Edit map form
/map_list/points/{mapId}Adv_maps_admin::points($mapId)List locations for a map
/map_list/add_point/{mapId}Adv_maps_admin::add_point($mapId)Add location to map
/map_list/edit_point/{id}Adv_maps_admin::edit_point($id)Edit location
/map_list/deletePoint/{mapId}/{pointId}Adv_maps_admin::deletePoint()Delete location
/map_list/deleteMap/{id}Adv_maps_admin::deleteMap($id)Delete map + its locations

Code Flow

Storefront Page Render

File: ecommercen/maps/controllers/Adv_maps.php

  1. Route dispatch: _remap() receives slug from URL, calls getMapContent($slug) via pscache
  2. Map lookup: Model JOINs maps + maps_mui on current language + slug match; 404 if not found
  3. Language links: renderMapLinks() fetches all MUI records for the map, builds hreflang URLs (maps/{slug} per language)
  4. Location points: getMapPoints($mapId) JOINs location + location_mui for current language
  5. DTO mapping: Each location is mapped to SmartPointDTO (shared with transporter pickup points); locations missing any required field are silently skipped
  6. View data: Points array injected into jsonState['mapPoints'] for Vue/JS, template renders mapPointsContainer layout
  7. JS bootstrap: Footer template detects isMapPointsPage flag, serializes points as window.mapPoints for the map-points.js script

REST Write Flow (Map)

File: src/Domains/Map/Map/WriteService.php

  1. Create: WriteData::fromArray() extracts isActive, image; Validator::validateForCreate() runs; translations parsed from translations[] array; transactional insert of main record + MUI rows
  2. Update: Same flow but uses toArray(excludeNull: true) to only update provided fields; MUI uses replace strategy (delete all + re-insert)
  3. Delete: Transactional delete of MUI rows first, then main record
  4. File upload (Map only): Map controller uses HandlesUploadActions with upload path files/maps/; accepts both application/json and multipart/form-data

REST Write Flow (Location)

File: src/Domains/Map/Location/WriteService.php

Identical transactional pattern to Map. Location uses HandlesWriteActions (JSON-only, no file upload via REST). Fields: mapId, country, latitude, longitude, image, active.


Domain Layer

Map Entity

ComponentPath
Entitysrc/Domains/Map/Map/Repository/Entity.php
MUI Entitysrc/Domains/Map/Map/Repository/MuiEntity.php
Repositorysrc/Domains/Map/Map/Repository/Repository.php (table: maps)
MUI Repositorysrc/Domains/Map/Map/Repository/MuiRepository.php (table: maps_mui)
RepositoryConfiguratorsrc/Domains/Map/Map/Repository/RepositoryConfigurator.php
Servicesrc/Domains/Map/Map/Service.php
WriteServicesrc/Domains/Map/Map/WriteService.php
WriteRepositorysrc/Domains/Map/Map/Repository/WriteRepository.php
MuiWriteRepositorysrc/Domains/Map/Map/Repository/MuiWriteRepository.php
Validatorsrc/Domains/Map/Map/Validator.php
WriteDatasrc/Domains/Map/Map/WriteData.php
MuiWriteDatasrc/Domains/Map/Map/MuiWriteData.php
ListRequestsrc/Domains/Map/Map/ListRequest.php
FilterByTranslationsrc/Domains/Map/Map/Repository/Specification/FilterByTranslation.php
SortByTranslationsrc/Domains/Map/Map/Repository/Specification/SortByTranslation.php

Location Entity

ComponentPath
Entitysrc/Domains/Map/Location/Repository/Entity.php
MUI Entitysrc/Domains/Map/Location/Repository/MuiEntity.php
Repositorysrc/Domains/Map/Location/Repository/Repository.php (table: location)
MUI Repositorysrc/Domains/Map/Location/Repository/MuiRepository.php (table: location_mui)
RepositoryConfiguratorsrc/Domains/Map/Location/Repository/RepositoryConfigurator.php
Servicesrc/Domains/Map/Location/Service.php
WriteServicesrc/Domains/Map/Location/WriteService.php
WriteRepositorysrc/Domains/Map/Location/Repository/WriteRepository.php
MuiWriteRepositorysrc/Domains/Map/Location/Repository/MuiWriteRepository.php
Validatorsrc/Domains/Map/Location/Validator.php
WriteDatasrc/Domains/Map/Location/WriteData.php
MuiWriteDatasrc/Domains/Map/Location/MuiWriteData.php
ListRequestsrc/Domains/Map/Location/ListRequest.php
FilterByTranslationsrc/Domains/Map/Location/Repository/Specification/FilterByTranslation.php
SortByTranslationsrc/Domains/Map/Location/Repository/Specification/SortByTranslation.php

REST Layer

ComponentPath
Map Controllersrc/Rest/Map/Controllers/Map.php (uses HandlesUploadActions)
Location Controllersrc/Rest/Map/Controllers/Location.php (uses HandlesWriteActions)
Map Resourcesrc/Rest/Map/Resources/Map/Resource.php
Map MUI Resourcesrc/Rest/Map/Resources/Map/MuiResource.php
Map Collectionsrc/Rest/Map/Resources/Map/Collection.php
Map MUI Collectionsrc/Rest/Map/Resources/Map/MuiCollection.php
Location Resourcesrc/Rest/Map/Resources/Location/Resource.php
Location MUI Resourcesrc/Rest/Map/Resources/Location/MuiResource.php
Location Collectionsrc/Rest/Map/Resources/Location/Collection.php
Location MUI Collectionsrc/Rest/Map/Resources/Location/MuiCollection.php

Shared DTO

ComponentPath
SmartPointDTOsrc/SmartPoints/DTO/SmartPointDTO.php

Architecture

Relationship Model

maps (1) ──── (*) maps_mui          [translations per language]

  └──── (*) location (1) ──── (*) location_mui   [translations per language]
  • Map -> Locations: one-to-many via location.map_id
  • Map -> Translations: one-to-many via maps_mui.map_id
  • Location -> Translations: one-to-many via location_mui.location_id
  • Location -> Map: belongs-to via location.map_id

DI Container

Registration FileScope
src/Domains/Map/container.phpDomain services (Map + Location read/write)
src/Rest/Map/container.phpREST controllers with injected services
application/config/container/modules.php (line 13)Domain module registration
application/config/container/modules.php (line 31)REST module registration

Map REST controller receives UploadService injection for image file handling. Location REST controller does not (JSON-only writes).

Frontend Architecture

Two rendering paths coexist:

Legacy JS (assets/main/js/map-points.js):

  • Used on storefront map pages (isMapPointsPage flag)
  • Reads window.mapPoints array serialized from controller
  • Initializes Google Maps centered on Greece (lat: 38.13, lng: 22.24)
  • Adds markers, implements text search via Places API with 200km distance filter
  • Filters the store listing DOM (<ul class="store_listing">) to match search results

Vue/Vuex + GoogleMapsService (assets/main/vue/googleMaps/):

  • Shared service class used by both store locator pages and checkout smart points (transporter pickup)
  • GoogleMapsService loads Google Maps API via google-maps Loader, supports marker clustering (@googlemaps/markerclusterer)
  • googleMapMixin.js provides search, distance sorting (spherical geometry), geolocation, and point selection logic
  • googleMap.vue is a slot-based container component (header, search, listing, map slots)
  • Vuex state: mapPoints, googleService, googleMapLoading, googleMapsApiKey
  • InitialData.vue hydrates mapPoints from window.vueData.initialData

Map options (assets/main/vue/googleMaps/googleMapOptions.js): Default center Athens (37.98, 23.73), zoom 4, roadmap type, cooperative gesture handling.


Data Model

maps

ColumnTypeDescription
idINT, PK, AIMap identifier
activeBOOLWhether map is published
imageVARCHAR(255), NULLBackground/header image filename (added in migration 20251030175615)

maps_mui

ColumnTypeDescription
idINT, PK, AITranslation row ID
map_idINT, FKParent map
titleVARCHARMap title
slugVARCHARURL slug (unique per language)
langVARCHARLanguage code

location

ColumnTypeDescription
idINT, PK, AILocation identifier
map_idINT, FKParent map
countryVARCHARCountry name or code
latitudeVARCHARGPS latitude
longitudeVARCHARGPS longitude
imageVARCHARLocation/pin image filename
activeBOOLWhether location is active

location_mui

ColumnTypeDescription
idINT, PK, AITranslation row ID
location_idINT, FKParent location
titleVARCHARLocation name
addressVARCHARStreet address
cityVARCHARCity name
zip_codeVARCHARPostal code
descriptionTEXTRich description/notes
langVARCHARLanguage code

Configuration

Registry Settings (DB)

GroupKeyDescription
GOOGLE_MAPSAPI_KEYGoogle Maps JavaScript API key
GOOGLE_MAPSDEFAULT_LOCATION_CENTERDefault map center coordinates

Configured in Admin > Settings > Google (ecommercen/settings/controllers/Adv_settings.php). The API key is injected into the frontend config object (Adv_front_controller.php line 723) and consumed by GoogleMapsService via Vuex getter googleMapsApiKey.

Routes

Storefront (application/config/routes.php):

/maps           -> maps controller
/maps/{slug}    -> maps/$1
/map_list       -> maps/maps_admin (admin)
/map_list/{..}  -> maps/maps_admin/$1

REST (application/config/rest_routes.php, lines 210-235): All Map and Location CRUD routes with locale-prefixed variants.

Template Layout

The storefront map page uses the mapPointsContainer layout defined in application/config/mainTemplate.json:

json
"mapPointsContainer": {
    "view": "layouts/maps/map_points",
    "scss": ""
}

Client Extension Points

Admin Controller Hooks

Adv_maps_admin provides empty protected methods for client overrides:

MethodPurpose
beforeAddMapRecord($data, $muiData)Transform data before map insert
beforeEditMapRecord($id, $data, $muiData)Transform data before map update
beforeAddPointRecord($data, $muiData)Transform data before location insert
beforeEditPointRecord($id, $data, $muiData)Transform data before location update
setNewMapRelations($entityId)Create related records after map insert
setUpdateMapRelations($entityId)Update related records after map edit
setNewPointRelations($entityId)Create related records after location insert
setUpdatePointRelations($entityId)Update related records after location edit
indexRender()Inject extra view data on map listing
addRender() / editRender()Inject extra view data on map forms
pointRender()Inject extra view data on location listing
addPointRender() / editPointRender()Inject extra view data on location forms

Validation Overrides

  • mapValidation($isUpdate) -- override to add custom map form rules
  • pointValidation() -- override to add custom location form rules

DI Overrides (Client Repos)

Client repos can override domain services by aliasing in custom/Domains/container.php:

php
$services->set(\Custom\Domains\Map\Location\Repository\RepositoryConfigurator::class);
$services->alias(
    \Advisable\Domains\Map\Location\Repository\RepositoryConfigurator::class,
    \Custom\Domains\Map\Location\Repository\RepositoryConfigurator::class
);

SmartPointDTO Reuse

The SmartPointDTO is shared between store locator and transporter smart points (checkout pickup). Client repos extending transporter integration automatically benefit from the same Google Maps UI infrastructure.


Business Rules

RuleImplementation
Slug uniqueness per languagecreateSlug() in model -- greek_lower() with incrementing suffix
Incomplete locations excludedStorefront array_reduce filter checks all 9 required fields
Cache invalidationclearCache('maps') called on every admin write operation
Map deletion cascadesdeleteMap() removes maps, maps_mui, location (by map_id), and location_mui (by location_id)
Location deletion cascadesdeletePoint() removes location_mui first, then location row
MUI replace strategy (REST)On update, all MUI rows are deleted and re-inserted within a transaction
Translation language requiredValidator enforces lang field present on every translation entry
Upload pathMap images stored in files/maps/ (both map and location images)