Relations
Office App supports two relationship types: hasParent (many-to-one) and hasMany (one-to-many). Together they create linked record hierarchies.
hasParent — "This Record Belongs To..."
A hasParent reference creates a foreign key column on the current model's table, pointing to a parent record.
Basic Example
A ContractReminder belongs to a Contract:
| <?php
namespace App\Model\Contracts;
use System\Model\DefaultModel;
use System\ModelMeta;
use System\StringProperty;
use System\DateProperty;
use System\TextProperty;
class ContractReminder extends DefaultModel
{
public static function meta(): ?ModelMeta
{
return (new ModelMeta('Reminders', 'bell'))
->withSingular('Reminder')
->enable();
}
protected function getAdditionalProperties(): array
{
return [
$this->hasParent('contract', Contract::class), // (1)
new DateProperty('due_date'),
(new StringProperty('subject'))->required(),
(new TextProperty('note'))->asRichtextEditor(),
];
}
public function getLabel(): string
{
return $this->subject ?? '';
}
}
|
- Creates a
contract_id column in the contractreminder table.
Navigation Modes
Control how users navigate to the parent record:
| use System\ReferenceProperty;
// Open parent in a popup dialog (default)
$this->hasParent('contract', Contract::class)
->navLinks(ReferenceProperty::NAV_MODAL)
// Navigate in the same tab
$this->hasParent('contract', Contract::class)
->navLinks(ReferenceProperty::NAV_INLINE)
// Multiple navigation options
$this->hasParent('contract', Contract::class)
->navLinks(ReferenceProperty::NAV_MODAL, ReferenceProperty::NAV_TAB, ReferenceProperty::NAV_PIN)
// All available modes
$this->hasParent('company', Company::class)
->navLinks(ReferenceProperty::NAV_ALL)
|
Available modes:
| Constant |
Behavior |
NAV_MODAL |
Open in popup dialog |
NAV_INLINE |
Navigate in same tab |
NAV_TAB |
Open in new browser tab |
NAV_PIN |
Pin to side panel |
NAV_PERMALINK |
Copy permalink to clipboard |
NAV_ALL |
All of the above |
NAV_NONE |
No navigation links |
Calendar Resource
Mark a parent as a calendar resource dimension (for resource timeline views):
| $this->hasParent('type', ContractType::class)->asResource()
|
hasMany — "This Record Has Child Records"
A hasMany relation shows child records as an inline list on the parent's detail view.
Basic Example
A Contract has many ContractReminder and ContractAmendment records:
| class Contract extends DefaultModel
{
protected function getAdditionalProperties(): array
{
return [
(new StringProperty('title'))->autofocus(),
new DateProperty('start_date'),
new DateProperty('end_date'),
new CurrencyProperty('value'),
// Child relations
$this->hasMany('reminders', ContractReminder::class, 'contract_id')
->allowAdd(true), // (1)
$this->hasMany('amendments', ContractAmendment::class, 'contract_id')
->allowAdd(true),
new StateProperty(),
];
}
}
|
allowAdd(true) shows an "Add" button on the inline list, letting users create child records directly from the parent.
Parameters
| $this->hasMany(
'reminders', // property name (used in layouts)
ContractReminder::class, // child model class
'contract_id' // foreign key column on the child table
)
|
How It Works
- The child model must have a
hasParent(...) pointing back to this model.
- The framework auto-detects the relationship and shows child records as a tab or inline list.
- The foreign key is automatically set when creating a child from the parent view.
Complete Example: Contract with Children
Here's a real-world example showing a parent model with two child models.
Parent: Contract
| <?php
namespace App\Model\Contracts;
use System\Model\DefaultModel;
use System\ModelMeta;
use System\StringProperty;
use System\DateProperty;
use System\CurrencyProperty;
use System\PdfProperty;
use System\StateProperty;
use System\TodoListProperty;
class Contract extends DefaultModel
{
public static function meta(): ?ModelMeta
{
return (new ModelMeta('Contracts', 'file-text'))
->withSingular('Contract')
->enable();
}
protected function getAdditionalProperties(): array
{
return [
(new StringProperty('title'))->autofocus(),
$this->hasParent('type', ContractType::class),
$this->hasParent('party', ContractParty::class),
new StringProperty('reference'),
new DateProperty('start_date'),
new DateProperty('end_date'),
new CurrencyProperty('value'),
(new PdfProperty('document'))->asArray(0, 3),
// Child relations
$this->hasMany('reminders', ContractReminder::class, 'contract_id')
->allowAdd(true),
$this->hasMany('amendments', ContractAmendment::class, 'contract_id')
->allowAdd(true),
new TodoListProperty('todo'),
new StateProperty(),
$this->enableVersioning(),
];
}
public function getLabel(): string
{
return $this->title ?? '';
}
}
|
Child: ContractReminder
| <?php
namespace App\Model\Contracts;
use System\Model\DefaultModel;
use System\ModelMeta;
use System\StringProperty;
use System\DateProperty;
use System\TextProperty;
use System\TodoListProperty;
class ContractReminder extends DefaultModel
{
public static function meta(): ?ModelMeta
{
return (new ModelMeta('Reminders', 'bell'))
->withSingular('Reminder')
->enable();
}
protected function getAdditionalProperties(): array
{
return [
$this->hasParent('contract', Contract::class), // FK back to parent
(new DateProperty('due_date'))->required(),
(new StringProperty('subject'))->required(),
(new TextProperty('note'))->asRichtextEditor(),
new TodoListProperty('todo'),
];
}
public function getLabel(): string
{
return ($this->subject ?? '') . ' — ' . ($this->due_date ?? '');
}
}
|
Lookup Model: ContractType
Simple lookup models provide dropdown options for parent references:
| <?php
namespace App\Model\Contracts;
use System\Model\DefaultModel;
use System\ModelMeta;
use System\StringProperty;
class ContractType extends DefaultModel
{
public static function meta(): ?ModelMeta
{
return (new ModelMeta('Contract Types', 'tag'))
->withSingular('Contract Type')
->enable();
}
protected function getAdditionalProperties(): array
{
return [
(new StringProperty('name'))->autofocus(),
];
}
public function getLabel(): string
{
return $this->name ?? '';
}
}
|
ReferenceProperty — Manual Foreign Key
If you need more control than hasParent(), use ReferenceProperty directly:
| use System\ReferenceProperty;
// Single reference (stores company_id column)
new ReferenceProperty('company', Company::class,
fn() => $this->database->find(Company::class, [])->toLabelMap()
)
// Array reference (stores in sys_reference table, renders as tag pills)
(new ReferenceProperty('companies', Company::class,
fn() => $this->database->find(Company::class, [])->toLabelMap()
))->asArray()
|
AJAX Threshold
For large datasets, the dropdown automatically switches to AJAX search above a threshold:
| $this->hasParent('company', Company::class)
->selectAjaxThreshold(10) // use AJAX when more than 10 options
->selectAjaxMinChars(2) // require 2 chars before searching
|