# 0011. Locations, offices and ad spaces share one table

- **Status:** Accepted
- **Date:** 2026-06-10

## Context

Beyond the storage sites, the business has offices and rented advertising spaces. All three are a named place
with an address, photos, attachments, events and — for sites — a floor map. Only storage sites are public.

## Decision

**One `location` table, discriminated by a `type` column**, with two single-table subclasses:

```php
class Office extends Location {
    public static function tableName() : string { return Location::tableName(); }
    public function beforeValidate() : bool { …; $this->type = self::TYPE_OFFICE; $this->isPublished = false; … }
    public function beforeSave( $insert ) : bool { …; $this->type = self::TYPE_OFFICE; … }
}
```

`AdSpace` is the same for `TYPE_AD_SPACE`. Both force `isPublished = false` — an office is never a public
page.

The admin presents three screens (`LocationsController`, `OfficesController`, `AdSpacesController`) over the
same table, separated by their filter.

## Alternatives

- **Three tables.** Three copies of address, photos, attachments and events, and three sets of relations from
  everything that points at a place.
- **One `Location` model with no subclasses**, and the type set by the controller. Rejected: nothing would
  stop an office being saved as a storage site, and the `isPublished` rule would live in the admin rather
  than the model.
- **A shared `Place` base with three child tables** (class-table inheritance). Correct in the abstract, and a
  join on every read for a distinction that only matters in the back office.

## Consequences

- **The type is enforced in the model**, twice — `beforeValidate()` and `beforeSave()` — so it cannot be
  bypassed by a mass assignment or a direct save.
- **One set of relations.** `Payment.locationId`, `LocationEvent.locationId` and the gate device columns all
  point at one table regardless of type.
- **A field change on `Location` shows up on all three admin screens** unless the field declarations differ
  per controller.
- **`type` is a plain string, not an enum table.** A typo produces a row that appears on no screen at all.
- The dashboard counts them separately with `counterWhere`, and the location-event reminder notification maps
  the type back to the right controller for its link.
- Gate control columns live here because the gate belongs to the site, not to a unit — see
  [0015](0015-gate-control-through-a-queue.md).
