diff options
| -rwxr-xr-x | includes/classes/LibertyContent.php | 18 | ||||
| -rw-r--r-- | includes/classes/LibertyXref.php | 35 | ||||
| -rw-r--r-- | includes/classes/LibertyXrefContent.php | 25 | ||||
| -rw-r--r-- | includes/classes/LibertyXrefGroup.php | 78 | ||||
| -rw-r--r-- | includes/classes/LibertyXrefInfo.php | 98 | ||||
| -rw-r--r-- | includes/classes/LibertyXrefType.php | 96 |
6 files changed, 166 insertions, 184 deletions
diff --git a/includes/classes/LibertyContent.php b/includes/classes/LibertyContent.php index 839c652..4f94a0d 100755 --- a/includes/classes/LibertyContent.php +++ b/includes/classes/LibertyContent.php @@ -89,7 +89,7 @@ define( 'LIBERTY_SPLIT_REGEX', "!\.[3]split\.[3][\t ]*\n?!" ); * ## XRef System * * LibertyContent is the gateway into the xref system for content subclasses: - * - `loadXrefInfo()` — populate `$this->mXrefInfo` (a LibertyXrefInfo) with all + * - `loadXrefInfo()` — populate `$this->mXrefInfo` (a LibertyXrefContent) with all * display groups and their rows for this content item. Call this from page files * before assigning `gXrefInfo` to Smarty. Package classes override this to * enrich rows (e.g. resolving contact titles from xref content_ids). @@ -149,8 +149,8 @@ class LibertyContent extends LibertyBase implements BitCacheable { */ public $mType; - /** Populated by loadXrefInfo() — LibertyXrefInfo instance for this content item */ - public ?LibertyXrefInfo $mXrefInfo = null; + /** Populated by loadXrefInfo() — LibertyXrefContent instance for this content item */ + public ?LibertyXrefContent $mXrefInfo = null; /** *Permissions hash specific to the user accessing this LibertyContent object @@ -3920,7 +3920,7 @@ class LibertyContent extends LibertyBase implements BitCacheable { // ========================================================================= // Xref methods — delegates to LibertyXrefType (query logic) and - // LibertyXref / LibertyXrefInfo (data logic). + // LibertyXref / LibertyXrefContent (data logic). // ========================================================================= /** @@ -3935,7 +3935,7 @@ class LibertyContent extends LibertyBase implements BitCacheable { * All public xref query methods on LibertyContent delegate here — do not * construct LibertyXrefType directly in page or subclass code. */ - private function xrefType(): LibertyXrefType { + protected function xrefType(): LibertyXrefType { return $this->mXrefType ??= new LibertyXrefType( $this->mContentTypeGuid, $this->mPackageGuid ?: null ); } @@ -3975,14 +3975,14 @@ class LibertyContent extends LibertyBase implements BitCacheable { } /** - * Populate mXrefInfo with a LibertyXrefInfo instance for this content item. + * Populate mXrefInfo with a LibertyXrefContent for this content item. * Creates one LibertyXrefGroup per display group (sort_order > 0), each holding - * its raw xref data rows. + * LibertyXref instances for its active rows, plus a synthetic 'history' group + * when expired rows exist. */ public function loadXrefInfo(): void { if( $this->isValid() && !empty( $this->mContentTypeGuid ) ) { - $this->mXrefInfo = new LibertyXrefInfo( $this->mContentTypeGuid ); - $this->mXrefInfo->load( $this->mContentId ); + $this->mXrefInfo = $this->xrefType()->loadContent( $this->mContentId ); } } diff --git a/includes/classes/LibertyXref.php b/includes/classes/LibertyXref.php index 328b07e..6c5b821 100644 --- a/includes/classes/LibertyXref.php +++ b/includes/classes/LibertyXref.php @@ -27,13 +27,13 @@ use Bitweaver\BitDate; * last_update_date — last write timestamp * * This class handles load/verify/store for a single row. For bulk loading of all - * xref rows belonging to a content item, use LibertyXrefGroup / LibertyXrefInfo. + * xref rows belonging to a content item, use LibertyXrefGroup / LibertyXrefContent. * * The stepXref() method implements an audit-trail pattern: instead of updating a row * in place it closes the current row (sets end_date) and opens a new one, preserving - * history. Expired rows are swept into the synthetic 'history' group by LibertyXrefInfo. + * history. Expired rows are swept into the synthetic 'history' group by LibertyXrefType::loadContent(). */ -class LibertyXref extends BitBase { +class LibertyXref extends BitBase implements \ArrayAccess { /** x_group value of the loaded xref's item definition */ public $mType; /** item key of the loaded row (matches liberty_xref_item.item) */ @@ -46,6 +46,8 @@ class LibertyXref extends BitBase { public $mDate; /** when set, scopes liberty_xref_item lookups to this content type */ public $mContentTypeGuid = ''; + /** flat row data — populated by load() and fromRow(); underpins ArrayAccess */ + protected array $mRow = []; public function __construct( $iXrefId = NULL ) { $this->mXrefId = NULL; @@ -59,6 +61,30 @@ class LibertyXref extends BitBase { $this->mDate->get_display_offset(); } + /** + * Construct a LibertyXref instance from a bulk-loaded result row. + * + * Used by LibertyXrefType::loadContent() to populate LibertyXrefGroup::mXrefs + * without going through load(). The row is stored flat in $mRow; ArrayAccess + * exposes it so templates continue to use {$xrefInfo.xkey} syntax unchanged. + * + * @param array $row result row from the LibertyXrefType::loadContent() query + */ + public static function fromRow( array $row ): self { + $xref = new self(); + $xref->mRow = $row; + $xref->mXrefId = $row['xref_id'] ?? null; + $xref->mItem = $row['item'] ?? null; + $xref->mContentId = $row['content_id'] ?? null; + return $xref; + } + + // ArrayAccess — lets templates use {$xrefInfo.key} dot notation on bulk-loaded rows. + public function offsetExists( mixed $offset ): bool { return isset( $this->mRow[$offset] ); } + public function offsetGet( mixed $offset ): mixed { return $this->mRow[$offset] ?? null; } + public function offsetSet( mixed $offset, mixed $value ): void { $this->mRow[$offset] = $value; } + public function offsetUnset( mixed $offset ): void { unset( $this->mRow[$offset] ); } + /** @return bool true if a row has been loaded (mXrefId is a valid integer) */ public function isValid() { return $this->verifyId( $this->mXrefId ); @@ -96,11 +122,12 @@ class LibertyXref extends BitBase { $this->mXrefId = $pXrefId; $this->mContentId = $result['content_id']; $this->mType = $result["x_group"]; - $this->mItem = $result['item']; + $this->mItem = $result['item']; $this->mInfo['title'] = $result['source_title']; $this->mInfo['format_guid'] = 'text'; unset( $result['source_title'] ); $this->mInfo['data'] = $result; + $this->mRow = $result; } } } diff --git a/includes/classes/LibertyXrefContent.php b/includes/classes/LibertyXrefContent.php new file mode 100644 index 0000000..7e1acdd --- /dev/null +++ b/includes/classes/LibertyXrefContent.php @@ -0,0 +1,25 @@ +<?php +/** + * @package liberty + * @subpackage classes + */ + +namespace Bitweaver\Liberty; + +/** + * Data container for all xref groups loaded for a specific content item. + * + * Produced by LibertyXrefType::loadContent(). Holds one LibertyXrefGroup per + * display group (sort_order > 0) plus a synthetic 'history' group when expired + * rows exist. Groups are keyed by x_group name. + * + * Packages that need to enrich rows (e.g. resolving contact titles from xref + * content_ids) should override loadXrefInfo() in their own class, call the + * parent, then walk $this->mXrefInfo->mGroups and mutate as needed. + * + * Template access: {foreach $gXrefInfo->mGroups as $xrefGroup} + */ +class LibertyXrefContent { + /** @var LibertyXrefGroup[] Loaded groups, keyed by x_group name */ + public array $mGroups = []; +} diff --git a/includes/classes/LibertyXrefGroup.php b/includes/classes/LibertyXrefGroup.php index 15beef7..58e4ec6 100644 --- a/includes/classes/LibertyXrefGroup.php +++ b/includes/classes/LibertyXrefGroup.php @@ -15,11 +15,11 @@ namespace Bitweaver\Liberty; * * loadXrefs() separates rows into two buckets: * - active rows → stored in mXrefs, rendered by the group's template - * - expired rows → returned to LibertyXrefInfo, which collects them into + * - expired rows → returned to the caller, which collects them into * a synthetic 'history' group (sort_order 999) * - * Instances are created by LibertyXrefInfo::load(); do not instantiate directly - * except in tests or package-level loadXrefInfo() overrides. + * Instances are created by LibertyXrefType::loadContent(); do not instantiate + * directly except in tests or package-level loadXrefInfo() overrides. * * Template access: group templates receive the whole object as $xrefGroup and * iterate $xrefGroup->mXrefs. The first two lines of every group template must be: @@ -27,7 +27,7 @@ namespace Bitweaver\Liberty; * {assign var=xrefAllowEdit value=$allow_edit|default:false} * {assign var=isHistory value=($xrefGroup->mXGroup eq 'history')} */ -class LibertyXrefGroup extends LibertyBase { +class LibertyXrefGroup { /** x_group key (e.g. 'address', 'reference', 'quantity', 'history') */ public string $mXGroup; /** content_type_guid this group belongs to (e.g. 'contact', 'stockmovement') */ @@ -42,7 +42,7 @@ class LibertyXrefGroup extends LibertyBase { public int $mRoleId; /** optional package-level guid — xref_item rows with this guid are also matched */ public ?string $mPackageGuid; - /** @var array[] active liberty_xref data rows for the current content item */ + /** @var LibertyXref[] active xref rows for the current content item */ public array $mXrefs = []; /** @@ -51,7 +51,6 @@ class LibertyXrefGroup extends LibertyBase { * @param string|null $packageGuid optional package-level guid (e.g. 'stock') */ public function __construct( array $groupRow, string $contentTypeGuid, ?string $packageGuid = null ) { - parent::__construct(); $this->mXGroup = $groupRow['x_group']; $this->mContentTypeGuid = $contentTypeGuid; $this->mPackageGuid = $packageGuid; @@ -61,71 +60,4 @@ class LibertyXrefGroup extends LibertyBase { $this->mRoleId = (int)( $groupRow['role_id'] ?? 0 ); } - /** - * Load liberty_xref rows for this group under a given content item. - * - * Queries liberty_xref joined to liberty_xref_item (scoped to this group and - * content type) and address_postcode (for address groups). Role filtering is - * applied against the current user's roles; anonymous gets role_id -1. - * - * Rows whose end_date is in the past are classified as 'history' and returned - * separately so LibertyXrefInfo can accumulate them into the synthetic history - * group. Active rows are stored directly in $this->mXrefs. - * - * Packages that need to enrich rows (e.g. resolving contact titles from xref - * content_ids) should do so by overriding loadXrefInfo() in their own class - * after calling parent::loadXrefInfo(), not by modifying this method. - * - * @param int $contentId liberty_content.content_id to fetch rows for - * @return array[] expired rows (type_source='history') for the caller to collect - */ - public function loadXrefs( int $contentId ): array { - global $gBitUser; - - if( empty( $this->mContentTypeGuid ) ) { - $this->mErrors[] = 'LibertyXrefGroup::load() requires mContentTypeGuid — cannot scope liberty_xref_item query without it.'; - return []; - } - - $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; - $userId = $gBitUser->mUserId; - $bindVars = array_merge( [ $this->mDb->NOW(), $contentId ], $roles, [ $userId ] ); - - $guidFilter = $this->mPackageGuid - ? "IN ('{$this->mContentTypeGuid}', '{$this->mPackageGuid}')" - : "= '{$this->mContentTypeGuid}'"; - - $sql = "SELECT x.`xref_id`, x.`item`, x.`xref`, x.`xkey`, x.`xkey_ext`, - x.`xorder`, x.`data`, x.`start_date`, x.`end_date`, x.`last_update_date`, - s.`template`, s.`cross_ref_href`, - CASE WHEN x.`xorder` = 0 THEN s.`cross_ref_title` - ELSE s.`cross_ref_title` || '-' || x.`xorder` END AS xref_title, - CASE WHEN x.`end_date` IS NOT NULL AND x.`end_date` < ? THEN 'history' - ELSE s.`x_group` END AS type_source, - pc.`add1` || ',' || pc.`add2` || ',' || pc.`add4` || ',' || pc.`town` AS address - FROM `" . BIT_DB_PREFIX . "liberty_xref` x - JOIN `" . BIT_DB_PREFIX . "liberty_xref_item` s - ON s.`item` = x.`item` - AND s.`content_type_guid` $guidFilter - AND s.`x_group` = '{$this->mXGroup}' - LEFT JOIN `" . BIT_DB_PREFIX . "address_postcode` pc ON pc.`postcode` = x.`xkey` - LEFT OUTER JOIN `" . BIT_DB_PREFIX . "users_roles_map` purm - ON purm.`user_id` = $userId AND purm.`role_id` = s.`role_id` - WHERE x.`content_id` = ? - AND (s.`role_id` IN(" . implode( ',', array_fill( 0, count( $roles ), '?' ) ) . ") OR purm.`user_id` = ?) - ORDER BY x.`item`, x.`xorder`"; - - $historyRows = []; - $result = $this->mDb->query( $sql, $bindVars ); - if( $result ) { - while( $row = $result->fetchRow() ) { - if( $row['type_source'] === 'history' ) { - $historyRows[] = $row; - } else { - $this->mXrefs[] = $row; - } - } - } - return $historyRows; - } } diff --git a/includes/classes/LibertyXrefInfo.php b/includes/classes/LibertyXrefInfo.php deleted file mode 100644 index 0285968..0000000 --- a/includes/classes/LibertyXrefInfo.php +++ /dev/null @@ -1,98 +0,0 @@ -<?php -/** - * @package liberty - * @subpackage classes - */ - -namespace Bitweaver\Liberty; - -/** - * Top-level container for all xref groups belonging to one content item. - * - * LibertyXrefInfo::load() is the single entry point for fetching the complete - * xref picture for a content item. It: - * 1. Queries liberty_xref_group for all groups defined for the content type - * (sort_order > 0 only — sort_order 0 is reserved for the 'type' group - * which is loaded separately by loadXrefTypeList()). - * 2. Applies the current user's role filter on each group's role_id gate. - * 3. Creates a LibertyXrefGroup for each row and calls loadXrefs() on it. - * 4. Collects any expired rows returned by loadXrefs() and, if any exist, - * synthesises a 'history' group (sort_order 999) to hold them. - * - * After load(), $mGroups is a keyed array of LibertyXrefGroup objects ordered - * by sort_order. Templates iterate this directly: - * - * {foreach $gXrefInfo->mGroups as $xrefGroup} - * {include file=$gContent->getXrefListTemplate($xrefGroup->mTemplate) xrefGroup=$xrefGroup} - * {/foreach} - * - * Package-level enrichment (resolving contact titles, component details, etc.) - * is done in the package class's loadXrefInfo() override after calling - * parent::loadXrefInfo(), by walking $mXrefInfo->mGroups and mutating rows. - */ -class LibertyXrefInfo { - /** content_type_guid this info object was built for */ - public string $mContentTypeGuid; - /** optional package-level guid whose xref_group/xref_item rows are also loaded (e.g. 'stock') */ - public ?string $mPackageGuid; - /** @var LibertyXrefGroup[] keyed by x_group, ordered by sort_order */ - public array $mGroups = []; - - /** - * @param string $contentTypeGuid e.g. 'stockassembly', 'contact' - * @param string|null $packageGuid e.g. 'stock' — additional guid whose xref_group rows are merged in - */ - public function __construct( string $contentTypeGuid, ?string $packageGuid = null ) { - $this->mContentTypeGuid = $contentTypeGuid; - $this->mPackageGuid = $packageGuid; - } - - /** - * Load all visible xref groups and their rows for a content item. - * - * Populates $this->mGroups. Safe to call multiple times — each call - * rebuilds mGroups from scratch. - * - * @param int $contentId liberty_content.content_id - */ - public function load( int $contentId ): void { - global $gBitDb, $gBitUser; - - $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; - $userId = $gBitUser->mUserId; - $bindVars = array_merge( $roles, [ $userId ] ); - - $guidFilter = $this->mPackageGuid - ? "g.`content_type_guid` IN ('{$this->mContentTypeGuid}', '{$this->mPackageGuid}')" - : "g.`content_type_guid` = '{$this->mContentTypeGuid}'"; - - $sql = "SELECT g.`x_group`, g.`title`, g.`sort_order`, g.`template`, g.`role_id` - FROM `" . BIT_DB_PREFIX . "liberty_xref_group` g - LEFT OUTER JOIN `" . BIT_DB_PREFIX . "users_roles_map` purm - ON purm.`user_id` = $userId AND purm.`role_id` = g.`role_id` - WHERE $guidFilter - AND g.`sort_order` > 0 - AND (g.`role_id` IN(" . implode( ',', array_fill( 0, count( $roles ), '?' ) ) . ") OR purm.`user_id` = ?) - ORDER BY g.`sort_order`"; - - $result = $gBitDb->query( $sql, $bindVars ); - if( !$result ) return; - - $allHistory = []; - while( $row = $result->fetchRow() ) { - $group = new LibertyXrefGroup( $row, $this->mContentTypeGuid, $this->mPackageGuid ); - $allHistory = array_merge( $allHistory, $group->loadXrefs( $contentId ) ); - $this->mGroups[$row['x_group']] = $group; - } - - if( !empty( $allHistory ) ) { - $historyGroup = new LibertyXrefGroup( - [ 'x_group' => 'history', 'title' => 'History', 'sort_order' => 999, 'template' => null, 'role_id' => 0 ], - $this->mContentTypeGuid, - $this->mPackageGuid - ); - $historyGroup->mXrefs = $allHistory; - $this->mGroups['history'] = $historyGroup; - } - } -} diff --git a/includes/classes/LibertyXrefType.php b/includes/classes/LibertyXrefType.php index 5ed5fac..1ef71ca 100644 --- a/includes/classes/LibertyXrefType.php +++ b/includes/classes/LibertyXrefType.php @@ -199,6 +199,102 @@ class LibertyXrefType { } /** + * Load all xref groups and their rows for a specific content item. + * + * Replaces the former LibertyXrefInfo::load() + LibertyXrefGroup::loadXrefs() + * two-class pattern. Returns a LibertyXrefContent whose mGroups array is keyed + * by x_group name. A synthetic 'history' group (sort_order 999) is appended + * when any expired rows are found. + * + * Each group's mXrefs array holds LibertyXref instances built via + * LibertyXref::fromRow(). Templates continue to use {$xrefInfo.xkey} dot + * notation — LibertyXref::ArrayAccess maps this transparently. + * + * Packages that enrich rows after loading (e.g. resolving contact titles) + * should override loadXrefInfo() in their class, call the parent, then walk + * $this->mXrefInfo->mGroups and mutate as needed. + * + * @param int $contentId liberty_content.content_id to load rows for + * @return LibertyXrefContent + */ + public function loadContent( int $contentId ): LibertyXrefContent { + global $gBitSystem, $gBitUser; + + $db = $gBitSystem->mDb; + $roles = array_keys( $gBitUser->mRoles ?? [] ) ?: [-1]; + $userId = (int)( $gBitUser->mUserId ?? 0 ); + $guidFilter = $this->packageGuid + ? "IN ('$this->contentTypeGuid', '$this->packageGuid')" + : "= '$this->contentTypeGuid'"; + $rolePlaceholders = implode( ',', array_fill( 0, count( $roles ), '?' ) ); + + $content = new LibertyXrefContent(); + $allHistory = []; + + $groupResult = $db->query( + "SELECT g.`x_group`, g.`title`, g.`sort_order`, g.`template`, g.`role_id` + FROM `".BIT_DB_PREFIX."liberty_xref_group` g + LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm + ON purm.`user_id` = $userId AND purm.`role_id` = g.`role_id` + WHERE g.`content_type_guid` $guidFilter AND g.`sort_order` > 0 + AND (g.`role_id` IN($rolePlaceholders) OR purm.`user_id` = ?) + ORDER BY g.`sort_order`", + array_merge( $roles, [ $userId ] ) + ); + if( !$groupResult ) return $content; + + while( $groupRow = $groupResult->fetchRow() ) { + $group = new LibertyXrefGroup( $groupRow, $this->contentTypeGuid, $this->packageGuid ); + $xGroup = $groupRow['x_group']; + $rowResult = $db->query( + "SELECT x.`xref_id`, x.`item`, x.`xref`, x.`xkey`, x.`xkey_ext`, + x.`xorder`, x.`data`, x.`start_date`, x.`end_date`, x.`last_update_date`, + s.`template`, s.`cross_ref_href`, + CASE WHEN x.`xorder` = 0 THEN s.`cross_ref_title` + ELSE s.`cross_ref_title` || '-' || x.`xorder` END AS xref_title, + CASE WHEN x.`end_date` IS NOT NULL AND x.`end_date` < ? THEN 'history' + ELSE s.`x_group` END AS type_source, + pc.`add1` || ',' || pc.`add2` || ',' || pc.`add4` || ',' || pc.`town` AS address + FROM `".BIT_DB_PREFIX."liberty_xref` x + JOIN `".BIT_DB_PREFIX."liberty_xref_item` s + ON s.`item` = x.`item` + AND s.`content_type_guid` $guidFilter + AND s.`x_group` = '$xGroup' + LEFT JOIN `".BIT_DB_PREFIX."address_postcode` pc ON pc.`postcode` = x.`xkey` + LEFT OUTER JOIN `".BIT_DB_PREFIX."users_roles_map` purm + ON purm.`user_id` = $userId AND purm.`role_id` = s.`role_id` + WHERE x.`content_id` = ? + AND (s.`role_id` IN($rolePlaceholders) OR purm.`user_id` = ?) + ORDER BY x.`item`, x.`xorder`", + array_merge( [ $db->NOW(), $contentId ], $roles, [ $userId ] ) + ); + if( $rowResult ) { + while( $row = $rowResult->fetchRow() ) { + $xref = LibertyXref::fromRow( $row ); + if( $row['type_source'] === 'history' ) { + $allHistory[] = $xref; + } else { + $group->mXrefs[] = $xref; + } + } + } + $content->mGroups[$xGroup] = $group; + } + + if( !empty( $allHistory ) ) { + $historyGroup = new LibertyXrefGroup( + [ 'x_group' => 'history', 'title' => 'History', 'sort_order' => 999, 'template' => null, 'role_id' => 0 ], + $this->contentTypeGuid, + $this->packageGuid + ); + $historyGroup->mXrefs = $allHistory; + $content->mGroups['history'] = $historyGroup; + } + + return $content; + } + + /** * Return the distinct template format names defined across all item slots for * this content type, filtered to the current user's roles. * |
