%PDF- %PDF-
Direktori : /home/bitrix/www/bitrix/modules/report/lib/visualconstructor/internal/ |
Current File : /home/bitrix/www/bitrix/modules/report/lib/visualconstructor/internal/model.php |
<?php namespace Bitrix\Report\VisualConstructor\Internal; use Bitrix\Main\ArgumentException; use Bitrix\Main\Entity\Query\Filter\ConditionTree; use Bitrix\Main\NotImplementedException; use Bitrix\Main\Type\DateTime; use Bitrix\Report\VisualConstructor\Config\Common; use Bitrix\Report\VisualConstructor\Internal\Error\IErrorable; /** * This class developed now only for models influencing with report visual constructor. * @package Bitrix\Report\VisualConstructor\Internal */ abstract class Model implements IErrorable { const ATTRIBUTE_SLICE_DELIMITER = '__'; protected $id; protected $createdAt; protected $updatedAt; protected $errors; private $deletedEntities = array(); private $currentDbState = array(); //private $lazyAttributes; /** * Model constructor. */ public function __construct() { $this->createdAt = new DateTime(); $this->updatedAt = new DateTime(); } /** * Gets the fully qualified name of table class which belongs to current model. * @throws \Bitrix\Main\NotImplementedException * @return void */ public static function getTableClassName() { throw new NotImplementedException; } /** * Returns the list of pair for mapping data and object properties. * Key is field in DataManager, value is object property. * @return array */ public static function getMapAttributes() { return array( 'ID' => 'id', 'CREATED_DATE' => 'createdAt', 'UPDATED_DATE' => 'updatedAt' ); } /** * Returns map of lazy loaded attributes of current model. * supported relation types ONE_TO_MANY, MANY_TO_ONE, MANY_TO_MANY * example: * array( * 'lazyLoadAttributeName_1' => array( * 'type' => Common::ONE_TO_MANY, * 'targetEntity' => TargetEntityClass::getClassName(), //inheritor of this class * 'mappedBy' => 'targetEntityField', * ), * 'lazyLoadAttributeName_2' => array( * 'type' => Common::MANY_TO_ONE, * 'targetEntity' => TargetEntityClass::getClassName(), //inheritor of this class * 'inveredBy' => '', * 'join' => array( * 'field' => array('thisFieldName', 'relationEntityFieldMame') * ) * ), * 'lazyLoadAttributeName_1' => array( * 'type' => Common::MANY_TO_MANY, * 'targetEntity' => TargetEntityClass::getClassName(), //inheritor of this class * 'join' => array( * 'tableClassName' => TableClassName::getClassName //Supporting table ORM class name for connecting 2 entities * 'column' => array(SUPPORTING_CONNECT_COLUMN => array('thisPrimaryFieldName', 'SUPPORTING_TABLE_APPROPRIATE_FIELD_NAME')), * 'inverseColumn' => array(SUPPORTING_CONNECT_COLUMN => array('relationEntityPrimaryFieldName', 'SUPPORTING_TABLE_APPROPRIATE_FIELD_NAME')), * ), * ) * ) * @return array */ public static function getMapReferenceAttributes() { return array(); } /** * @return Model */ public function save() { $referenceMapAttributes = static::getMapReferenceAttributes(); foreach ($referenceMapAttributes as $referenceAttributeName => $assoc) { if (!empty($this->{$referenceAttributeName})) { switch ($assoc['type']) { case Common::MANY_TO_ONE: $this->saveManyToOneReference($this->{$referenceAttributeName}, $assoc); break; case Common::ONE_TO_ONE: //TODO: break; } } } $ormFields = $this->getConvertedMapAttributesToOrmFields(); if (!$ormFields['ID']) { $addResult = $this->add($ormFields); $ownerId = $addResult->getId(); $this->setId($ownerId); } else { $ownerId = $ormFields['ID']; $changedAttributes = $this->getChangedOrmAttributes($ormFields); if ($changedAttributes) { $this->update(array('ID' => $ownerId), $changedAttributes); } if (!empty($this->deletedEntities)) { $this->deleteReference($this->deletedEntities); } } foreach ($referenceMapAttributes as $referenceAttributeName => $assoc) { if (!empty($this->{$referenceAttributeName})) { switch ($assoc['type']) { case Common::ONE_TO_MANY: $this->saveOneToManyReferences($this->{$referenceAttributeName}, $assoc, $ownerId); break; case Common::MANY_TO_MANY: $this->saveManyToManyReferences($this->{$referenceAttributeName}, $assoc, $ownerId); break; } } } return $ownerId; } /** * @param static[] $references * @param $assoc * @param $ownerEntityId */ private function saveOneToManyReferences($references, $assoc, $ownerEntityId) { foreach ($references as $key => $reference) { if ($reference instanceof $assoc['targetEntity']) { $mapReferenceAttributes = $reference::getMapReferenceAttributes(); $reference->{$mapReferenceAttributes[$assoc['mappedBy']]['join']['field'][0]} = $ownerEntityId; $reference->save(); } } } /** * @param static[] $references * @param $assoc * @param $ownerEntityId */ private function saveManyToManyReferences($references, $assoc, $ownerEntityId) { foreach ($references as $key => $reference) { if ($reference instanceof $assoc['targetEntity']) { $isReferenceNew = !(boolean)$reference->getId(); $referenceId = $reference->save(); if ($isReferenceNew) { $column = array_values($assoc['join']['column']); $column = $column[0]; $inverseColumn = array_values($assoc['join']['inverseColumn']); $inverseColumn = $inverseColumn[0]; $connectData = array( $column[1] => $ownerEntityId, $inverseColumn[1] => $referenceId, ); /** @var \Bitrix\Main\Entity\DataManager $ormTableClassName */ $ormTableClassName = $assoc['join']['tableClassName']; $ormTableClassName::add($connectData); } } } } /** * @param static $reference * @param $assoc */ private function saveManyToOneReference($reference, $assoc) { if ($reference instanceof $assoc['targetEntity']) { $reference = clone $reference; $reference->{$assoc['inversedBy']} = null; $reference->save(); $this->{$assoc['join']['field'][0]} = $reference->getId(); } } /** * @return array */ private function getConvertedMapAttributesToOrmFields() { $result = array(); $fieldsMap = static::getMapAttributes(); foreach ($fieldsMap as $ormFieldName => $objectProperty) { $result[$ormFieldName] = $this->{$objectProperty}; } return $result; } /** * @param array $data * @return \Bitrix\Main\Entity\AddResult */ private function add(array $data) { $tableClassName = static::getTableClassName(); $resultData = $tableClassName::add($data); $this->currentDbState = $resultData->getData(); return $resultData; } /** * @param $newEntityAttributes * @return array */ private function getChangedOrmAttributes($newEntityAttributes) { /** * DONE * Optimise here: maybe add some property where will located state of values of entity when it select from DB */ $oldEntityAttributes = $this->getCurrentDbState(); unset($oldEntityAttributes['CREATED_DATE']); unset($oldEntityAttributes['UPDATED_DATE']); unset($newEntityAttributes['CREATED_DATE']); unset($newEntityAttributes['UPDATED_DATE']); $result = array(); foreach ($oldEntityAttributes as $key => $value) { if ($newEntityAttributes[$key] != $value) { $result[$key] = $newEntityAttributes[$key]; } } return $result; } /** * @param $primary * @param array $data * @return \Bitrix\Main\Entity\UpdateResult */ private function update($primary, array $data) { $tableClassName = static::getTableClassName(); $data['UPDATED_DATE'] = new DateTime(); $resultData = $tableClassName::update($primary, $data); foreach ($resultData->getData() as $key => $value) { $this->currentDbState[$key] = $value; } return $resultData; } /** * @return string */ public static function getClassName() { return get_called_class(); } /** * @param array|ConditionTree $filter Filter parameters. * @param array $with Relation keys to load. * @param array $order Order parameters. * @return static */ public static function load($filter, array $with = array(), $order = array()) { $models = static::getModelList(array( 'select' => array('*'), 'filter' => $filter, 'with' => $with, 'order' => $order )); return array_shift($models); } /** * Get model list like getList * @param array $parameters * @return static[] */ protected static function getModelList(array $parameters) { $modelList = array(); $query = static::getList($parameters); while ($row = $query->fetch()) { if (!$modelList[$row['ID']]) { $model = static::buildFromArray($row); } else { $model = static::buildFromArray($row, $modelList[$row['ID']]); } if ($model->id) { $modelList[$model->id] = $model; } } return $modelList; } /** * @param array $parameters * @return \Bitrix\Main\DB\Result * @throws \Bitrix\Main\NotImplementedException */ protected static function getList(array $parameters) { /** @var DataManager $tableClass */ $tableClass = static::getTableClassName(); return $tableClass::getList(static::prepareGetListParameters($parameters)); } /** * Builds model from array. * @param array $attributes Model attributes. * @param $parentEntity * @return static * @internal */ protected static function buildFromArray(array $attributes, $parentEntity = null) { /** @var Model $model */ $model = new static; return $model->setAttributes($attributes, $parentEntity); } /** * Method which transfer from Array to object of special type. * * @param array $attributes * @param $parentEntity * @return Model */ private function setAttributes(array $attributes, $parentEntity) { foreach ($attributes as $key => $value) { if ($value === null) { unset($attributes[$key]); } } if (!$parentEntity) { $mapAttributes = static::getMapAttributes(); foreach ($attributes as $key => $value) { if (!empty($mapAttributes[$key])) { $this->{$mapAttributes[$key]} = $value; $this->currentDbState[$key] = $value; unset($attributes[$key]); } } $parentEntity = $this; } if (empty($attributes)) { return $parentEntity; } $subEntitiesMapAttributes = static::getMapReferenceAttributes(); $subEntitiesKeys = array_keys($subEntitiesMapAttributes); $subEntityAttributes = array(); $loadedSubEntitiesKeys = array(); foreach ($attributes as $key => $value) { $delimiter = self::ATTRIBUTE_SLICE_DELIMITER; $selectedAttributeParts = explode($delimiter, $key); if (count($selectedAttributeParts) == 2 && in_array($selectedAttributeParts[0], $subEntitiesKeys)) { $loadedSubEntitiesKeys[$selectedAttributeParts[0]] = $selectedAttributeParts[0]; $subEntityAttributes[$selectedAttributeParts[0]][$selectedAttributeParts[1]] = $value; } elseif (count($selectedAttributeParts) >= 2) { $nestedEntityParentKey = array_shift($selectedAttributeParts); $nestedElementKey = implode(self::ATTRIBUTE_SLICE_DELIMITER, $selectedAttributeParts); $subEntityAttributes[$nestedEntityParentKey][$nestedElementKey] = $value; } } foreach ($subEntityAttributes as $key => $validAttributes) { if (!empty($subEntitiesMapAttributes[$key])) { /** @var static $targetEntityClass */ $targetEntityClass = $subEntitiesMapAttributes[$key]['targetEntity']; if ($subEntitiesMapAttributes[$key]['type'] != Common::MANY_TO_ONE) { if (!isset($parentEntity->{$key}[$validAttributes['ID']])) { $subEntity = $targetEntityClass::buildFromArray($validAttributes); $nestedEntityReferenceMap = $subEntity::getMapReferenceAttributes(); /** * If connection type is one to many we can map to nested parent entity automatically */ if ($subEntitiesMapAttributes[$key]['type'] == Common::ONE_TO_MANY) { if (!empty($nestedEntityReferenceMap[$subEntitiesMapAttributes[$key]['mappedBy']])) { $subEntity->{$subEntitiesMapAttributes[$key]['mappedBy']} = $parentEntity; } } $parentEntity->{$key}[$subEntity->id] = $subEntity; } else { $targetEntityClass::buildFromArray($validAttributes, $parentEntity->{$key}[$validAttributes['ID']]); } } else { $subEntity = $targetEntityClass::buildFromArray($validAttributes); $parentEntity->{$key} = $subEntity; } } } return $parentEntity; } /** * @param array $parameters * @throws \Bitrix\Main\SystemException * @return array */ protected static function prepareGetListParameters(array $parameters) { if (!empty($parameters['with'])) { if (!is_array($parameters['with'])) { throw new ArgumentException('"with" must be array'); } if (!isset($parameters['select'])) { $parameters['select'] = array('*'); } elseif (!in_array('*', $parameters['select']) && !in_array('ID', $parameters['select'])) { $parameters['select'][] = 'ID'; } $parameters['select'] = array_merge($parameters['select'], static::buildOrmSelectForReference($parameters['with'])); } unset($parameters['with']); return $parameters; } /** * @param array $with * @return array * @throws ArgumentException */ protected static function buildOrmSelectForReference(array $with) { $select = array(); $referenceAttributes = static::getMapReferenceAttributes(); foreach ($with as $referenceKey) { $testNesting = explode('.', $referenceKey); $nestedReferenceAttributes = $referenceAttributes; $prefix = ''; $fromKeyNamePrefix = ''; foreach ($testNesting as $reference) { if (!empty($nestedReferenceAttributes[$reference])) { $prefix = $prefix . $reference . self::ATTRIBUTE_SLICE_DELIMITER; switch ($nestedReferenceAttributes[$reference]['type']) { case Common::ONE_TO_MANY: /** @var static $targetEntity */ $targetEntity = $nestedReferenceAttributes[$reference]['targetEntity']; $targetOrmTable = $targetEntity::getTableClassName(); $fromKeyNamePrefix = !empty($fromKeyNamePrefix) ? $fromKeyNamePrefix . '.' : ''; $select[$prefix] = $fromKeyNamePrefix . $targetOrmTable::getClassName() . ':' . strtoupper($nestedReferenceAttributes[$reference]['mappedBy']); $fromKeyNamePrefix .= $targetOrmTable::getClassName() . ':' . strtoupper($nestedReferenceAttributes[$reference]['mappedBy']); $nestedReferenceAttributes = $targetEntity::getMapReferenceAttributes(); break; case Common::MANY_TO_MANY: $fromKeyName = array_keys($nestedReferenceAttributes[$reference]['join']['column']); $fromKeyName = $fromKeyName[0]; $fromKeyNamePrefix = !empty($fromKeyNamePrefix) ? $fromKeyNamePrefix . '.' : ''; $toKeyName = array_keys($nestedReferenceAttributes[$reference]['join']['inverseColumn']); $toKeyName = $toKeyName[0]; $select[$prefix] = $fromKeyNamePrefix . $nestedReferenceAttributes[$reference]['join']['tableClassName'] . ':' . $fromKeyName . '.' . $toKeyName; $targetEntity = $nestedReferenceAttributes[$reference]['targetEntity']; $nestedReferenceAttributes = $targetEntity::getMapReferenceAttributes(); break; case Common::MANY_TO_ONE: $fromKeyNamePrefix = !empty($fromKeyNamePrefix) ? $fromKeyNamePrefix . '.' : ''; $select[$prefix] = $fromKeyNamePrefix . strtoupper($reference); $fromKeyNamePrefix .= strtoupper($reference); break; } } else { throw new ArgumentException("Reference with name:" . $reference . ' not define in reference map'); } } } return $select; } /** * @return bool|null */ public function delete() { $ownerId = $this->getId(); $referenceAttributesMap = static::getMapReferenceAttributes(); foreach ($referenceAttributesMap as $referenceKey => $referenceAttributes) { if (!empty($referenceAttributes['options']['deleteSkip'])) { continue; } if (!$this->{$referenceKey}) { $this->loadAttribute($referenceKey); } if ($this->{$referenceKey}) { switch ($referenceAttributes['type']) { case Common::ONE_TO_MANY: $this->deleteOneToManyReferences($this->{$referenceKey}); break; case Common::MANY_TO_MANY: $this->deleteManyToManyReferences($this->{$referenceKey}, $referenceAttributes, $ownerId); break; case Common::MANY_TO_ONE: $this->deleteManyToOneReference($this->{$referenceKey}, $referenceAttributes, $ownerId); break; case Common::ONE_TO_ONE: //TODO break; } } } $entityTableClass = static::getTableClassName(); $deleteEntity = $entityTableClass::delete($ownerId); if ($deleteEntity->isSuccess()) { return true; } else { $this->errors[] = $deleteEntity->getErrors(); return null; } } /** * @param static[] $referenceEntities */ private function deleteOneToManyReferences($referenceEntities) { foreach ($referenceEntities as $referenceEntity) { $referenceEntity->delete(); } } /** * @param static[] $referenceEntities * @param $assoc * @param $ownerId */ private function deleteManyToManyReferences($referenceEntities, $assoc, $ownerId) { $connectColumn = array_shift($assoc['join']['column']); $connectInverseColumn = array_shift($assoc['join']['inverseColumn']); foreach ($referenceEntities as $referenceEntity) { $connectPrimaryKey = array(); /** @var \Bitrix\Main\Entity\DataManager $connectTableClass */ $connectTableClass = $assoc['join']['tableClassName']; $connectPrimaryKey[$connectColumn[1]] = $ownerId; $connectPrimaryKey[$connectInverseColumn[1]] = $referenceEntity->getId();; $connectTableClass::delete($connectPrimaryKey); $referenceEntity->delete(); } } /** * Clean from parent reference list deleted entity * @param $referenceEntity * @param $assoc * @param $ownerId */ private function deleteManyToOneReference(&$referenceEntity, $assoc, $ownerId) { if ($referenceEntity instanceof $assoc['targetEntity']) { unset($referenceEntity->{$assoc['inversedBy']}[$ownerId]); } } /** * @return mixed */ public function getId() { return $this->id; } /** * @param mixed $id Id property value. * @return void */ public function setId($id) { $this->id = $id; } /** * @return DateTime */ public function getCreatedAt() { return $this->createdAt; } /** * @param DateTime $createdAt Record create time. * @return void */ public function setCreatedAt(DateTime $createdAt) { $this->createdAt = $createdAt; } /** * @return DateTime */ public function getUpdatedAt() { return $this->updatedAt; } /** * @param DateTime $updatedAt Record update time. * @return void */ public function setUpdatedAt(DateTime $updatedAt) { $this->updatedAt = $updatedAt; } /** * @param mixed $id Load entity by id. * @return static */ public static function loadById($id) { $entity = static::load(array('ID' => $id)); return $entity; } /** * @param string $attributeName Attribute name to load to property from db. * @return void */ public function loadAttribute($attributeName) { if (property_exists($this, $attributeName) && $this->{$attributeName} == null) { $entity = static::load(array('ID' => $this->getId()), array($attributeName)); $referencesAttributeMap = $this::getMapReferenceAttributes(); foreach ($referencesAttributeMap as $referenceKey => $referenceMapAttributes) { if ($referenceMapAttributes['type'] === Common::ONE_TO_MANY && $referenceKey === $attributeName && !empty($entity->{$attributeName})) { foreach ($entity->{$attributeName} as $subEntity) { if ($subEntity instanceof $referenceMapAttributes['targetEntity']) { $entity->{$attributeName}->{$referenceMapAttributes['mappedBy']} = $this; } } } } $this->{$attributeName} = $entity->{$attributeName}; } } /** * Implement delete and add actions for nested relations. * * @param string $name Getter name. * @param array $arguments Arguments passed to getter. * @return void */ public function __call($name , array $arguments) { $isDeleteReferenceCall = preg_match_all('/^delete(\w+)/', $name, $deleteCallNameParts); if ($isDeleteReferenceCall) { $referenceName = $deleteCallNameParts[1][0]; $referenceName = strtolower($referenceName); $referenceMapAttributes = $this::getMapReferenceAttributes(); if (!empty($referenceMapAttributes[$referenceName])) { /** @var static[] $entities */ $entities = !empty($arguments[0]) ? $arguments[0] : array(); if (!is_array($entities)) { $entities = array( $entities ); } foreach ($entities as $entity) { $this->deletedEntities[$referenceName][$entity->getId()] = $entity; unset($this->{$referenceName}[$entity->getId()]); } } } $isAddReferenceCall = preg_match_all('/^add(\w+)/', $name, $addCallNameParts); if ($isAddReferenceCall) { $referenceName = $addCallNameParts[1][0]; $referenceName = strtolower($referenceName); $referenceMapAttributes = $this::getMapReferenceAttributes(); if (!empty($referenceMapAttributes[$referenceName])) { /** @var static[] $entities */ $entities = !empty($arguments[0]) ? $arguments[0] : array(); if (!is_array($entities)) { $entities = array( $entities ); } foreach ($entities as $entity) { $this->{$referenceName}[] = $entity; } } } } /** * @param static[][] $deletedReferenceEntities */ private function deleteReference($deletedReferenceEntities) { foreach ($deletedReferenceEntities as $referenceName => $referenceEntities) { $map = $this::getMapReferenceAttributes(); $map = $map[$referenceName]; switch ($map['type']) { case Common::ONE_TO_MANY: foreach ($referenceEntities as $referenceEntity) { $referenceEntity->delete(); } break; case Common::MANY_TO_MANY: $connectColumn = array_shift($map['join']['column']); $connectInverseColumn = array_shift($map['join']['inverseColumn']); foreach ($referenceEntities as $referenceEntity) { $connectPrimaryKey = array(); /** @var \Bitrix\Main\Entity\DataManager $connectTableClass */ $connectTableClass = $map['join']['tableClassName']; $connectPrimaryKey[$connectColumn[1]] = $this->getId(); $connectPrimaryKey[$connectInverseColumn[1]] = $referenceEntity->getId(); $connectTableClass::delete($connectPrimaryKey); } break; case Common::ONE_TO_ONE: //TODO break; case Common::MANY_TO_ONE: //TODO break; } } } /** * @return array */ public function getErrors() { return $this->errors; } /** * @return array */ public function getCurrentDbState() { return $this->currentDbState; } }