Skip to content

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 ?? '';
    }
}
  1. Creates a contract_id column in the contractreminder table.

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(),
        ];
    }
}
  1. allowAdd(true) shows an "Add" button on the inline list, letting users create child records directly from the parent.

Parameters

1
2
3
4
5
$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:

1
2
3
$this->hasParent('company', Company::class)
    ->selectAjaxThreshold(10)    // use AJAX when more than 10 options
    ->selectAjaxMinChars(2)      // require 2 chars before searching