diff --git a/application/Espo/Core/Utils/FieldManager.php b/application/Espo/Core/Utils/FieldManager.php index c8d1a4a934..ca288ca401 100644 --- a/application/Espo/Core/Utils/FieldManager.php +++ b/application/Espo/Core/Utils/FieldManager.php @@ -39,7 +39,6 @@ class FieldManager protected $customOptionName = 'isCustom'; - public function __construct(Metadata $metadata, Language $language) { $this->metadata = $metadata; @@ -122,7 +121,8 @@ class FieldManager 'links.'.$name, ); - $res = $this->getMetadata()->delete($unsets, $this->metadataType, $scope); + $this->getMetadata()->delete($this->metadataType, $scope, $unsets); + $res = $this->getMetadata()->save(); $res &= $this->deleteLabel($name, $scope); return (bool) $res; @@ -132,25 +132,25 @@ class FieldManager { $fieldDef = $this->normalizeDefs($name, $fieldDef, $scope); - $data = Json::encode($fieldDef); - $res = $this->getMetadata()->set($data, $this->metadataType, $scope); + $this->getMetadata()->set($this->metadataType, $scope, $fieldDef); + $res = $this->getMetadata()->save(); return $res; } protected function setTranslatedOptions($name, $value, $scope) { - return $this->getLanguage()->set($name, $value, 'options', $scope); + return $this->getLanguage()->set($scope, 'options', $name, $value); } protected function setLabel($name, $value, $scope) { - return $this->getLanguage()->set($name, $value, 'fields', $scope); + return $this->getLanguage()->set($scope, 'fields', $name, $value); } protected function deleteLabel($name, $scope) { - $this->getLanguage()->delete($name, 'fields', $scope); + $this->getLanguage()->delete($scope, 'fields', $name); return $this->getLanguage()->save(); } diff --git a/application/Espo/Core/Utils/Language.php b/application/Espo/Core/Utils/Language.php index 1c5bd51cc1..19439929b8 100644 --- a/application/Espo/Core/Utils/Language.php +++ b/application/Espo/Core/Utils/Language.php @@ -220,7 +220,6 @@ class Language } $this->clearChanges(); - $this->init(true); return (bool) $result; } @@ -234,6 +233,7 @@ class Language { $this->changedData = array(); $this->deletedData = array(); + $this->init(true); } /** @@ -253,53 +253,76 @@ class Language /** * Set/change a label - * @param string | array $label - * @param mixed $value - * @param string $category + * * @param string $scope + * @param string $category + * @param string | array $name + * @param mixed $value + * + * @return void */ - public function set($label, $value, $category = 'labels', $scope = 'Global') + public function set($scope, $category, $name, $value) { - if (is_array($label)) { - foreach ($label as $rowLabel => $rowValue) { - $this->set($rowLabel, $rowValue, $category, $scope); + if (is_array($name)) { + foreach ($name as $rowLabel => $rowValue) { + $this->set($scope, $category, $rowLabel, $rowValue); } return; } - $this->changedData[$scope][$category][$label] = $value; + $this->changedData[$scope][$category][$name] = $value; $currentLanguage = $this->getLanguage(); - $this->data[$currentLanguage][$scope][$category][$label] = $value; + if (!isset($this->data[$currentLanguage])) { + $this->init(); + } + $this->data[$currentLanguage][$scope][$category][$name] = $value; + + $this->undelete($scope, $category, $name); } /** * Remove a label * - * @param string $label + * @param string $name * @param string $category * @param string $scope * * @return void */ - public function delete($label, $category = 'labels', $scope = 'Global') + public function delete($scope, $category, $name) { - if (is_array($label)) { - foreach ($label as $rowLabel) { - $this->delete($rowLabel, $category, $scope); + if (is_array($name)) { + foreach ($name as $rowLabel) { + $this->delete($scope, $category, $rowLabel); } return; } - $this->deletedData[$scope][$category][] = $label; + $this->deletedData[$scope][$category][] = $name; $currentLanguage = $this->getLanguage(); - if (isset($this->data[$currentLanguage][$scope][$category][$label])) { - unset($this->data[$currentLanguage][$scope][$category][$label]); + if (!isset($this->data[$currentLanguage])) { + $this->init(); } - if (isset($this->changedData[$scope][$category][$label])) { - unset($this->changedData[$scope][$category][$label]); + if (isset($this->data[$currentLanguage][$scope][$category][$name])) { + unset($this->data[$currentLanguage][$scope][$category][$name]); + } + + if (isset($this->changedData[$scope][$category][$name])) { + unset($this->changedData[$scope][$category][$name]); + } + } + + protected function undelete($scope, $category, $name) + { + if (isset($this->deletedData[$scope][$category])) { + foreach ($this->deletedData[$scope][$category] as $key => $labelName) { + if ($name === $labelName) { + unset($this->deletedData[$scope][$category][$key]); + } + } } } diff --git a/application/Espo/Core/Utils/Metadata.php b/application/Espo/Core/Utils/Metadata.php index d16c86f156..25959c0fa4 100644 --- a/application/Espo/Core/Utils/Metadata.php +++ b/application/Espo/Core/Utils/Metadata.php @@ -61,6 +61,10 @@ class Metadata */ protected $defaultModuleOrder = 10; + private $deletedData = array(); + + private $changedData = array(); + public function __construct(\Espo\Core\Utils\Config $config, \Espo\Core\Utils\File\Manager $fileManager) { $this->config = $config; @@ -213,52 +217,139 @@ class Metadata /** * Set Metadata data - * Ex. $type= menu, $scope= Account then will be created a file metadataFolder/menu/Account.json + * Ex. $key1 = menu, $key2 = Account then will be created a file metadataFolder/menu/Account.json * + * @param string $key1 + * @param string $key2 * @param JSON string $data - * @param string $type - ex. menu - * @param string $scope - Account * * @return bool */ - public function set($data, $type, $scope) + public function set($key1, $key2, $data) { - $path = $this->paths['customPath']; + $newData = array( + $key1 => array( + $key2 => $data, + ), + ); - $result = $this->getFileManager()->mergeContents(array($path, $type, $scope.'.json'), $data, true); - if ($result === false) { - throw new Error("Error saving metadata. See log file for details."); - } + $this->changedData = Util::merge($this->changedData, $newData); + $this->meta = Util::merge($this->getData(), $newData); - $this->init(true); - - return $result; + $this->undelete($key1, $key2, $data); } /** * Unset some fields and other stuff in metadat * + * @param string $key1 + * @param string $key2 * @param array | string $unsets Ex. 'fields.name' - * @param string $type Ex. 'entityDefs' - * @param string $scope + * * @return bool */ - public function delete($unsets, $type, $scope) + public function delete($key1, $key2, $unsets) + { + if (!is_array($unsets)) { + $unsets = (array) $unsets; + } + + $normalizedData = array( + '__APPEND__', + ); + $metaUnsetData = array(); + foreach ($unsets as $unsetItem) { + $normalizedData[] = $unsetItem; + $metaUnsetData[] = implode('.', array($key1, $key2, $unsetItem)); + } + + $unsetData = array( + $key1 => array( + $key2 => $normalizedData, + ), + ); + + $this->deletedData = Util::merge($this->deletedData, $unsetData); + $this->deletedData = Util::unsetInArrayByValue('__APPEND__', $this->deletedData); + + $this->meta = Util::unsetInArray($this->getData(), $metaUnsetData); + } + + /** + * Undelete the deleted items + * + * @param string $key1 + * @param string $key2 + * @param array $data + * @return void + */ + protected function undelete($key1, $key2, $data) + { + if (isset($this->deletedData[$key1][$key2])) { + foreach ($this->deletedData[$key1][$key2] as $unsetIndex => $unsetItem) { + $value = Util::getValueByKey($data, $unsetItem); + if (isset($value)) { + unset($this->deletedData[$key1][$key2][$unsetIndex]); + } + } + } + } + + /** + * Clear unsaved changes + * + * @return void + */ + public function clearChanges() + { + $this->changedData = array(); + $this->deletedData = array(); + $this->init(true); + } + + /** + * Save changes + * + * @return bool + */ + public function save() { $path = $this->paths['customPath']; - $result = $this->getFileManager()->unsetContents(array($path, $type, $scope.'.json'), $unsets, true); - - if ($result == false) { - $GLOBALS['log']->warning('Delete metadata items available only for custom code.'); + $result = true; + if (!empty($this->changedData)) { + foreach ($this->changedData as $key1 => $keyData) { + foreach ($keyData as $key2 => $data) { + if (!empty($data)) { + $result &= $this->getFileManager()->mergeContents(array($path, $key1, $key2.'.json'), $data, true); + } + } + } } - $this->init(true); + if (!empty($this->deletedData)) { + foreach ($this->deletedData as $key1 => $keyData) { + foreach ($keyData as $key2 => $unsetData) { + if (!empty($unsetData)) { + $rowResult = $this->getFileManager()->unsetContents(array($path, $key1, $key2.'.json'), $unsetData, true); + if ($rowResult == false) { + $GLOBALS['log']->warning('Metadata items ['.$key1.'.'.$key2.'] can be deleted for custom code only.'); + } + $result &= $rowResult; + } + } + } + } - return $result; + if ($result == false) { + throw new Error("Error saving metadata. See log file for details."); + } + + $this->clearChanges(); + + return (bool) $result; } - public function getOrmMetadata($reload = false) { if (!empty($this->ormMeta) && is_array($this->ormMeta) && !$reload) { diff --git a/tests/Espo/Core/Utils/FieldManagerTest.php b/tests/Espo/Core/Utils/FieldManagerTest.php index fc045f10bd..6db875d491 100644 --- a/tests/Espo/Core/Utils/FieldManagerTest.php +++ b/tests/Espo/Core/Utils/FieldManagerTest.php @@ -1,4 +1,4 @@ -method('get') ->will($this->returnValue($data)); - $this->assertTrue($this->object->update('name', $data, 'Account')); + $this->object->update('name', $data, 'Account'); } public function testUpdateCustomFieldIsNotChanged() @@ -138,10 +138,9 @@ class FieldManagerTest extends \PHPUnit_Framework_TestCase "isCustom" => true, ); - $this->assertTrue($this->object->update('varName', $data, 'CustomEntity')); + $this->object->update('varName', $data, 'CustomEntity'); } - public function testRead() { $data = array( diff --git a/tests/Espo/Core/Utils/LanguageTest.php b/tests/Espo/Core/Utils/LanguageTest.php index 99b5b64df5..788cde284f 100644 --- a/tests/Espo/Core/Utils/LanguageTest.php +++ b/tests/Espo/Core/Utils/LanguageTest.php @@ -1,4 +1,4 @@ -object->setLanguage($originalLang); } - public function testGetLangCacheFile() { $cacheFile = $this->cacheFile; @@ -96,7 +95,6 @@ class LanguageTest extends \PHPUnit_Framework_TestCase $this->object->setLanguage($originalLang); } - public function testGetData() { $result = array ( @@ -142,7 +140,6 @@ class LanguageTest extends \PHPUnit_Framework_TestCase $this->assertEquals($result, $this->reflection->invokeMethod('getData', array())); } - public function testGet() { $result = array ( @@ -204,7 +201,6 @@ class LanguageTest extends \PHPUnit_Framework_TestCase $this->assertEquals($result, $this->object->translate('language', 'options', 'Global', $requiredOptions)); } - public function testTranslateArray() { $input = array( @@ -227,20 +223,124 @@ class LanguageTest extends \PHPUnit_Framework_TestCase public function testSet() { $label = 'TEST'; - $this->object->set('label', $label, 'fields', 'User'); + $this->object->set('User', 'fields', 'label', $label); $this->assertEquals($label, $this->object->translate('label', 'fields', 'User')); + $result = array( + 'User' => array( + 'fields' => array( + 'label' => 'TEST', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('changedData')); + $label2 = 'TEST2'; - $this->object->set('name', $label2, 'fields', 'User'); + $this->object->set('User', 'fields', 'name', $label2); $this->assertEquals($label2, $this->object->translate('name', 'fields', 'User')); + $result = array( + 'User' => array( + 'fields' => array( + 'label' => 'TEST', + 'name' => 'TEST2', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('changedData')); + $label3 = 'TEST3'; - $this->object->set('name', $label3, 'fields', 'Account'); + $this->object->set('Account', 'fields', 'name', $label3); $this->assertEquals($label3, $this->object->translate('name', 'fields', 'Account')); - $this->reflection->invokeMethod('init', array(true)); + $result = array( + 'User' => array( + 'fields' => array( + 'label' => 'TEST', + 'name' => 'TEST2', + ), + ), + 'Account' => array( + 'fields' => array( + 'name' => 'TEST3', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('changedData')); + + $this->object->clearChanges(); + + $this->assertEquals(array(), $this->reflection->getProperty('changedData')); + $this->assertNotEquals('TEST', $this->object->get('User', 'fields', 'label')); + } + + public function testDelete() + { + $this->object->delete('User', 'fields', 'label'); + $this->assertNull($this->object->get('User.fields.label')); + + $result = array( + 'User' => array( + 'fields' => array( + 'label', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('deletedData')); + + $this->object->delete('User', 'fields', 'name'); + $this->assertNull($this->object->get('User.fields.name')); + + $result = array( + 'User' => array( + 'fields' => array( + 'label', + 'name', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('deletedData')); + + $this->object->clearChanges(); + + $this->assertNotNull($this->object->get('User.fields.label')); + $this->assertNotNull($this->object->get('User.fields.name')); + + $this->assertEquals(array(), $this->reflection->getProperty('deletedData')); + } + + public function testUndelete() + { + $this->object->delete('User', 'fields', 'label'); + $this->assertNull($this->object->get('User.fields.label')); + + $this->object->delete('User', 'fields', 'name'); + $this->assertNull($this->object->get('User.fields.name')); + + $label = 'TEST'; + $this->object->set('User', 'fields', 'label', $label); + $this->assertEquals($label, $this->object->translate('label', 'fields', 'User')); + + $result = array( + 'User' => array( + 'fields' => array( + 1 => 'name', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('deletedData')); + + $label2 = 'TEST2'; + $this->object->set('User', 'fields', 'name', $label2); + $this->assertEquals($label2, $this->object->translate('name', 'fields', 'User')); + + $result = array( + 'User' => array( + 'fields' => array( + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('deletedData')); } } - -?> diff --git a/tests/Espo/Core/Utils/MetadataTest.php b/tests/Espo/Core/Utils/MetadataTest.php index 38e5865013..337e7adb19 100644 --- a/tests/Espo/Core/Utils/MetadataTest.php +++ b/tests/Espo/Core/Utils/MetadataTest.php @@ -1,4 +1,4 @@ -cacheFile)) { + copy($this->defaultCacheFile, $this->cacheFile); + } + $this->objects['config'] = $this->getMockBuilder('\Espo\Core\Utils\Config')->disableOriginalConstructor()->getMock(); - $this->objects['fileManager'] = new \Espo\Core\Utils\File\Manager(); + $this->objects['fileManager'] = new \Espo\Core\Utils\File\Manager(); + $this->objects['Unifier'] = $this->getMockBuilder('\Espo\Core\Utils\File\Unifier')->disableOriginalConstructor()->getMock(); //set to use cache $this->objects['config'] ->expects($this->any()) ->method('get') - ->will($this->returnValue(true)); - + ->will($this->returnValue(true)); + $this->object = new \Espo\Core\Utils\Metadata($this->objects['config'], $this->objects['fileManager']); - $this->reflection = new ReflectionHelper($this->object); - $this->reflection->setProperty('cacheFile', 'tests/testData/Utils/Metadata/metadata.php'); - $this->reflection->setProperty('ormCacheFile', 'tests/testData/Utils/Metadata/ormMetadata.php'); + $this->reflection = new ReflectionHelper($this->object); + $this->reflection->setProperty('cacheFile', $this->cacheFile); + $this->reflection->setProperty('ormCacheFile', $this->ormCacheFile); } protected function tearDown() { $this->object = NULL; } - - - function testGet() - { + + public function testGet() + { $result = 'System'; - $this->assertEquals($result, $this->object->get('app.adminPanel.system.label')); + $this->assertEquals($result, $this->object->get('app.adminPanel.system.label')); $result = 'fields'; - $this->assertArrayHasKey($result, $this->object->get('entityDefs.User')); - } + $this->assertArrayHasKey($result, $this->object->get('entityDefs.User')); + } + public function testSet() + { + $data = array ( + 'fields' => + array ( + 'name' => + array ( + 'required' => false, + 'maxLength' => 150, + 'view' => 'Views.Test.Custom', + ), + ), + ); + $this->object->set('entityDefs', 'Attachment', $data); + $this->assertEquals('Views.Test.Custom', $this->object->get('entityDefs.Attachment.fields.name.view')); + $this->assertEquals(150, $this->object->get('entityDefs.Attachment.fields.name.maxLength')); + $result = array( + 'entityDefs' => array( + 'Attachment' => $data + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('changedData')); -} + $data = array ( + 'fields' => + array ( + 'name' => + array ( + 'maxLength' => 200, + ), + ), + ); + $this->object->set('entityDefs', 'Attachment', $data); + $this->assertEquals(200, $this->object->get('entityDefs.Attachment.fields.name.maxLength')); + $this->assertEquals('Views.Test.Custom', $this->object->get('entityDefs.Attachment.fields.name.view')); -?> + $result = array( + 'entityDefs' => array( + 'Attachment' => array ( + 'fields' => + array ( + 'name' => + array ( + 'required' => false, + 'maxLength' => 200, + 'view' => 'Views.Test.Custom', + ), + ), + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('changedData')); + + $this->object->clearChanges(); + + $this->assertEquals(array(), $this->reflection->getProperty('changedData')); + $this->assertNull($this->object->get('entityDefs.Attachment.fields.name.view')); + } + + public function testDelete() + { + $data = array ( + 'fields.name.type', + ); + $this->object->delete('entityDefs', 'Attachment', $data); + $this->assertNull($this->object->get('entityDefs.Attachment.fields.name.type')); + + $result = array( + 'entityDefs' => array( + 'Attachment' => array( + 'fields.name.type', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('deletedData')); + + $data = array ( + 'fields.name.required', + ); + $this->object->delete('entityDefs', 'Attachment', $data); + $this->assertNull($this->object->get('entityDefs.Attachment.fields.name.required')); + + $result = array( + 'entityDefs' => array( + 'Attachment' => array( + 'fields.name.type', + 'fields.name.required', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('deletedData')); + + $this->object->init(false); + + $this->assertNotNull($this->object->get('entityDefs.Attachment.fields.name.type')); + $this->assertNotNull($this->object->get('entityDefs.Attachment.fields.name.required')); + + $this->object->clearChanges(); + $this->assertEquals(array(), $this->reflection->getProperty('deletedData')); + } + + public function testUndelete() + { + $data = array ( + 'fields.name.type', + 'fields.name.required', + ); + $this->object->delete('entityDefs', 'Attachment', $data); + $this->assertNull($this->object->get('entityDefs.Attachment.fields.name.type')); + + $data = array ( + 'fields' => + array ( + 'name' => + array ( + 'type' => 'enum', + ), + ), + ); + $this->object->set('entityDefs', 'Attachment', $data); + $this->assertEquals('enum', $this->object->get('entityDefs.Attachment.fields.name.type')); + + $result = array( + 'entityDefs' => array( + 'Attachment' => array( + 1 => 'fields.name.required', + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('deletedData')); + + $data = array ( + 'fields' => + array ( + 'name' => + array ( + 'required' => true, + ), + ), + ); + $this->object->set('entityDefs', 'Attachment', $data); + $this->assertEquals(true, $this->object->get('entityDefs.Attachment.fields.name.required')); + + $result = array( + 'entityDefs' => array( + 'Attachment' => array( + ), + ), + ); + $this->assertEquals($result, $this->reflection->getProperty('deletedData')); + } + +} \ No newline at end of file