buildQuery($params); // (view): $total = $queryFeatures->getTotal(); // (view): $items = $queryFeatures->getItems(); // example: @see TableAcl, TableAjax // Special Filter Access - btns visible only if user don't have super access perms. If has, then will always see all rows. class AclQueryFeatures { public $_params; public $_acl; public $_query; public $_total; public $_legacyMode; public $_selectLocalFields; // TODO: leave here or move to AclQueryBuilder? use join for perf? public $_selectRemote; // TODO: ... public function __construct($acl, $params, $legacyMode = false) { $this->_acl = $acl; $this->_params = $params; $this->_query = null; $this->_total = null; $this->_legacyMode = $legacyMode; // TODO: _legacyMode = ($from instanceof simple schema or another programmed objects) $this->_selectLocalFields = null; $this->_selectRemote = null; } public function parseQueryValue($fieldName, $searchQuery, $fieldType = 'xsd:string') { if ('!NULL' === $searchQuery) return ['is not null', null]; if ('IS NOT NULL' === $searchQuery) return ['is not null', null]; if ('NULL' === $searchQuery) return ['is null', null]; if ('IS NULL' === $searchQuery) return ['is null', null]; switch ($fieldType) { case 'gml:PolygonPropertyType': case 'gml:PointPropertyType': case 'gml:LineStringPropertyType': case 'gml:GeometryPropertyType': return $this->_parseGeomQuery($searchQuery); // $sqlFilter = $this->_sqlValueForGeomField($fldName, $v, 't'); // if ('_CSV_NUM' == substr($fldName, -8)) { // if ($this->isCsvNumericField($fldName)) { // TODO: xsd type - p5:csv_num // $sqlFilter = $this->_sqlValueForCsvNumericField($fldName, $v, 't'); // if ($sqlFilter) $sql_where_and[] = $sqlFilter; // continue; // } } switch (substr($searchQuery, 0, 1)) { case '=': return ['=', substr($searchQuery, 1)]; case '>': switch (substr($searchQuery, 1, 1)) { case '=': return ['>=', substr($searchQuery, 2)]; default: return ['>', substr($searchQuery, 1)]; } case '<': switch (substr($searchQuery, 1, 1)) { case '=': return ['<=', substr($searchQuery, 2)]; case '>': return ['!=', substr($searchQuery, 2)]; default: return ['<', substr($searchQuery, 1)]; } case '!': switch (substr($searchQuery, 1, 1)) { case '=': return ['!=', substr($searchQuery, 2)]; default: return ['not like', substr($searchQuery, 1)]; } default: { switch ($fieldType) { case 'xsd:number': case 'xsd:int': case 'xsd:integer': { if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery]; return ['=', $searchQuery]; } default: { if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery]; $queryWhereBuilder = new SqlQueryWhereBuilder(); return ['and' , array_map(function ($word) use ($fieldName) { return [$fieldName, 'like', "%{$word}%"]; }, $queryWhereBuilder->splitQueryToWords($searchQuery) ) ]; } } return ['=', $searchQuery]; } } } public function _parseGeomQuery($searchQuery) { // _sqlValueForGeomField($fldName, $fltrValue, $tblPrefix = 't') // example: BBOX:54.40993961633866,18.583889010112824,54.337945760687454,18.397121431987586 DBG::log($searchQuery, 'string', "\$searchQuery"); if ('BBOX:' == substr($searchQuery, 0, 5)) { $valParts = explode(',', substr($searchQuery, 5)); if (4 !== count($valParts)) throw new Exception("Wrong BBOX query"); $valParts = array_filter($valParts, 'is_numeric'); if (4 !== count($valParts)) throw new Exception("Wrong BBOX query - expected 4 numeric values"); $bounds = "POLYGON(( {$valParts[3]} {$valParts[2]}, {$valParts[3]} {$valParts[0]}, {$valParts[1]} {$valParts[0]}, {$valParts[1]} {$valParts[2]}, {$valParts[3]} {$valParts[2]} ))"; // for mysql 5.6 use ST_Contains() @see http://dev.mysql.com/doc/refman/5.6/en/spatial-relation-functions.html return [ 'Intersects', $bounds ]; } else if ('GeometryType=' == substr($fltrValue, 0, 13)) { return [ 'GeometryType', substr($fltrValue, 13) ]; } throw new Exception("Not implemented geometry query string"); // TODO:? return null; } public function _sqlValueForCsvNumericField($fldName, $fltrValue, $tblPrefix = 't') { $sqlFilter = false; if (is_numeric($fltrValue)) { $sqlFilter = "FIND_IN_SET('{$fltrValue}', `{$fldName}`)>0"; } else if (false !== strpos($fltrValue, ' ')) { $sqlGlue = " or "; $fltrValues = $fltrValue; if ('&' == substr($fltrValues, 0, 1)) { $fltrValues = substr($fltrValues, 1); $sqlGlue = " and "; } $fltrValues = explode(' ', $fltrValues); $sqlNumericValues = array(); foreach ($fltrValues as $fltrVal) { if (is_numeric($fltrVal)) { $sqlNumericValues[] = "FIND_IN_SET('{$fltrVal}', `{$fldName}`)>0"; } } if (!empty($sqlNumericValues)) { $sqlFilter = "(" . implode($sqlGlue, $sqlNumericValues) . ")"; } } return $sqlFilter; } public function parseSpecialFilterMsgs($type) { $rootTableName = $this->_acl->getRootTableName(); DBG::log($rootTableName, 'string', "parse SpecialFilter Msgs({$type}), \$rootTableName"); $sqlHasFltrMsgs = " select 1 from `CRM_UI_MSGS` m where m.`uiTargetName`=CONCAT('{$rootTableName}.', t.`ID`) and m.`uiTargetType`='default_db_table_record' and m.`A_STATUS` not in('DELETED') limit 1 "; switch ($type) { case 'HAS_MSGS': return " ({$sqlHasFltrMsgs})=1 "; case 'NO_MSGS': return " ({$sqlHasFltrMsgs}) is null "; case 'NEW_MSGS': { $sqlNewFltrMsgs = " select 1 from `CRM_UI_MSGS` m where m.`uiTargetName`=CONCAT('{$rootTableName}.', t.`ID`) and m.`uiTargetType`='default_db_table_record' and m.`A_STATUS` in('WAITING') limit 1 "; return " ({$sqlNewFltrMsgs})=1 "; } } return null; } public function parseSpecialFilterProblemy($type) { DBG::log($type, 'string', "parse SpecialFilter Problemy"); switch ($type) { case 'PROBLEM': return ['A_PROBLEM', '!=', '']; case 'WARNING': return ['A_PROBLEM', '=', 'WARNING']; case 'NORMAL': return ['A_PROBLEM', '=', 'NORMAL']; } return null; } public function parseSpecialFilterStatus($type) { DBG::log($type, 'string', "parse SpecialFilter Status"); switch ($type) { case 'WAITING': return ['A_STATUS', '=', 'WAITING']; case 'AKTYWNI': return ['A_STATUS', 'or', [ // `A_STATUS` in('NORMAL', 'WARNING') "; ['A_STATUS', '=', 'NORMAL'], ['A_STATUS', '=', 'WARNING'], ] ]; } return null; } public function parseSpecialFilterSpotkania($type) { DBG::log($type, 'string', "parse SpecialFilter Spotkania"); switch ($type) { case 'OLD': return ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN_NOW']; // COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < UNIX_TIMESTAMP() // and t.`L_APPOITMENT_DATE` != '' // and t.`L_APPOITMENT_DATE` != '0000-00-00 00:00:00' case 'NOW': return ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_NOW_3600']; // COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < UNIX_TIMESTAMP()+3600 // and COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) > UNIX_TIMESTAMP()-3600 case 'TODAY': return ['L_APPOITMENT_DATE', 'and', [ ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d"), date("Y"))], ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN', mktime(0,0,0, date("m"), date("d") + 1, date("Y"))], ] ]; // $start = mktime(0,0,0, date("m"), date("d"), date("Y")); // $end = mktime(0,0,0, date("m"), date("d") + 1, date("Y")); // $sqlFltr = " // COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) > '{$start}' // and COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < '{$end}' // "; case 'TOMORROW': return ['L_APPOITMENT_DATE', 'and', [ ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d") + 1, date("Y"))], ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN', mktime(0,0,0, date("m"), date("d") + 2, date("Y"))], ] ]; case 'YESTERDAY': return ['L_APPOITMENT_DATE', 'and', [ ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d") - 2, date("Y"))], ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN', mktime(0,0,0, date("m"), date("d") - 1, date("Y"))], ] ]; case 'BRAK': return ['L_APPOITMENT_DATE', 'or', [ ['L_APPOITMENT_DATE', '=', ''], ['L_APPOITMENT_DATE', '=', '0000-00-00 00:00:00'], ] ]; } return null; } public function parseSpecialFilterAccess() { $userLogin = User::getLogin(); $usrAclGroups = User::getLdapGroupsNames(); DBG::log(['login'=>$userLogin, 'groups'=>$usrAclGroups, 'hasFieldWrite'=>$this->_acl->hasField('A_ADM_COMPANY'), 'hasFieldRead'=>$this->_acl->hasField('A_CLASSIFIED'), 'acl'=>$this->_acl], 'array', "parse SpecialFilter Access"); $orWhere = []; if ($this->_acl->hasField('A_ADM_COMPANY')) { $orWhere[] = ['A_ADM_COMPANY', '=', ''];// TODO: allow empty for everyone? foreach ($usrAclGroups as $group) $orWhere[] = ['A_ADM_COMPANY', '=', $group]; } if ($this->_acl->hasField('A_CLASSIFIED')) { $orWhere[] = ['A_CLASSIFIED', '=', ''];// TODO: allow empty for everyone? foreach ($usrAclGroups as $group) $orWhere[] = ['A_CLASSIFIED', '=', $group]; } if (!empty($orWhere) && $this->_acl->hasField('L_APPOITMENT_USER')) { $orWhere[] = ['L_APPOITMENT_USER', '=', $userLogin]; } return (!empty($orWhere)) ? [null, 'or', $orWhere] : null; } public function parseOgcFilter($ogcFilter) { $parser = new ParseOgcFilter(); $parser->loadOgcFilter($ogcFilter); $queryWhereBuilder = $parser->convertToSqlQueryWhereBuilder(); return $queryWhereBuilder->getQueryWhere('t'); // TODO: $this->_fromPrefix } public function getQuery() { if ($this->_query) return clone($this->_query); // $ds = $this->_acl->getDataSource(); // TODO: only for TableAcl // TODO: move _parseSqlWhere to this class $filtrIsInstance = []; // $filtrIsInstance = [ $this->_acl->getNamespace() ]; $filtrIsNotInstance = []; if (!empty($this->_params['f_is_instance'])) $filtrIsInstance = $this->_params['f_is_instance']; if (!empty($this->_params['f_is_not_instance'])) $filtrIsNotInstance = $this->_params['f_is_not_instance']; $this->_query = ACL::query($this->_acl) ->isInstance($filtrIsInstance) ->isNotInstance($filtrIsNotInstance); // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPrimaryKey} and i.idInstance = {$idInstance}" ]) // $this->_query->where($ds->_parseSqlWhere($params)) DBG::log($this->_params, 'array', "AclQueryFeatures::getQuery \$this->_params"); foreach ($this->_params as $k => $v) { // DBG::log(['v'=>$v, 'is_numeric' => is_numeric($k), 'is_int' => is_int($k), 'is_array' => is_array($v)], 'array', "AclQueryFeatures::getQuery \$this->_params[{$k}]"); if (is_int($k) && is_array($v)) { $this->_query->where($v); // TODO: check format [$fieldName, $comparisonSign, $value] } else if (is_int($k) && null === $v) { // skip NULL } else if ('f_is_instance' === $k) { // parsed before } else if ('f_is_not_instance' === $k) { // parsed before } else if ('@instances' === $k) { // skip - select } else if ('cols' === $k) { // skip - select } else if ('f_' === substr($k, 0, 2) && is_string($v) && strlen($k) > 3) { $fieldName = substr($k, 2); $fieldType = $this->_acl->getXsdFieldType($fieldName); list($comparisonSign, $value) = $this->parseQueryValue($fieldName, $v, $fieldType); DBG::log([ $fieldName, $comparisonSign, $value, $fieldType ], 'array', "parseQueryValue"); $this->_query->where([$fieldName, $comparisonSign, $value]); } else if ('sf_' === substr($k, 0, 3) && is_string($v) && strlen($k) > 4) { switch (substr($k, 3)) { case 'Msgs': $this->_query->where($this->parseSpecialFilterMsgs($v)); break; case 'Problemy': $this->_query->where($this->parseSpecialFilterProblemy($v)); break; case 'Status': $this->_query->where($this->parseSpecialFilterStatus($v)); break; case 'Spotkania': $this->_query->where($this->parseSpecialFilterSpotkania($v)); break; case 'Access': break; // SKIP - used below default: throw new Exception("Not Implemented special filter '".substr($k, 3)."'"); } } else if ('ogc:Filter' === $k) { $this->_query->where($this->parseOgcFilter($v)); } else if ('primaryKey' === $k) { $fieldName = $this->_acl->getPrimaryKeyField(); $fieldType = $this->_acl->getXsdFieldType($fieldName); list($comparisonSign, $value) = $this->parseQueryValue($fieldName, $v, $fieldType); DBG::log([ $fieldName, $comparisonSign, $value, $fieldType ], 'array', "parseQueryValue"); $this->_query->where([$fieldName, $comparisonSign, $value]); } else if ('__backRef' === $k) { // skip - parse below } else if ('limit' === $k) { } else if ('limitstart' === $k) { } else if ('order_by' === $k) { } else if ('order_dir' === $k) { } else if ('sortBy' === $k) { } else { throw new Exception("Not Implemented param '{$k}' = '{$v}'"); } } // sf_Access: if 'SHOW' then show all rows, but data with *** if ('SHOW' !== V::get('sf_Access', '', $this->_params)) $this->_query->where($this->parseSpecialFilterAccess()); if (array_key_exists('__backRef', $this->_params)) { $backRef = $this->_params['__backRef']; if (!is_array($backRef)) throw new Exception("Wrong back ref structure - expected array"); if (empty($backRef['namespace'])) throw new Exception("Wrong back ref structure - missing namespace"); if (empty($backRef['primaryKey'])) throw new Exception("Wrong back ref structure - missing primaryKey"); if (empty($backRef['fieldName'])) throw new Exception("Wrong back ref structure - missing fieldName"); // TODO: $this->_query->where([ '__backRef' ]); or $this->_query->join([ '__backRef' ]); $refAcl = ACL::getAclByNamespace($backRef['namespace']); if ($refAcl->getSourceName() !== $this->_acl->getSourceName()) throw new Exception("Not implemented join with different source"); $refTable = ACL::getRefTable($refAcl->getNamespace(), $backRef['fieldName']); // TODO: 'in' operator? // $this->_query->where($pkField, 'in', ""); $sqlPk = $this->getAclSqlPrimaryKeyField(); $sqlBackRefPk = DB::getPDO()->quote($backRef['primaryKey']); $this->_query->where(" t.{$sqlPk} in ( select refTable.REMOTE_PRIMARY_KEY from `{$refTable}` refTable where refTable.PRIMARY_KEY = {$sqlBackRefPk} ) "); } return clone($this->_query); } public function getTotal() { if ($this->_legacyMode) return $this->_acl->getTotal($this->_params); if (null !== $this->_total) return $this->_total; $this->_total = $this->getQuery()->fetchTotal(); return $this->_total; } public function hasParam($key) { return !empty($this->_params[$key]); } public function getParam($key) { return V::get($key, '', $this->_params); } public function getItems() { if ($this->_legacyMode) return $this->_acl->getItems($this->_params);// TODO: array_map( $r => (array)$r ) // 'limit' => 10, // 'limitstart' => 0, // 'order_by' => 'ID', // 'order_dir' => 'desc', // TODO: sortBy from wfs query $sortBy = ($this->hasParam('sortBy')) ? $this->getParam('sortBy') : null; if (!$sortBy) { $sortBy = $this->hasParam('order_by') ? ( $this->hasParam('order_dir') ? $this->getParam('order_by') . " " . $this->getParam('order_dir') : $this->getParam('order_by') ) : ''; } $limit = V::get('limit', 10, $this->_params, 'int'); $offset = V::get('limitstart', 0, $this->_params, 'int'); DBG::log(['params' => $this->_params, 'sortBy' => $sortBy, 'limit' => $limit, 'offset' => $offset], 'array', '$this->_params'); $select = $this->prepareSelect(); DBG::log($select, 'array', "\$select is(TableAcl)=(".($this->_acl instanceof TableAcl).")"); DBG::log($this->getQuery(), 'array', "\$select is(TableAcl)=(".($this->_acl instanceof TableAcl).") \$this->getQuery()"); return $this->fetchRowsRefs( $this->getQuery() ->select($select) ->limit($limit) ->offset($offset) ->orderBy($sortBy) ->fetchAll() ); } public function getItem($primaryKey) { // TODO: throw exception if not found? if ($this->_legacyMode) return (array)$this->_acl->getItem($primaryKey, $this->_params); $select = $this->prepareSelect(); $pkField = $this->_acl->getPrimaryKeyField(); return $this->fetchRowRefs( $this->getQuery() ->select($select) ->where([$pkField, '=', $primaryKey]) ->fetchFirst() ); } public function prepareSelect() { // TODO: replace with getSelectLocal // TODO: select from params: 'cols' => [ fieldName, ... ] // TODO: select from params: '@instances' => 1 // TODO: if no fields set, then '*' // TODO: select must contain primaryKey return $this->getSelectLocal(); } public function getSelectLocal() { // @returns [ $fieldName, ... ] // TODO: rawSelect! if (null !== $this->_selectLocalFields) return $this->_selectLocalFields; $this->_selectLocalFields = []; $todoFetchAllCols = false; // select t.* if (!empty($this->_params['cols'])) { if (in_array('*', $this->_params['cols'])) $todoFetchAllCols = true; $acl = $this->_acl; $this->_selectLocalFields = array_filter($this->_params['cols'], function ($fieldQuery) use ($acl) { if ('*' === $fieldQuery) return false; list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2); if (!empty($subFieldQuery)) return false; return $acl->isLocalField($fieldName); }); } if (!empty($this->_params['@instances'])) $this->_selectLocalFields[] = '@instances'; if (empty($this->_selectLocalFields)) $todoFetchAllCols = true; if (1 === count($this->_selectLocalFields) && in_array('@instances', $this->_selectLocalFields)) $todoFetchAllCols = true; // if ($this->_acl instanceof TableAcl) { // $rawSelect = $this->_acl->getDataSource()->_getSqlCols(); // DBG::log($rawSelect, 'string', "DBG raw select"); // if ('*' !== $rawSelect && 't.*' !== $rawSelect) { // $this->_selectLocalFields['rawSelect'] = $rawSelect; // } // } if (!empty($this->_params['cols'])) DBG::log($this->_params['cols'], 'array', '$this->_params[cols]'); if ($todoFetchAllCols) { // $this->_selectLocalFields[] = '*'; // TODO: select all $this->from local fields DBG::log($this->_acl->getLocalFieldList(), 'array', "\$this->_acl->getLocalFieldList()"); foreach ($this->_acl->getLocalFieldList() as $localFieldName) { $this->_selectLocalFields[] = $localFieldName; } } $primaryKey = $this->getAclSqlPrimaryKeyField(); if (!in_array($primaryKey, $this->_selectLocalFields)) { $this->_selectLocalFields[] = $primaryKey; } DBG::log($this->_selectLocalFields, 'array', '$this->_selectLocalFields'); return $this->_selectLocalFields; } public function getSelectRemote() { // @returns [ $fieldName => [ $fieldName, ... ] ] if (null !== $this->_selectRemote) return $this->_selectRemote; $this->_selectRemote = []; if (!empty($this->_params['cols'])) { $cols = $this->_params['cols']; if (in_array('*', $cols)) { $this->_selectLocalFields[] = '*'; $cols = array_filter($cols, function ($fieldQuery) { return '*' !== $fieldQuery; }); } $acl = $this->_acl; $cols = array_filter($cols, function ($fieldQuery) use ($acl) { list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2); // if (empty($subFieldQuery)) return false; return !$acl->isLocalField($fieldName); }); foreach ($cols as $fieldQuery) { // group by fieldName list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2); if (!array_key_exists($fieldName, $this->_selectRemote)) $this->_selectRemote[$fieldName] = []; if (!empty($subFieldQuery)) $this->_selectRemote[$fieldName][] = $subFieldQuery; } } return $this->_selectRemote; } public function fetchRowsRefs($rows) { return array_map([ $this, 'fetchRowRefs' ], $rows); // if (!empty($rows) && !empty($rows[0])) { // $rows[0]['default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK'] = [ // [ 'xlink' => 'default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK.999' ], // [ 'xlink' => 'default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK.998' ], // ]; // } return $rows; } public function fetchRowRefs($row) { if (!$row) return $row; $sqlPk = $this->getAclSqlPrimaryKeyField(); $primaryKey = $row[$sqlPk]; DBG::log($row, 'array', "DBG primaryKey '{$primaryKey}'"); if (!$primaryKey) throw new Exception("Missing primaryKey"); foreach ($this->getSelectRemote() as $fieldName => $cols) { DBG::log($cols, 'array', "add select remote '{$fieldName}' \$cols"); $xsdType = $this->_acl->getXsdFieldType($fieldName); if ('ref:' === substr($xsdType, 0, 4) && empty($cols)) { // only xlink's DBG::log("add remote xlink's '{$fieldName}' \$items[{$primaryKey}] ..."); $refTable = ACL::getRefTable($this->_acl->getNamespace(), $fieldName); if (!$refTable) DBG::log("BUG: Missing refTable in add remote xlink's '{$fieldName}' \$items[{$primaryKey}]"); if ($refTable) { $xlinks = DB::getPDO()->fetchAll(" select r.REMOTE_PRIMARY_KEY from `{$refTable}` r where r.PRIMARY_KEY = '{$primaryKey}' "); DBG::log($xlinks, 'array', "add remote xlink's for '{$fieldName}' \$items[{$primaryKey}]"); $row[$fieldName] = array_map(function ($refInfo) use ($fieldName) { $ns = Core_AclHelper::parseTypeName($fieldName); return [ // '_ns' => $ns, 'xlink' => "{$ns['url']}#{$ns['name']}.{$refInfo['REMOTE_PRIMARY_KEY']}", ]; }, $xlinks); DBG::log($row[$fieldName], 'array', "remote xlinks for \$items[{$primaryKey}][{$fieldName}]"); } } else if ('ref:' === substr($xsdType, 0, 4)) { // cols $items = ACL::getAclByTypeName($fieldName)->buildQuery([ 'cols' => $cols, '__backRef' => [ 'namespace' => $this->_acl->getNamespace(), 'primaryKey' => $primaryKey, 'fieldName' => $fieldName, ] ])->getItems(); DBG::log($items, 'array', "add remote items '{$fieldName}' \$items"); $row[$fieldName] = $items; } else { // TODO: field is not ref DBG::log($items, 'array', "NotImplemented - add remote items for non ref field \$items[{$primaryKey}][{$fieldName}]?"); } } return $row; } public function getAclSqlPrimaryKeyField() { return ($this->_acl instanceof Core_AclBase) ? $this->_acl->getSqlPrimaryKeyField() : 'ID'; } }