nvg-cockpit:dxo-base-model
This is an old revision of the document!
_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).
Quick facts
- Constructor: construct(int $id = 0, array $initial = []) * Primary key: id (int) * Table: child classes set protected ?string $table * Caching: per-class, per-id in-memory cache (TTL = 10s) * Consistency: outbound edges are (re)registered on every save * Deletion: always call \_destroyMySelf() – routes through Consistency Manager (policy-aware) * GlobalOptions: accepts GlobalOptions::XYZ or \Lib\GlobalOptions::XYZ – resolved to raw table names —- ===== Example: minimal child model ===== <code php> 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); } } </code> —- ===== 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 ===== <code php> public function __construct(int $id = 0, array $initial = []) </code> * If$id > 0: loads the record. * If$id == 0: creates an empty row (insertEmptyRow()), then loads. * Populates$this→recordand$this→id.insertEmptyRow()uses an empty insert: <code php> INSERT INTO `<table>` () VALUES () </code> On success, the newidis stored and the record is reloaded. —- ===== Magic accessors ===== Use dynamic getters/setters:getXxx()/setXxx($val).Xxxis converted tosnake_case. <code php> $partner = new Partner(123); $name = $partner→getName(); reads $record['name'] $ok = $partner→setName('ACME'); validates, saves, re-registers edges </code> Guards: *$allowedFields(whitelist) and$readOnlyFields(defaults toid) are enforced. * Per-field validators can be added via$validators['field'] = fn($val,$field,$self) ⇒ bool. —- ===== Persistence ===== ==== Save ==== <code php> protected function _save(): false|int </code> * If the row exists →UPDATEchanged fields. * If not →INSERT(or empty insert when$recordis empty). * On success: * invalidates the cache, * registers outbound edges, * returnsid. ==== Load (with cache) ==== <code php> protected function _load(): false|array </code> * Per-class, per-id cache with TTL (RECORD_CACHE_TTL). * Falls back to DB if expired/missing. —- ===== Table name resolution ===== <code php> protected function fetchTableName(): string|false protected function resolveTableName(string $maybe): string </code> *fetchTableName(): returns$this→table, logs error if missing. *resolveTableName(): acceptsGlobalOptions::Xor\Lib\GlobalOptions::Xand calls the static method to resolve to a raw table name; otherwise returns input. —- ===== Consistency edges ===== Outbound edges are derived from$edgesMapand the current$record. ==== edgesFromSelf() ==== <code php> public function edgesFromSelf(): array </code> Returns an array of edges: <code> [ '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, ] </code> Notes: *dst_idis cast to string to allow unique external keys. * Ifresolve_byis present, the manager may resolve the key → numericidbefore storing the edge. ==== registerConsistencyEdges() ==== <code php> protected function registerConsistencyEdges(): void </code> * Skips if\Lib\Model\_ConsistencyManageris not available. * Determines$srcSpec($this→tableSpecif set, else physical table). * Removes all existing outbound edges for(src_table, src_id). * Recreates edges viaensureLink()and forwardsresolve_bywhen configured. —- ===== Policy-aware deletion ===== <code php> public function _destroyMySelf(): bool </code> * Validatesidandtable. * If\Lib\Model\_ConsistencyManagerexists, delegates: <code php> $mgr→deleteNode(['table' ⇒ $this→table, 'id' ⇒ $this→id]); </code> * The manager readsConsistencyPolicy::map()and applies RESTRICT / NULLIFY / CASCADE / DETACH. * Fallback: hard delete (not recommended). Always delete via_destroyMySelf(). —- ===== Utilities ===== <code php> public function getId(): int public function invalidateRecordCache(): void protected function toSnakeCase(string $name): string </code> —- ===== Logging & error handling ===== * No exceptions are thrown to the caller in normal flow; failures are logged viaUtils→log(level)with levels likedebug,warning,error. * Methods returnfalseon error (or0for IDs), enabling simple guard patterns. —- ===== Worked snippets ===== Create, set, save <code php> $p = new Partner(0, ['name' ⇒ 'Init']); $p→setName('ACME SA'); UPDATE (after empty insert), edges re-registered $id = $p→getId(); </code> Policy-aware delete <code php> $p = new Partner(123); if (!$p→_destroyMySelf()) { Deletion was restricted by policy (see logs) } </code> Custom validator <code php> $this→validators['email'] = function($val) { return filter_var($val, FILTER_VALIDATE_EMAIL) !== false; }; </code> Restrict writable fields <code php> protected ?array $allowedFields = ['name','email','owner_user_id']; protected array $readOnlyFields = ['id','created_at']; </code> —- ===== Interop with Consistency Manager ===== *edgesFromSelf()emits canonicaldst_tablespecs (GlobalOptions::XYZor raw). *registerConsistencyEdges()calls_ConsistencyManager::ensureLink(): * ResolvesGlobalOptions→ raw tables before insert. * Optionally resolves destination byresolve_by(e.g.uniqueid) to numericid. * Stores edges withsrc_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): RestoreinsertEmptyRow()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 usingUtils→log(). * 1.0.0 (2025-11-06): Created.
nvg-cockpit/dxo-base-model.1762880666.txt.gz · Last modified: by admin
