This is an old revision of the document!
Table of Contents
_DXOBaseModel (PHP)
Generic Base Model providing shared ORM-like functionality.
- Author: dynaxio AG
- Created: 2025-11-06
- Updated: 2025-11-11
- Version: 1.4.1
- License: See
LICENSE.mdin the project root.
Provides dynamic getters/setters, automatic persistence, per-class caching, consistency-edges registration, and policy-aware deletion via _destroyMySelf().
Edges and policies may use \Lib\GlobalOptions::* identifiers for table names (resolved to physical tables at runtime).
Example: minimal child model
namespace Lib\Model; use Lib\_DXOBaseModel; class Partner extends _DXOBaseModel { protected ?string $table = null; // Outbound relations from Partner rows protected array $edgesMap = [ 'owner_user_id' => [ 'relation' => 'partner:owner', 'dst_table' => 'GlobalOptions::CPTDB_TBL_USERS', ], 'account_key' => [ 'relation' => 'partner:account', 'dst_table' => 'GlobalOptions::CPTDB_TBL_ACCOUNTS', 'resolve_by' => 'uniqueid', // external key → accounts.id ], ]; public function __construct(int $id = 0, array $initial = []) { $this->table = \Lib\GlobalOptions::CPTDB_TBL_PARTNERS(); parent::__construct($id, $initial); } }
Properties
| Property | Type | Purpose | |
|---|---|---|---|
protected \PDO $pdo | PDO | Shared DB handle. | |
protected ?Utils $utils | Utils\ | null | Logger & helpers. |
public int $id | int | Primary key. | |
public array $record | array | Full row (assoc). | |
protected ?string $table | string\ | null | Physical table name (child sets). |
protected ?array $allowedFields | string[]\ | null | Optional whitelist for setters. |
protected array $readOnlyFields | string[] | Defaults to [id]. | |
protected array $validators | array<string,callable> | fn($val,$field,$this):bool. | |
protected array $edgesMap | array | Outbound edges (field → relation config). | |
protected const RECORD_CACHE_TTL | int | Cache TTL (default 10s). | |
protected static array $recordCache | array | In-memory cache by class/id. | |
protected ?string $tableSpec | string\ | null | Optional canonical source spec (e.g. GlobalOptions::XYZ). |
Construction lifecycle
public function __construct(int $id = 0, array $initial = [])
* If $id > 0: loads the record.
* If $id == 0: creates an empty row (insertEmptyRow()), then loads.
* Populates $this→record and $this→id.
insertEmptyRow() uses an empty insert:
INSERT INTO `<table>` () VALUES ()
On success, the new id is stored and the record is reloaded.
Magic accessors
Use dynamic getters/setters: getXxx() / setXxx($val). Xxx is converted to snake_case.
$partner = new Partner(123); $name = $partner->getName(); // reads $record['name'] $ok = $partner->setName('ACME'); // validates, saves, re-registers edges
Guards:
* $allowedFields (whitelist) and $readOnlyFields (defaults to id) are enforced.
* Per-field validators can be added via $validators['field'] = fn($val,$field,$self) ⇒ bool.
Persistence
Save
protected function _save(): false|int
* If the row exists → UPDATE changed fields.
* If not → INSERT (or empty insert when $record is empty).
* On success:
- invalidates the cache,
- registers outbound edges,
- returns
id.
Load (with cache)
protected function _load(): false|array
* Per-class, per-id cache with TTL (RECORD_CACHE_TTL).
* Falls back to DB if expired/missing.
Table name resolution
protected function fetchTableName(): string|false protected function resolveTableName(string $maybe): string
* fetchTableName(): returns $this→table, logs error if missing.
* resolveTableName(): accepts GlobalOptions::X or \Lib\GlobalOptions::X and calls the static method to resolve to a raw table name; otherwise returns input.
Consistency edges
Outbound edges are derived from $edgesMap and the current $record.
edgesFromSelf()
public function edgesFromSelf(): array
Returns an array of edges:
[ 'relation' => string, 'dst_table' => string, // canonical: 'GlobalOptions::XYZ' or raw table 'dst_id' => string, // left as provided (string or numeric) 'resolve_by' => string|null,// e.g. 'uniqueid' 'meta' => array, ]
Notes:
* dst_id is cast to string to allow unique external keys.
* If resolve_by is present, the manager may resolve the key → numeric id before storing the edge.
registerConsistencyEdges()
protected function registerConsistencyEdges(): void
* Skips if \Lib\Model\_ConsistencyManager is not available.
* Determines $srcSpec ($this→tableSpec if set, else physical table).
* Removes all existing outbound edges for (src_table, src_id).
* Recreates edges via ensureLink() and forwards resolve_by when configured.
Policy-aware deletion
public function _destroyMySelf(): bool
* Validates id and table.
* If \Lib\Model\_ConsistencyManager exists, delegates:
<code php> $mgr->deleteNode(['table' => $this->table, 'id' => $this->id]); </code>
* The manager reads ConsistencyPolicy::map() and applies RESTRICT / NULLIFY / CASCADE / DETACH.
* Fallback: hard delete (not recommended).
Always delete via _destroyMySelf().
Utilities
public function getId(): int public function invalidateRecordCache(): void protected function toSnakeCase(string $name): string
Logging & error handling
* No exceptions are thrown to the caller in normal flow; failures are logged via Utils→log(level) with levels like debug, warning, error.
* Methods return false on error (or 0 for IDs), enabling simple guard patterns.
Worked snippets
Create, set, save
$p = new Partner(0, ['name' => 'Init']); $p->setName('ACME SA'); // UPDATE (after empty insert), edges re-registered $id = $p->getId();
Policy-aware delete
$p = new Partner(123); if (!$p->_destroyMySelf()) { // Deletion was restricted by policy (see logs) }
Custom validator
$this->validators['email'] = function($val) { return filter_var($val, FILTER_VALIDATE_EMAIL) !== false; };
Restrict writable fields
protected ?array $allowedFields = ['name','email','owner_user_id']; protected array $readOnlyFields = ['id','created_at'];
Interop with Consistency Manager
* edgesFromSelf() emits canonical dst_table specs (GlobalOptions::XYZ or raw).
* registerConsistencyEdges() calls _ConsistencyManager::ensureLink():
- Resolves
GlobalOptions→ raw tables before insert. - Optionally resolves destination by
resolve_by(e.g.uniqueid) to numericid. - Stores edges with
src_id(int) anddst_id(string or resolved numeric string).
See the Consistency Manager page for schema, policies, and the reindex CLI.
Changelog
- 1.4.1 (2025-11-11): Restore
insertEmptyRow()and fix method name. - 1.4.0 (2025-11-11): Add GlobalOptions table-name resolver for edges/policy.
- 1.3.0 (2025-11-11): Single-method delete approach (always call
_destroyMySelf()). - 1.2.0 (2025-11-11): Edges + policy-aware
_destroyMySelf(). - 1.1.0 (2025-11-06): Convert to non-exception flow using
Utils→log(). - 1.0.0 (2025-11-06): Created.
