* */ /** * Абстрактный класс сущности ORM - аналог active record * Позволяет без написания SQL запросов работать с базой данных. *
 * $oUser=$this->User_GetUserById(1);
 * $oUser->setName('Claus');
 * $oUser->Update();
 * 
* Возможно получать списки объектов по фильтру: *
 * $aUsers=$this->User_GetUserItemsByAgeAndSex(18,'male');
 * // эквивалентно
 * $aUsers=$this->User_GetUserItemsByFilter(array('age'=>18,'sex'=>'male'));
 * // эквивалентно, но при использовании #where необходимо указывать префикс таблицы "t"
 * $aUsers=$this->User_GetUserItemsByFilter(array('#where'=>array('t.age = ?d and t.sex = ?' => array(18,'male'))));
 * 
* * @package framework.engine.orm * @since 1.0 */ abstract class EntityORM extends Entity { /** * Типы связей сущностей * */ const RELATION_TYPE_BELONGS_TO = 'belongs_to'; const RELATION_TYPE_HAS_MANY = 'has_many'; const RELATION_TYPE_HAS_ONE = 'has_one'; const RELATION_TYPE_MANY_TO_MANY = 'many_to_many'; const RELATION_TYPE_TREE = 'tree'; /** * Массив исходных данных сущности * * @var array */ protected $_aOriginalData = array(); /** * Список полей таблицы сущности * * @var array */ protected $aFields = array(); /** * Список связей * * @var array */ protected $aRelations = array(); /** * Список данных связей * * @var array */ protected $aRelationsData = array(); /** * Список полей, которые нужно хранить как json строку * * @var array */ protected $aJsonFields = array(); /** * Объекты связей many_to_many * * @var array */ protected $_aManyToManyRelations = array(); /** * Флаг новая или нет сущность * * @var bool */ protected $bIsNew = true; /** * Установка связей * @see Entity::__construct * * @param bool $aParam Ассоциативный массив данных сущности */ public function __construct($aParam = false) { parent::__construct($aParam); $this->aRelations = $this->_getRelations(); } /** * Устанавливает данные сущности из БД * * @param $aData */ public function _setDataFromDb($aData) { $this->_SetIsNew(false); $this->_setOriginalData($aData); foreach ($aData as $sField => $mValue) { if (in_array($sField, $this->aJsonFields)) { if (!is_null($mValue) and $mJsonData = @json_decode($mValue, true)) { $aData[$sField] = $mJsonData; } else { $aData[$sField] = null; } } } $this->_setData($aData); } /** * Получение primary key из схемы таблицы * * @return string|array Если индекс составной, то возвращает массив полей */ public function _getPrimaryKey() { if (!$this->sPrimaryKey) { if ($aIndex = $this->ShowPrimaryIndex()) { if (count($aIndex) > 1) { // Составной индекс $this->sPrimaryKey = $aIndex; } else { $this->sPrimaryKey = $aIndex[1]; } } } return $this->sPrimaryKey; } /** * Получение значения primary key * * @return string */ public function _getPrimaryKeyValue() { return $this->_getDataOne($this->_getPrimaryKey()); } /** * Получение имени родительского поля. Используется в связи RELATION_TYPE_TREE * * @return string */ public function _getTreeParentKey() { return 'parent_id'; } /** * Получение значения родителя. Используется в связи RELATION_TYPE_TREE * * @return string */ public function _getTreeParentKeyValue() { return $this->_getDataOne($this->_getTreeParentKey()); } /** * Новая или нет сущность * Новая - еще не сохранялась в БД * * @return bool */ public function _isNew() { return $this->bIsNew; } /** * Установка флага "новая" * * @param bool $bIsNew Флаг - новая сущность или нет */ public function _SetIsNew($bIsNew) { $this->bIsNew = $bIsNew; } /** * Добавление сущности в БД * * @return Entity|false */ public function Add() { if ($this->beforeSave()) { if ($res = $this->_Method(__FUNCTION__)) { $this->afterSave(); return $res; } } return false; } /** * Обновление сущности в БД * * @return Entity|false */ public function Update() { if ($this->beforeSave()) { if ($res = $this->_Method(__FUNCTION__)) { $this->afterSave(); return $res; } } return false; } /** * Сохранение сущности в БД (если новая то создается) * * @return Entity|false */ public function Save() { if ($this->beforeSave()) { if ($res = $this->_Method(__FUNCTION__)) { $this->afterSave(); return $res; } } return false; } /** * Удаление сущности из БД * * @return Entity|false */ public function Delete() { if ($this->beforeDelete()) { if ($res = $this->_Method(__FUNCTION__)) { $this->afterDelete(); return $res; } } return false; } /** * Обновляет данные сущности из БД * * @return Entity|false */ public function Reload() { return $this->_Method(__FUNCTION__); } /** * Возвращает список полей сущности * * @return array */ public function ShowColumns() { return $this->_Method(__FUNCTION__ . 'From'); } /** * Возвращает primary индекс сущности * * @return array */ public function ShowPrimaryIndex() { return $this->_Method(__FUNCTION__ . 'From'); } /** * Хук, срабатывает перед сохранением сущности * * @return bool */ protected function beforeSave() { $bResult = true; $this->RunBehaviorHook('before_save', array('bResult' => &$bResult)); return $bResult; } /** * Хук, срабатывает после сохранения сущности * */ protected function afterSave() { $this->RunBehaviorHook('after_save'); } /** * Хук, срабатывает перед удалением сущности * * @return bool */ protected function beforeDelete() { $bResult = true; $this->RunBehaviorHook('before_delete', array('bResult' => &$bResult)); return $bResult; } /** * Хук, срабатывает после удаления сущности * */ protected function afterDelete() { $this->RunBehaviorHook('after_delete'); } /** * Для сущности со связью RELATION_TYPE_TREE возвращает список прямых потомков * * @return array */ public function getChildren() { if ($this->_isUsedRelationType(self::RELATION_TYPE_TREE)) { return $this->_Method(__FUNCTION__ . 'Of'); } return $this->__call(__FUNCTION__, array()); } /** * Для сущности со связью RELATION_TYPE_TREE возвращает список всех потомков * * @return array */ public function getDescendants() { if ($this->_isUsedRelationType(self::RELATION_TYPE_TREE)) { return $this->_Method(__FUNCTION__ . 'Of'); } return $this->__call(__FUNCTION__, array()); } /** * Для сущности со связью RELATION_TYPE_TREE возвращает предка * * @return Entity */ public function getParent() { if ($this->_isUsedRelationType(self::RELATION_TYPE_TREE)) { return $this->_Method(__FUNCTION__ . 'Of'); } return $this->__call(__FUNCTION__, array()); } /** * Для сущности со связью RELATION_TYPE_TREE возвращает список всех предков * * @return array */ public function getAncestors() { if ($this->_isUsedRelationType(self::RELATION_TYPE_TREE)) { return $this->_Method(__FUNCTION__ . 'Of'); } return $this->__call(__FUNCTION__, array()); } /** * Для сущности со связью RELATION_TYPE_TREE устанавливает потомков * * @param array $aChildren Список потомков */ public function setChildren($aChildren = array()) { if ($this->_isUsedRelationType(self::RELATION_TYPE_TREE)) { $this->aRelationsData['children'] = $aChildren; } else { $aArgs = func_get_args(); return $this->__call(__FUNCTION__, $aArgs); } } /** * Для сущности со связью RELATION_TYPE_TREE устанавливает потомков * * @param array $aDescendants Список потомков */ public function setDescendants($aDescendants = array()) { if ($this->_isUsedRelationType(self::RELATION_TYPE_TREE)) { $this->aRelationsData['descendants'] = $aDescendants; } else { $aArgs = func_get_args(); return $this->__call(__FUNCTION__, $aArgs); } } /** * Для сущности со связью RELATION_TYPE_TREE устанавливает предка * * @param Entity $oParent Родитель */ public function setParent($oParent = null) { if ($this->_isUsedRelationType(self::RELATION_TYPE_TREE)) { $this->aRelationsData['parent'] = $oParent; } else { $aArgs = func_get_args(); return $this->__call(__FUNCTION__, $aArgs); } } /** * Для сущности со связью RELATION_TYPE_TREE устанавливает предков * * @param array $oParent Родитель */ public function setAncestors($oParent = null) { if ($this->_isUsedRelationType(self::RELATION_TYPE_TREE)) { $this->aRelationsData['ancestors'] = $oParent; } else { $aArgs = func_get_args(); return $this->__call(__FUNCTION__, $aArgs); } } /** * Проксирует вызов методов в модуль сущности * * @param string $sName Название метода * @return mixed */ protected function _Method($sName) { $sRootDelegater = $this->Plugin_GetRootDelegater('entity', get_class($this)); $sModuleName = Engine::GetModuleName($sRootDelegater); $sPluginPrefix = Engine::GetPluginPrefix($sRootDelegater); $sEntityName = Engine::GetEntityName($sRootDelegater); return Engine::GetInstance()->_CallModule("{$sPluginPrefix}{$sModuleName}_{$sName}{$sEntityName}", array($this)); } /** * Устанавливает данные сущности * * @param array $aData Ассоциативный массив данных сущности */ public function _setData($aData) { if (is_array($aData)) { foreach ($aData as $sKey => $val) { if (array_key_exists($sKey, $this->aRelations)) { $this->aRelationsData[$sKey] = $val; } else { $this->_aData[$sKey] = $val; } } } } /** * Устанавливает все оригинальные данные * * @param $aData */ public function _setOriginalData($aData) { $this->_aOriginalData = $aData; } /** * Возвращает все оригинальные данные сущности * * @return array */ public function _getOriginalData() { return $this->_aOriginalData; } /** * Возвращает "оригинальные" данные по конкретному полю * * @param string $sKey Название поля, например
'my_property'
* @return null|mixed */ public function _getOriginalDataOne($sKey) { if (array_key_exists($sKey, $this->_aOriginalData)) { return $this->_aOriginalData[$sKey]; } return null; } /** * Возвращает данные для списка полей сущности * * @return array */ public function _getDataFields($bOnlyChanged = false) { $aData = $this->_getData($this->_getFields()); if ($bOnlyChanged) { /** * Сравниваем список оригинальных значений с текущими */ $aDataOriginal = $this->_getOriginalData(); foreach ($aData as $sKey => $sValue) { if (array_key_exists($sKey, $aDataOriginal) and $aDataOriginal[$sKey] === $sValue) { unset($aData[$sKey]); } } } return $aData; } public function _getDataFieldsForDb($bOnlyChanged = false) { $aData = $this->_getDataFields($bOnlyChanged); /** * Проверяем на json поля */ foreach ($aData as $sField => $mValue) { if (in_array($sField, $this->aJsonFields)) { $aData[$sField] = json_encode($mValue); } } return $aData; } /** * Возвращает список полей сущности * * @return array */ public function _getFields() { if (empty($this->aFields)) { $this->aFields = $this->ShowColumns(); } return $this->aFields; } /** * Возвращает поле в нужном формате * * @param string $sField Название поля * @param int $iPersistence Тип "глубины" определения поля * @return null|string */ public function _getField($sField, $iPersistence = 3) { $sRootDelegater = $this->Plugin_GetRootDelegater('entity', get_class($this)); if ($aFields = $this->_getFields()) { if (in_array($sField, $aFields)) { return $sField; } if ($iPersistence == 0) { return null; } $sFieldU = func_camelize($sField); $sEntityField = func_underscore(Engine::GetEntityName($sRootDelegater) . $sFieldU); if (in_array($sEntityField, $aFields)) { return $sEntityField; } if ($iPersistence == 1) { return null; } $sModuleEntityField = func_underscore(Engine::GetModuleName($sRootDelegater) . Engine::GetEntityName($sRootDelegater) . $sFieldU); if (in_array($sModuleEntityField, $aFields)) { return $sModuleEntityField; } if ($iPersistence == 2) { return null; } $sModuleField = func_underscore(Engine::GetModuleName($sRootDelegater) . $sFieldU); if (in_array($sModuleField, $aFields)) { return $sModuleField; } } return $sField; } /** * Возвращает список связей * * @return array */ public function _getRelations() { /** * Преобразуем связи к единому ассоциативному виду и проставляем дефолтные значения */ foreach ($this->aRelations as $sName => $aParams) { if (is_int($sName)) { /** * Старый вариант использования связи RELATION_TYPE_TREE */ $this->aRelations[$sName] = array( 'type' => self::RELATION_TYPE_TREE ); continue; } if (isset($aParams['type'])) { /** * Проставляем дефолтные значения */ if (!array_key_exists('filter', $aParams)) { $aParams['filter'] = array(); } if ($aParams['type'] == EntityORM::RELATION_TYPE_BELONGS_TO) { if (!array_key_exists('rel_key_to', $aParams)) { $aParams['rel_key_to'] = null; } } if ($aParams['type'] == EntityORM::RELATION_TYPE_HAS_MANY) { if (!array_key_exists('key_from', $aParams)) { $aParams['key_from'] = null; } } $this->aRelations[$sName] = $aParams; continue; } /** * Преобразование от старой записи к новой */ $aParamsNew = array( 'type' => $aParams[0], 'rel_entity' => $aParams[1], 'rel_key' => $aParams[2], 'filter' => array() ); if ($aParamsNew['type'] == EntityORM::RELATION_TYPE_BELONGS_TO) { $aParams['rel_key_to'] = null; } if ($aParamsNew['type'] == EntityORM::RELATION_TYPE_HAS_ONE) { if (isset($aParams[3])) { $aParamsNew['filter'] = $aParams[3]; } } if ($aParamsNew['type'] == EntityORM::RELATION_TYPE_HAS_MANY) { if (isset($aParams[3])) { $aParamsNew['filter'] = $aParams[3]; } $aParams['key_from'] = null; } if ($aParamsNew['type'] == EntityORM::RELATION_TYPE_MANY_TO_MANY) { $aParamsNew['join_entity'] = $aParams[3]; $aParamsNew['join_key'] = $aParams[4]; if (isset($aParams[5])) { $aParamsNew['filter'] = $aParams[5]; } } $this->aRelations[$sName] = $aParamsNew; } return $this->aRelations; } /** * Проверяет факт использования сущностью определенного типа связи * * @param $sType * @return bool */ public function _isUsedRelationType($sType) { $aRelations = $this->_getRelations(); foreach ($aRelations as $sName => $aParams) { if (isset($aParams['type']) and $aParams['type'] == $sType) { return true; } } return false; } /** * Возвращает список данных связей * * @param string|null $sKey * * @return array|null */ public function _getRelationsData($sKey = null) { if ($sKey) { if (array_key_exists($sKey, $this->aRelationsData)) { return $this->aRelationsData[$sKey]; } return null; } return $this->aRelationsData; } /** * Устанавливает данные связей * * @param array $aData Список связанных данных */ public function _setRelationsData($aData) { $this->aRelationsData = $aData; } /** * Устанавливает вспомогательные объекты для связи MANY_TO_MANY * * @param array $aData * @param string|null $sRelationKey */ public function _setManyToManyRelations($aData, $sRelationKey = null) { if ($sRelationKey) { $this->_aManyToManyRelations[$sRelationKey] = $aData; } else { $this->_aManyToManyRelations = $aData; } } /** * Возвращает сущность связи при MANY_TO_MANY * Актуально только в том случае, если текущая сущность была получена через обращение к связи MANY_TO_MANY * * @return mixed|null */ public function _getManyToManyRelationEntity() { return $this->_getDataOne('_relation_entity'); } /** * Ставим хук на вызов неизвестного метода и считаем что хотели вызвать метод какого либо модуля * Также производит обработку методов set* и get* * Учитывает связи и может возвращать связанные данные * @see Engine::_CallModule * * @param string $sName Имя метода * @param array $aArgs Аргументы * @return mixed */ public function __call($sName, $aArgs) { $sType = substr($sName, 0, strpos(func_underscore($sName), '_')); if (!strpos($sName, '_') and in_array($sType, array('get', 'set', 'reload'))) { $sKey = func_underscore(preg_replace('/' . $sType . '/', '', $sName, 1)); if ($sType == 'get') { if (isset($this->_aData[$sKey])) { return $this->_aData[$sKey]; } else { $sField = $this->_getField($sKey); if ($sField != $sKey && isset($this->_aData[$sField])) { return $this->_aData[$sField]; } } /** * Проверяем на связи */ $aRelations = $this->_getRelations(); if (array_key_exists($sKey, $aRelations)) { $sEntityRel = $aRelations[$sKey]['rel_entity']; $sRelationType = $aRelations[$sKey]['type']; $sRelationKey = $aRelations[$sKey]['rel_key']; $sRelModuleName = Engine::GetModuleName($sEntityRel); $sRelEntityName = Engine::GetEntityName($sEntityRel); $sRelPluginPrefix = Engine::GetPluginPrefix($sEntityRel); $sRelPrimaryKey = 'id'; if ($oRelEntity = Engine::GetEntity($sEntityRel)) { $sRelPrimaryKey = $oRelEntity->_getPrimaryKey(); } $iPrimaryKeyValue = $this->_getDataOne($this->_getPrimaryKey()); $bUseFilter = array_key_exists(0, $aArgs) && is_array($aArgs[0]); $sCmd = ''; $mCmdArgs = array(); switch ($sRelationType) { case self::RELATION_TYPE_BELONGS_TO : if (!$this->_getDataOne($sRelationKey)) { return null; } $sKeyTo = $aRelations[$sKey]['rel_key_to'] ?: $sRelPrimaryKey; $sCmd = "{$sRelPluginPrefix}{$sRelModuleName}_get{$sRelEntityName}By" . func_camelize($sKeyTo); $mCmdArgs = array($this->_getDataOne($sRelationKey)); break; case self::RELATION_TYPE_HAS_ONE : $aFilterAdd = $aRelations[$sKey]['filter']; $sCmd = "{$sRelPluginPrefix}{$sRelModuleName}_get{$sRelEntityName}ByFilter"; $aFilterAdd = array_merge(array($sRelationKey => $iPrimaryKeyValue), $aFilterAdd); if ($bUseFilter) { $aFilterAdd = array_merge($aFilterAdd, $aArgs[0]); } $mCmdArgs = array($aFilterAdd); break; case self::RELATION_TYPE_HAS_MANY : if ($aRelations[$sKey]['key_from']) { $sRelationKeyValue = $this->_getDataOne($aRelations[$sKey]['key_from']); } else { $sRelationKeyValue = $iPrimaryKeyValue; } $aFilterAdd = $aRelations[$sKey]['filter']; $sCmd = "{$sRelPluginPrefix}{$sRelModuleName}_get{$sRelEntityName}ItemsByFilter"; $aFilterAdd = array_merge(array($sRelationKey => $sRelationKeyValue), $aFilterAdd); if ($bUseFilter) { $aFilterAdd = array_merge($aFilterAdd, $aArgs[0]); } $mCmdArgs = array($aFilterAdd); break; case self::RELATION_TYPE_MANY_TO_MANY : $sEntityJoin = $aRelations[$sKey]['join_entity']; $sKeyJoin = $aRelations[$sKey]['join_key']; $aFilterAdd = $aRelations[$sKey]['filter']; $sCmd = "{$sRelPluginPrefix}Module{$sRelModuleName}_get{$sRelEntityName}ItemsByJoinEntity"; if ($bUseFilter) { $aFilterAdd = array_merge($aFilterAdd, $aArgs[0]); } $mCmdArgs = array($sEntityJoin, $sKeyJoin, $sRelationKey, $iPrimaryKeyValue, $aFilterAdd); break; default: break; } /** * Если связь уже загруженна, то возвращаем результат */ if (!$bUseFilter and array_key_exists($sKey, $this->aRelationsData)) { return $this->aRelationsData[$sKey]; } // Нужно ли учитывать дополнительный фильтр $res = Engine::GetInstance()->_CallModule($sCmd, $mCmdArgs); // Сохраняем данные только в случае "чистой" выборки if (!$bUseFilter) { $this->aRelationsData[$sKey] = $res; } // Создаём объекты-обёртки для связей MANY_TO_MANY if ($sRelationType == self::RELATION_TYPE_MANY_TO_MANY) { $this->_aManyToManyRelations[$sKey] = new ORMRelationManyToMany($res); } return $res; } return null; } elseif ($sType == 'set' and array_key_exists(0, $aArgs)) { if (array_key_exists($sKey, $this->aRelations)) { $this->aRelationsData[$sKey] = $aArgs[0]; } else { $this->_aData[$this->_getField($sKey)] = $aArgs[0]; } return $this; } elseif ($sType == 'reload') { if (array_key_exists($sKey, $this->aRelationsData)) { unset($this->aRelationsData[$sKey]); return $this->__call('get' . func_camelize($sKey), $aArgs); } } } else { return parent::__call($sName, $aArgs); } } /** * Используется для доступа к связанным данным типа MANY_TO_MANY * * @param string $sName Название свойства к которому обращаемсяя * @return mixed */ public function __get($sName) { // Обработка обращений к обёрткам связей MANY_TO_MANY // Если связь загружена, возвращаем объект связи if (isset($this->_aManyToManyRelations[func_underscore($sName)])) { return $this->_aManyToManyRelations[func_underscore($sName)]; // Есл не загружена, но связь с таким именем существет, пробуем загрузить и вернуть объект связи } elseif (isset($this->aRelations[func_underscore($sName)]) && $this->aRelations[func_underscore($sName)]['type'] == self::RELATION_TYPE_MANY_TO_MANY) { $sMethod = 'get' . func_camelize($sName); $this->__call($sMethod, array()); if (isset($this->_aManyToManyRelations[func_underscore($sName)])) { return $this->_aManyToManyRelations[func_underscore($sName)]; } // В противном случае возвращаем то, что просили у объекта } else { return parent::__get($sName); } } /** * Сбрасывает данные необходимой связи * * @param string $sKey Ключ(поле) связи */ public function resetRelationsData($sKey) { if (isset($this->aRelationsData[$sKey])) { unset($this->aRelationsData[$sKey]); } } }