diff --git a/application/Espo/Core/ORM/Entity.php b/application/Espo/Core/ORM/Entity.php index 7ffa140c10..2281efd05f 100644 --- a/application/Espo/Core/ORM/Entity.php +++ b/application/Espo/Core/ORM/Entity.php @@ -56,7 +56,7 @@ class Entity extends BaseEntity $this->hasRelation($field); } - public function loadParentNameField(string $field) + public function loadParentNameField(string $field) : void { if (!$this->hasAttribute($field. 'Id') || !$this->hasAttribute($field . 'Type')) { throw new LogicException("There's no link-parent field '{$field}'."); @@ -81,12 +81,16 @@ class Entity extends BaseEntity if ($foreignEntity) { $this->set($field . 'Name', $foreignEntity->get('name')); - } else { - $this->set($field . 'Name', null); + + return; } - } else { + $this->set($field . 'Name', null); + + return; } + + $this->set($field . 'Name', null); } protected function getRelationOrderParams(string $link) : ?array @@ -141,7 +145,7 @@ class Entity extends BaseEntity ]; } - public function loadLinkMultipleField(string $field, $columns = null) + public function loadLinkMultipleField(string $field, $columns = null) : void { if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Ids')) { return; @@ -158,7 +162,7 @@ class Entity extends BaseEntity } if (!empty($columns)) { - foreach ($columns as $key => $item) { + foreach ($columns as $item) { $select[] = $item; } } @@ -186,7 +190,9 @@ class Entity extends BaseEntity foreach ($collection as $e) { $id = $e->id; + $ids[] = $id; + $names->$id = $e->get('name'); if ($hasType) { @@ -221,7 +227,7 @@ class Entity extends BaseEntity } } - public function loadLinkField(string $field) + public function loadLinkField(string $field) : void { if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Id')) { throw new LogicException("There's no link field '{$field}'."); @@ -254,9 +260,13 @@ class Entity extends BaseEntity } $this->set($idAttribute, $entityId); + $this->set($field . 'Name', $entityName); } + /** + * @return ?mixed + */ public function getLinkMultipleName(string $field, string $id) { $namesAttribute = $field . 'Names'; @@ -267,18 +277,14 @@ class Entity extends BaseEntity $names = $this->get($namesAttribute); - if ($names instanceof StdClass) { - if (isset($names->$id)) { - if (isset($names->$id)) { - return $names->$id; - } - } + if ($names instanceof StdClass && isset($names->$id) && isset($names->$id)) { + return $names->$id; } return null; } - public function setLinkMultipleName(string $field, string $id, ?string $value) + public function setLinkMultipleName(string $field, string $id, ?string $value) : void { $namesAttribute = $field . 'Names'; @@ -293,9 +299,13 @@ class Entity extends BaseEntity } $object->$id = $value; + $this->set($namesAttribute, $object); } + /** + * @return ?mixed + */ public function getLinkMultipleColumn(string $field, string $column, string $id) { $columnsAttribute = $field . 'Columns'; @@ -306,18 +316,14 @@ class Entity extends BaseEntity $columns = $this->get($columnsAttribute); - if ($columns instanceof StdClass) { - if (isset($columns->$id)) { - if (isset($columns->$id->$column)) { - return $columns->$id->$column; - } - } + if ($columns instanceof StdClass && isset($columns->$id) && isset($columns->$id->$column)) { + return $columns->$id->$column; } return null; } - public function setLinkMultipleColumn(string $field, string $column, string $id, $value) + public function setLinkMultipleColumn(string $field, string $column, string $id, $value) : void { $columnsAttribute = $field . 'Columns'; @@ -344,14 +350,14 @@ class Entity extends BaseEntity $this->set($columnsAttribute, $object); } - public function setLinkMultipleIdList(string $field, array $idList) + public function setLinkMultipleIdList(string $field, array $idList) : void { $idsAttribute = $field . 'Ids'; $this->set($idsAttribute, $idList); } - public function addLinkMultipleId(string $field, string $id) + public function addLinkMultipleId(string $field, string $id) : void { $idsAttribute = $field . 'Ids'; @@ -379,7 +385,7 @@ class Entity extends BaseEntity } } - public function removeLinkMultipleId(string $field, string $id) + public function removeLinkMultipleId(string $field, string $id) : void { if ($this->hasLinkMultipleId($field, $id)) { $list = $this->getLinkMultipleIdList($field); @@ -421,7 +427,9 @@ class Entity extends BaseEntity { $idsAttribute = $field . 'Ids'; - if (!$this->hasAttribute($idsAttribute)) return false; + if (!$this->hasAttribute($idsAttribute)) { + return false; + } if (!$this->has($idsAttribute)) { if (!$this->isNew()) { diff --git a/application/Espo/Entities/Integration.php b/application/Espo/Entities/Integration.php index f24bfd1384..c2f1bb546e 100644 --- a/application/Espo/Entities/Integration.php +++ b/application/Espo/Entities/Integration.php @@ -122,6 +122,10 @@ class Integration extends \Espo\Core\ORM\Entity return parent::isAttributeChanged($name); } + /** + * @deprecated + * @todo Make protected. + */ public function populateFromArray(array $array, bool $onlyAccessible = true, bool $reset = false) : void { if ($reset) { @@ -141,29 +145,43 @@ class Integration extends \Espo\Core\ORM\Entity if (!is_null($value)) { switch ($defs['type']) { case self::VARCHAR: + break; + case self::BOOL: $value = ($value === 'true' || $value === '1' || $value === true); + break; + case self::INT: $value = intval($value); + break; + case self::FLOAT: $value = floatval($value); + break; + case self::JSON_ARRAY: $value = is_string($value) ? json_decode($value) : $value; + if (!is_array($value)) { $value = null; } break; + case self::JSON_OBJECT: $value = is_string($value) ? json_decode($value) : $value; - if (!($value instanceof \StdClass) && !is_array($value)) { + + if (!($value instanceof StdClass) && !is_array($value)) { $value = null; } + break; + default: + break; } } diff --git a/application/Espo/ORM/BaseEntity.php b/application/Espo/ORM/BaseEntity.php index 980629fe70..f64b2ac41b 100644 --- a/application/Espo/ORM/BaseEntity.php +++ b/application/Espo/ORM/BaseEntity.php @@ -29,21 +29,24 @@ namespace Espo\ORM; -use StdClass; - use const E_USER_DEPRECATED; +use StdClass; use InvalidArgumentException; class BaseEntity implements Entity { public $id = null; + protected $entityType; + private $isNew = false; private $isSaved = false; - protected $entityType; + protected $isFetched = false; + + protected $isBeingSaved = false; /** * @todo Make protected. Rename to `attributes`. @@ -54,21 +57,20 @@ class BaseEntity implements Entity protected $relations = []; /** - * @var array Field-Value pairs. + * A field-value map. */ protected $valuesContainer = []; /** - * @var array Field-Value pairs of initial values (fetched from DB). + * A field-value map of values fetched from DB (before changed). */ protected $fetchedValuesContainer = []; + /** + * @var ?EntityManager + */ protected $entityManager; - protected $isFetched = false; - - protected $isBeingSaved = false; - public function __construct(string $entityType, array $defs = [], ?EntityManager $entityManager = null) { $this->entityType = $entityType ?? null; @@ -79,6 +81,11 @@ class BaseEntity implements Entity $this->relations = $defs['relations'] ?? $this->relations; } + public function getId() : ?string + { + return $this->get('id'); + } + public function clear(string $name) : void { unset($this->valuesContainer[$name]); @@ -125,16 +132,22 @@ class BaseEntity implements Entity $this->id = $value; } - if ($this->hasAttribute($name)) { - $method = '_set' . ucfirst($name); - - if (method_exists($this, $method)) { - $this->$method($value); - } else { - $this->valuesContainer[$name] = $value; - } + if (!$this->hasAttribute($name)) { + return; } + $method = '_set' . ucfirst($name); + + if (method_exists($this, $method)) { + $this->$method($value); + + return; + } + + $this->populateFromArray([ + $name => $value, + ]); + return; } @@ -148,7 +161,7 @@ class BaseEntity implements Entity */ public function get(string $name, $params = []) { - if ($name == 'id') { + if ($name === 'id') { return $this->id; } @@ -172,7 +185,8 @@ class BaseEntity implements Entity // @todo Remove this. if ($this->hasRelation($name) && $this->id && $this->entityManager) { trigger_error( - "Accessing related records with Entity::get is deprecated. Use \$repository->getRelation(...)->find()", + "Accessing related records with Entity::get is deprecated. " . + "Use \$repository->getRelation(...)->find()", E_USER_DEPRECATED ); @@ -203,13 +217,17 @@ class BaseEntity implements Entity return false; } + /** + * @deprecated + * @todo Make protected. + */ public function populateFromArray(array $data, bool $onlyAccessible = true, bool $reset = false) : void { if ($reset) { $this->reset(); } - foreach ($this->getAttributes() as $attribute => $defs) { + foreach ($this->getAttributeList() as $attribute) { if (!array_key_exists($attribute, $data)) { continue; } @@ -226,79 +244,122 @@ class BaseEntity implements Entity $value = $data[$attribute]; - if (!is_null($value)) { - $valueType = $this->getAttributeType($attribute); - - if ($valueType === self::FOREIGN) { - $relation = $this->getAttributeParam($attribute, 'relation'); - $foreign = $this->getAttributeParam($attribute, 'foreign'); - - if (is_string($foreign)) { - $foreignEntityType = $this->getRelationParam($relation, 'entity'); - - if ($foreignEntityType && $this->entityManager) { - $valueType = $this->entityManager->getMetadata()->get( - $foreignEntityType, ['fields', $foreign, 'type'] - ); - } - } - } - - switch ($valueType) { - case self::VARCHAR: - - break; - - case self::BOOL: - $value = ($value === 'true' || $value === '1' || $value === true); - - break; - - case self::INT: - $value = intval($value); - - break; - - case self::FLOAT: - $value = floatval($value); - - break; - - case self::JSON_ARRAY: - $value = is_string($value) ? json_decode($value) : $value; - - if (!is_array($value)) { - $value = null; - } - - break; - - case self::JSON_OBJECT: - $value = is_string($value) ? json_decode($value) : $value; - - if (!($value instanceof StdClass) && !is_array($value)) { - $value = null; - } - - break; - - default: - break; - } - } - - $method = '_set' . ucfirst($attribute); - - if (method_exists($this, $method)) { - $this->$method($value); - - continue; - } - - $this->valuesContainer[$attribute] = $value; + $this->populateFromArrayItem($attribute, $value); } } + protected function populateFromArrayItem(string $attribute, $value) : void + { + $preparedValue = $this->preparePopulateFromArrayItemValue($attribute, $value); + + $method = '_set' . ucfirst($attribute); + + if (method_exists($this, $method)) { + $this->$method($preparedValue); + + return; + } + + $this->valuesContainer[$attribute] = $preparedValue; + } + + /** + * @return ?mixed + */ + protected function preparePopulateFromArrayItemValue(string $attribute, $value) + { + if (is_null($value)) { + return $value; + } + + $attributeType = $this->getAttributeType($attribute); + + if ($attributeType === self::FOREIGN) { + $attributeType = $this->getForeignAttributeType($attribute) ?? $attributeType; + } + + switch ($attributeType) { + case self::VARCHAR: + break; + + case self::BOOL: + return ($value === true || $value === 'true' || $value === '1'); + + case self::INT: + return intval($value); + + case self::FLOAT: + return floatval($value); + + case self::JSON_ARRAY: + $preparedValue = is_string($value) ? json_decode($value) : $value; + + if (!is_array($preparedValue)) { + $value = null; + } + + return $preparedValue; + + case self::JSON_OBJECT: + $preparedValue = is_string($value) ? json_decode($value) : $value; + + if (!($preparedValue instanceof StdClass) && !is_array($preparedValue)) { + $preparedValue = null; + } + + return $preparedValue; + + default: + break; + } + + return $value; + } + + private function getForeignAttributeType(string $attribute) : ?string + { + if (!$this->entityManager) { + return null; + } + + $defs = $this->entityManager->getDefs(); + + $entityDefs = $defs->getEntity($this->entityType); + + $relation = $entityDefs->getAttribute($attribute)->getParam('relation'); + $foreign = $entityDefs->getAttribute($attribute)->getParam('foreign'); + + if (!$relation) { + return null; + } + + if (!$foreign) { + return null; + } + + if (!is_string($foreign)) { + return self::VARCHAR; + } + + if (!$entityDefs->getRelation($relation)->hasForeignEntityType()) { + return null; + } + + $entityType = $entityDefs->getRelation($relation)->getForeignEntityType(); + + if (!$defs->hasEntity($entityType)) { + return null; + } + + $foreignEntityDefs = $defs->getEntity($entityType); + + if (!$foreignEntityDefs->hasAttribute($foreign)) { + return null; + } + + return $foreignEntityDefs->getAttribute($foreign)->getType(); + } + /** * Is an entity new. */ diff --git a/application/Espo/ORM/Entity.php b/application/Espo/ORM/Entity.php index 7379db5388..87bab08eeb 100644 --- a/application/Espo/ORM/Entity.php +++ b/application/Espo/ORM/Entity.php @@ -56,6 +56,11 @@ interface Entity const BELONGS_TO_PARENT = 'belongsToParent'; const HAS_CHILDREN = 'hasChildren'; + /** + * Get an entity ID. + */ + public function getId() : ?string; + /** * Reset all attributes (empty an entity). */ diff --git a/tests/unit/Espo/ORM/EntityTest.php b/tests/unit/Espo/ORM/EntityTest.php index 21ff1fb33c..c8b345914d 100644 --- a/tests/unit/Espo/ORM/EntityTest.php +++ b/tests/unit/Espo/ORM/EntityTest.php @@ -30,11 +30,11 @@ namespace tests\unit\Espo\ORM; use Espo\ORM\{ - DB\MysqlMapper, - DB\Query\Mysql as Query, - EntityFactory, Metadata, MetadataDataProvider, + Defs\Defs, + Defs\DefsData, + BaseEntity, }; use tests\unit\testData\DB\Job; @@ -57,19 +57,30 @@ class EntityTest extends \PHPUnit\Framework\TestCase ->willReturn($ormMetadata); $this->metadata = new Metadata($metadataDataProvider); + + $defsData = new DefsData($this->metadata); + + $defs = new Defs($defsData); + + $this->entityManager = $this->createMock(EntityManager::class); + + $this->entityManager + ->expects($this->any()) + ->method('getDefs') + ->willReturn($defs); } protected function tearDown() : void { } - protected function createEntity(string $entityType, string $className) + protected function createEntity(string $entityType, ?string $className = null) { - $em = $this->getMockBuilder(EntityManager::class)->disableOriginalConstructor()->getMock(); - $defs = $this->metadata->get($entityType); - $entity = new $className($entityType, $defs, $em); + $classNameToUse = $className ?? BaseEntity::class; + + $entity = new $classNameToUse($entityType, $defs, $this->entityManager); return $entity; } @@ -236,4 +247,43 @@ class EntityTest extends \PHPUnit\Framework\TestCase ]); $this->assertTrue($job->isAttributeChanged('object')); } + + public function testSetForeign() + { + $entity = $this->createEntity('Comment'); + + $entity->set([ + 'postName' => 'test', + ]); + + $this->assertEquals('test', $entity->get('postName')); + } + + public function testSetJsonObject() + { + $entity = $this->createEntity('Test'); + + $value = '{"test": "1"}'; + + $entity->set([ + 'object' => '{"test": "1"}', + ]); + + $this->assertEquals(json_decode($value), $entity->get('object')); + } + + public function testSetWrongType() + { + $entity = $this->createEntity('Test'); + + $entity->set([ + 'int' => '1', + ]); + + $this->assertEquals(1, $entity->get('int')); + + $entity->set('int', '1'); + + $this->assertEquals(1, $entity->get('int')); + } }