Appearance
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_muirecords for SEO hreflang
API Reference
REST Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/map/map | Guest | List maps (paginated, filterable) |
| GET | /rest/map/map/{id} | Guest | Get single map by ID |
| GET | /rest/map/map/item | Guest | Get single map by filter |
| POST | /rest/map/map | Backend | Create map (JSON or multipart with image) |
| POST | /rest/map/map/{id} | Backend | Update map (JSON or multipart with image) |
| DELETE | /rest/map/map/{id} | Backend | Delete map |
| GET | /rest/map/location | Guest | List locations (paginated, filterable) |
| GET | /rest/map/location/{id} | Guest | Get single location by ID |
| GET | /rest/map/location/item | Guest | Get single location by filter |
| POST | /rest/map/location | Backend | Create location (JSON) |
| POST | /rest/map/location/{id} | Backend | Update location (JSON) |
| DELETE | /rest/map/location/{id} | Backend | Delete location |
All REST endpoints support locale-prefixed paths: /{locale}/rest/map/...
Map Filters & Sorts
| Filter | Type | Example |
|---|---|---|
filter[id] | Exact (multi-value) | filter[id]=1,2,3 |
filter[active] | Exact | filter[active]=1 |
filter[title.{locale}] | Partial (LIKE) | filter[title.el]=Athens |
filter[slug.{locale}] | Exact | filter[slug.en]=our-stores |
| Sort | Description |
|---|---|
id, active | Root entity sorts |
slug.{locale}, title.{locale} | MUI translation sorts via subquery JOIN |
Location Filters & Sorts
| Filter | Type | Example |
|---|---|---|
filter[id] | Exact (multi-value) | filter[id]=5 |
filter[active] | Exact | filter[active]=1 |
filter[country] | Exact | filter[country]=GR |
filter[city.{locale}] | Exact | filter[city.el]=Athens |
filter[postalCode.{locale}] | Exact | filter[postalCode.el]=10431 |
| Sort | Description |
|---|---|
id, active, country | Root entity sorts |
city.{locale}, postalCode.{locale} | MUI translation sorts |
Relations
| Endpoint | Relation | Type |
|---|---|---|
| Map | translations | one-to-many (maps_mui) |
| Map | locations | one-to-many (location) |
| Location | translations | one-to-many (location_mui) |
| Location | map | belongs-to (maps) |
Use ?with=translations,locations to include nested data.
Legacy Storefront
| URL | Controller Method | Description |
|---|---|---|
/maps/{slug} | Adv_maps::index($slug) | Render map page with all location pins |
Legacy Admin Panel
| URL | Controller Method | Description |
|---|---|---|
/map_list | Adv_maps_admin::index() | List all maps |
/map_list/add | Adv_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
- Route dispatch:
_remap()receives slug from URL, callsgetMapContent($slug)via pscache - Map lookup: Model JOINs
maps+maps_muion current language + slug match; 404 if not found - Language links:
renderMapLinks()fetches all MUI records for the map, builds hreflang URLs (maps/{slug}per language) - Location points:
getMapPoints($mapId)JOINslocation+location_muifor current language - DTO mapping: Each location is mapped to
SmartPointDTO(shared with transporter pickup points); locations missing any required field are silently skipped - View data: Points array injected into
jsonState['mapPoints']for Vue/JS, template rendersmapPointsContainerlayout - JS bootstrap: Footer template detects
isMapPointsPageflag, serializes points aswindow.mapPointsfor the map-points.js script
REST Write Flow (Map)
File: src/Domains/Map/Map/WriteService.php
- Create:
WriteData::fromArray()extractsisActive,image;Validator::validateForCreate()runs; translations parsed fromtranslations[]array; transactional insert of main record + MUI rows - Update: Same flow but uses
toArray(excludeNull: true)to only update provided fields; MUI uses replace strategy (delete all + re-insert) - Delete: Transactional delete of MUI rows first, then main record
- File upload (Map only): Map controller uses
HandlesUploadActionswith upload pathfiles/maps/; accepts bothapplication/jsonandmultipart/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
| Component | Path |
|---|---|
| Entity | src/Domains/Map/Map/Repository/Entity.php |
| MUI Entity | src/Domains/Map/Map/Repository/MuiEntity.php |
| Repository | src/Domains/Map/Map/Repository/Repository.php (table: maps) |
| MUI Repository | src/Domains/Map/Map/Repository/MuiRepository.php (table: maps_mui) |
| RepositoryConfigurator | src/Domains/Map/Map/Repository/RepositoryConfigurator.php |
| Service | src/Domains/Map/Map/Service.php |
| WriteService | src/Domains/Map/Map/WriteService.php |
| WriteRepository | src/Domains/Map/Map/Repository/WriteRepository.php |
| MuiWriteRepository | src/Domains/Map/Map/Repository/MuiWriteRepository.php |
| Validator | src/Domains/Map/Map/Validator.php |
| WriteData | src/Domains/Map/Map/WriteData.php |
| MuiWriteData | src/Domains/Map/Map/MuiWriteData.php |
| ListRequest | src/Domains/Map/Map/ListRequest.php |
| FilterByTranslation | src/Domains/Map/Map/Repository/Specification/FilterByTranslation.php |
| SortByTranslation | src/Domains/Map/Map/Repository/Specification/SortByTranslation.php |
Location Entity
| Component | Path |
|---|---|
| Entity | src/Domains/Map/Location/Repository/Entity.php |
| MUI Entity | src/Domains/Map/Location/Repository/MuiEntity.php |
| Repository | src/Domains/Map/Location/Repository/Repository.php (table: location) |
| MUI Repository | src/Domains/Map/Location/Repository/MuiRepository.php (table: location_mui) |
| RepositoryConfigurator | src/Domains/Map/Location/Repository/RepositoryConfigurator.php |
| Service | src/Domains/Map/Location/Service.php |
| WriteService | src/Domains/Map/Location/WriteService.php |
| WriteRepository | src/Domains/Map/Location/Repository/WriteRepository.php |
| MuiWriteRepository | src/Domains/Map/Location/Repository/MuiWriteRepository.php |
| Validator | src/Domains/Map/Location/Validator.php |
| WriteData | src/Domains/Map/Location/WriteData.php |
| MuiWriteData | src/Domains/Map/Location/MuiWriteData.php |
| ListRequest | src/Domains/Map/Location/ListRequest.php |
| FilterByTranslation | src/Domains/Map/Location/Repository/Specification/FilterByTranslation.php |
| SortByTranslation | src/Domains/Map/Location/Repository/Specification/SortByTranslation.php |
REST Layer
| Component | Path |
|---|---|
| Map Controller | src/Rest/Map/Controllers/Map.php (uses HandlesUploadActions) |
| Location Controller | src/Rest/Map/Controllers/Location.php (uses HandlesWriteActions) |
| Map Resource | src/Rest/Map/Resources/Map/Resource.php |
| Map MUI Resource | src/Rest/Map/Resources/Map/MuiResource.php |
| Map Collection | src/Rest/Map/Resources/Map/Collection.php |
| Map MUI Collection | src/Rest/Map/Resources/Map/MuiCollection.php |
| Location Resource | src/Rest/Map/Resources/Location/Resource.php |
| Location MUI Resource | src/Rest/Map/Resources/Location/MuiResource.php |
| Location Collection | src/Rest/Map/Resources/Location/Collection.php |
| Location MUI Collection | src/Rest/Map/Resources/Location/MuiCollection.php |
Shared DTO
| Component | Path |
|---|---|
| SmartPointDTO | src/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 File | Scope |
|---|---|
src/Domains/Map/container.php | Domain services (Map + Location read/write) |
src/Rest/Map/container.php | REST 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 (
isMapPointsPageflag) - Reads
window.mapPointsarray 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)
GoogleMapsServiceloads Google Maps API viagoogle-mapsLoader, supports marker clustering (@googlemaps/markerclusterer)googleMapMixin.jsprovides search, distance sorting (spherical geometry), geolocation, and point selection logicgoogleMap.vueis a slot-based container component (header,search,listing,mapslots)- Vuex state:
mapPoints,googleService,googleMapLoading,googleMapsApiKey InitialData.vuehydratesmapPointsfromwindow.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
| Column | Type | Description |
|---|---|---|
id | INT, PK, AI | Map identifier |
active | BOOL | Whether map is published |
image | VARCHAR(255), NULL | Background/header image filename (added in migration 20251030175615) |
maps_mui
| Column | Type | Description |
|---|---|---|
id | INT, PK, AI | Translation row ID |
map_id | INT, FK | Parent map |
title | VARCHAR | Map title |
slug | VARCHAR | URL slug (unique per language) |
lang | VARCHAR | Language code |
location
| Column | Type | Description |
|---|---|---|
id | INT, PK, AI | Location identifier |
map_id | INT, FK | Parent map |
country | VARCHAR | Country name or code |
latitude | VARCHAR | GPS latitude |
longitude | VARCHAR | GPS longitude |
image | VARCHAR | Location/pin image filename |
active | BOOL | Whether location is active |
location_mui
| Column | Type | Description |
|---|---|---|
id | INT, PK, AI | Translation row ID |
location_id | INT, FK | Parent location |
title | VARCHAR | Location name |
address | VARCHAR | Street address |
city | VARCHAR | City name |
zip_code | VARCHAR | Postal code |
description | TEXT | Rich description/notes |
lang | VARCHAR | Language code |
Configuration
Registry Settings (DB)
| Group | Key | Description |
|---|---|---|
GOOGLE_MAPS | API_KEY | Google Maps JavaScript API key |
GOOGLE_MAPS | DEFAULT_LOCATION_CENTER | Default 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/$1REST (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:
| Method | Purpose |
|---|---|
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 rulespointValidation()-- 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
| Rule | Implementation |
|---|---|
| Slug uniqueness per language | createSlug() in model -- greek_lower() with incrementing suffix |
| Incomplete locations excluded | Storefront array_reduce filter checks all 9 required fields |
| Cache invalidation | clearCache('maps') called on every admin write operation |
| Map deletion cascades | deleteMap() removes maps, maps_mui, location (by map_id), and location_mui (by location_id) |
| Location deletion cascades | deletePoint() 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 required | Validator enforces lang field present on every translation entry |
| Upload path | Map images stored in files/maps/ (both map and location images) |
Related Flows
- CF-06 Order Preview -- checkout smart points (transporter pickup) reuse the same Google Maps infrastructure and
SmartPointDTO - SY-23 MUI Translation Pattern — how
maps_muiandlocation_muitables power multi-language map and location content - SY-28 Storage Abstraction — map and location image files stored via the storage abstraction layer