Skip to content

Store Locator / Maps ​

Flow ID: CF-28 | Module(s): maps, Map domain, SmartPoints | Complexity: Medium | Last Updated: 2026-09-14

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 239-265): 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)

Known Issues & Security Gaps ​

No known issues at this time.