diff --git a/app/code/Magento/Backend/Block/System/Account/Edit/Form.php b/app/code/Magento/Backend/Block/System/Account/Edit/Form.php index e620f365e7426..77aaf5e60b47d 100644 --- a/app/code/Magento/Backend/Block/System/Account/Edit/Form.php +++ b/app/code/Magento/Backend/Block/System/Account/Edit/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Backend\Block\System\Account\Edit; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Locale\OptionInterface; + /** * Adminhtml edit admin user account form * @@ -29,6 +32,13 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic */ protected $_localeLists; + /** + * Operates with deployed locales. + * + * @var OptionInterface + */ + private $deployedLocales; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -37,6 +47,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Backend\Model\Auth\Session $authSession * @param \Magento\Framework\Locale\ListsInterface $localeLists * @param array $data + * @param OptionInterface $deployedLocales Operates with deployed locales */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -45,11 +56,14 @@ public function __construct( \Magento\User\Model\UserFactory $userFactory, \Magento\Backend\Model\Auth\Session $authSession, \Magento\Framework\Locale\ListsInterface $localeLists, - array $data = [] + array $data = [], + OptionInterface $deployedLocales = null ) { $this->_userFactory = $userFactory; $this->_authSession = $authSession; $this->_localeLists = $localeLists; + $this->deployedLocales = $deployedLocales + ?: ObjectManager::getInstance()->get(OptionInterface::class); parent::__construct($context, $registry, $formFactory, $data); } @@ -121,7 +135,7 @@ protected function _prepareForm() 'name' => 'interface_locale', 'label' => __('Interface Locale'), 'title' => __('Interface Locale'), - 'values' => $this->_localeLists->getTranslatedOptionLocales(), + 'values' => $this->deployedLocales->getTranslatedOptionLocales(), 'class' => 'select' ] ); diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/LockProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/LockProcessor.php index 58a93e61781ba..8b97ba2bad469 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/LockProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/LockProcessor.php @@ -122,7 +122,7 @@ public function process($path, $value, $scope, $scopeCode) $backendModel->beforeSave(); $this->deploymentConfigWriter->saveConfig( - [ConfigFilePool::APP_CONFIG => $this->arrayManager->set($configPath, [], $backendModel->getValue())], + [ConfigFilePool::APP_ENV => $this->arrayManager->set($configPath, [], $backendModel->getValue())], false ); diff --git a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php index 787624b195361..f757115068da2 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php @@ -6,6 +6,7 @@ namespace Magento\Config\Console\Command; use Magento\Config\Console\Command\ConfigSet\ConfigSetProcessorFactory; +use Magento\Config\Model\Config\PathValidatorFactory; use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Scope\ValidatorInterface; @@ -33,33 +34,49 @@ class ConfigSetCommand extends Command /**#@-*/ /** + * The factory for config:set processors. + * * @var ConfigSetProcessorFactory */ private $configSetProcessorFactory; /** + * Scope validator. + * * @var ValidatorInterface */ private $validator; /** + * Scope manager. + * * @var ScopeInterface */ private $scope; /** - * @param ConfigSetProcessorFactory $configSetProcessorFactory - * @param ValidatorInterface $validator - * @param ScopeInterface $scope + * The factory for path validator. + * + * @var PathValidatorFactory + */ + private $pathValidatorFactory; + + /** + * @param ConfigSetProcessorFactory $configSetProcessorFactory The factory for config:set processors + * @param ValidatorInterface $validator Scope validator + * @param ScopeInterface $scope Scope manager + * @param PathValidatorFactory $pathValidatorFactory The factory for path validator */ public function __construct( ConfigSetProcessorFactory $configSetProcessorFactory, ValidatorInterface $validator, - ScopeInterface $scope + ScopeInterface $scope, + PathValidatorFactory $pathValidatorFactory ) { $this->configSetProcessorFactory = $configSetProcessorFactory; $this->validator = $validator; $this->scope = $scope; + $this->pathValidatorFactory = $pathValidatorFactory; parent::__construct(); } @@ -118,6 +135,14 @@ protected function execute(InputInterface $input, OutputInterface $output) // Emulating adminhtml scope to be able to read configs. $this->scope->setCurrentScope(Area::AREA_ADMINHTML); + /** + * Validates the entered config path. + * Requires emulated area. + */ + $this->pathValidatorFactory->create()->validate( + $input->getArgument(ConfigSetCommand::ARG_PATH) + ); + $processor = $input->getOption(static::OPTION_LOCK) ? $this->configSetProcessorFactory->create(ConfigSetProcessorFactory::TYPE_LOCK) : $this->configSetProcessorFactory->create(ConfigSetProcessorFactory::TYPE_DEFAULT); diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php index c60c983c383fa..77d29fa11a1fc 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php @@ -12,12 +12,18 @@ use Magento\Config\Model\Config\Structure\Element\Field; use Magento\Framework\Config\ScopeInterface; use Magento\Framework\App\Area; +use Magento\Config\Model\Config\Backend\Encrypted; /** * Class processes values using backend model which declared in system.xml. */ class ValueProcessor { + /** + * Placeholder for the output of sensitive data. + */ + const SAFE_PLACEHOLDER = '******'; + /** * System configuration structure factory. * @@ -55,7 +61,7 @@ public function __construct( } /** - * Processes value using backend model. + * Processes value to display using backend model. * * @param string $scope The scope of configuration. E.g. 'default', 'website' or 'store' * @param string $scopeCode The scope code of configuration @@ -78,6 +84,11 @@ public function process($scope, $scopeCode, $value, $path) $backendModel = $field && $field->hasBackendModel() ? $field->getBackendModel() : $this->configValueFactory->create(); + + if ($backendModel instanceof Encrypted) { + return $value ? self::SAFE_PLACEHOLDER : null; + } + $backendModel->setPath($path); $backendModel->setScope($scope); $backendModel->setScopeId($scopeCode); diff --git a/app/code/Magento/Config/Console/Command/ConfigShowCommand.php b/app/code/Magento/Config/Console/Command/ConfigShowCommand.php index 5003b0eab725b..17ad5aa199a2a 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShowCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigShowCommand.php @@ -144,7 +144,7 @@ protected function execute(InputInterface $input, OutputInterface $output) try { $this->scope = $input->getOption(self::INPUT_OPTION_SCOPE); $this->scopeCode = $input->getOption(self::INPUT_OPTION_SCOPE_CODE); - $this->inputPath = $input->getArgument(self::INPUT_ARGUMENT_PATH); + $this->inputPath = trim($input->getArgument(self::INPUT_ARGUMENT_PATH), '/'); $this->scopeValidator->isValid($this->scope, $this->scopeCode); $configPath = $this->pathResolver->resolve($this->inputPath, $this->scope, $this->scopeCode); diff --git a/app/code/Magento/Config/Model/Config/PathValidator.php b/app/code/Magento/Config/Model/Config/PathValidator.php new file mode 100644 index 0000000000000..49838c5dcb88d --- /dev/null +++ b/app/code/Magento/Config/Model/Config/PathValidator.php @@ -0,0 +1,47 @@ +structure = $structure; + } + + /** + * Checks whether the config path present in configuration structure. + * + * @param string $path The config path + * @return true The result of validation + * @throws ValidatorException If provided path is not valid + */ + public function validate($path) + { + $allPaths = $this->structure->getFieldPaths(); + + if (!array_key_exists($path, $allPaths)) { + throw new ValidatorException(__('The "%1" path does not exist', $path)); + } + + return true; + } +} diff --git a/app/code/Magento/Config/Model/Config/Structure.php b/app/code/Magento/Config/Model/Config/Structure.php index 9368f48f3a6ba..7f026adca3ce6 100644 --- a/app/code/Magento/Config/Model/Config/Structure.php +++ b/app/code/Magento/Config/Model/Config/Structure.php @@ -3,12 +3,11 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +namespace Magento\Config\Model\Config; /** - * System configuration structure + * System configuration structure. */ -namespace Magento\Config\Model\Config; - class Structure implements \Magento\Config\Model\Config\Structure\SearchInterface { /** @@ -251,4 +250,80 @@ protected function _getGroupFieldPathsByAttribute(array $fields, $parentPath, $a } return $result; } + + /** + * Collects config paths and their structure paths from configuration files. + * Returns the map of config paths and their structure paths. + * + * All paths are declared in module's system.xml. + * + * ```xml + *
+ * + * + * + * ... + * + * + * + * section/group/field + * ... + * + * + *
+ * ``` + * If node does not exist, then config path duplicates structure path. + * The result of this example will be: + * + * ```php + * [ + * 'section_id/group_id/field_one_id' => [ + * 'section_id/group_id/field_one_id' + * ], + * 'section/group/field' => [ + * 'section_id/group_id/field_two_id' + * ] + *``` + * + * @return array An array of config path to config structure path map + */ + public function getFieldPaths() + { + $sections = !empty($this->_data['sections']) ? $this->_data['sections'] : []; + + return $this->getFieldsRecursively($sections); + } + + /** + * Iteration that collects config field paths recursively from config files. + * + * @param array $elements The elements to be parsed + * @return array An array of config path to config structure path map + */ + private function getFieldsRecursively(array $elements = []) + { + $result = []; + + foreach ($elements as $element) { + if (isset($element['children'])) { + $result = array_replace_recursive( + $result, + $this->getFieldsRecursively($element['children']) + ); + } else { + if ($element['_elementType'] === 'field' && isset($element['label'])) { + $structurePath = (isset($element['path']) ? $element['path'] . '/' : '') . $element['id']; + $configPath = isset($element['config_path']) ? $element['config_path'] : $structurePath; + + if (!isset($result[$configPath])) { + $result[$configPath] = []; + } + + $result[$configPath][] = $structurePath; + } + } + } + + return $result; + } } diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockProcessorTest.php index 1ac9892f0f923..8c837baf14878 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockProcessorTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockProcessorTest.php @@ -164,7 +164,7 @@ public function testProcess($path, $value, $scope, $scopeCode) ->method('saveConfig') ->with( [ - ConfigFilePool::APP_CONFIG => [ + ConfigFilePool::APP_ENV => [ 'system' => [ 'default' => [ 'test' => [ @@ -234,7 +234,7 @@ public function testProcessBackendModelNotExists() ->method('saveConfig') ->with( [ - ConfigFilePool::APP_CONFIG => [ + ConfigFilePool::APP_ENV => [ 'system' => [ 'default' => [ 'test' => [ diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php index 1f4f30023c157..1a417073548b2 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php @@ -8,10 +8,13 @@ use Magento\Config\Console\Command\ConfigSet\ConfigSetProcessorFactory; use Magento\Config\Console\Command\ConfigSet\ConfigSetProcessorInterface; use Magento\Config\Console\Command\ConfigSetCommand; +use Magento\Config\Model\Config\PathValidator; +use Magento\Config\Model\Config\PathValidatorFactory; use Magento\Framework\App\Scope\ValidatorInterface; use Magento\Framework\Config\ScopeInterface; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; use PHPUnit_Framework_MockObject_MockObject as Mock; use Symfony\Component\Console\Tester\CommandTester; @@ -48,6 +51,16 @@ class ConfigSetCommandTest extends \PHPUnit_Framework_TestCase */ private $scopeMock; + /** + * @var PathValidatorFactory|Mock + */ + private $pathValidatorFactoryMock; + + /** + * @var PathValidator|Mock + */ + private $pathValidator; + /** * @inheritdoc */ @@ -63,11 +76,23 @@ protected function setUp() ->getMockForAbstractClass(); $this->scopeMock = $this->getMockBuilder(ScopeInterface::class) ->getMockForAbstractClass(); + $this->pathValidatorFactoryMock = $this->getMockBuilder(PathValidatorFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->pathValidator = $this->getMockBuilder(PathValidator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->pathValidatorFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->pathValidator); $this->command = new ConfigSetCommand( $this->configSetProcessorFactoryMock, $this->validatorMock, - $this->scopeMock + $this->scopeMock, + $this->pathValidatorFactoryMock ); } @@ -120,4 +145,28 @@ public function testExecuteNotValidScope() $this->assertSame(Cli::RETURN_FAILURE, $tester->getStatusCode()); } + + public function testExecuteWithException() + { + $this->validatorMock->expects($this->once()) + ->method('isValid') + ->willReturn(true); + $this->pathValidator->expects($this->once()) + ->method('validate') + ->willThrowException(new ValidatorException(__('The "test/test/test" path does not exists'))); + $this->configSetProcessorFactoryMock->expects($this->never()) + ->method('create'); + + $tester = new CommandTester($this->command); + $tester->execute([ + ConfigSetCommand::ARG_PATH => 'test/test/test', + ConfigSetCommand::ARG_VALUE => 'value' + ]); + + $this->assertContains( + __('The "test/test/test" path does not exists')->render(), + $tester->getDisplay() + ); + $this->assertSame(Cli::RETURN_FAILURE, $tester->getStatusCode()); + } } diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShow/ValueProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShow/ValueProcessorTest.php index 85eb78ba7380e..1aa8092390caa 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShow/ValueProcessorTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShow/ValueProcessorTest.php @@ -13,7 +13,13 @@ use Magento\Framework\Config\ScopeInterface; use Magento\Framework\App\Area; use Magento\Config\Console\Command\ConfigShow\ValueProcessor; +use Magento\Config\Model\Config\Backend\Encrypted; +/** + * Test for ValueProcessor. + * + * @see ValueProcessor + */ class ValueProcessorTest extends \PHPUnit_Framework_TestCase { /** @@ -59,13 +65,34 @@ protected function setUp() * @param bool $hasBackendModel * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectsGetBackendModel * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectsCreate + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectsGetValue + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectsSetPath + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectsSetScope + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectsSetScopeId + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectsSetValue + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectsAfterLoad + * @param string $expectsValue + * @param string $className + * @param string $value * @dataProvider processDataProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ - public function testProcess($hasBackendModel, $expectsGetBackendModel, $expectsCreate) - { + public function testProcess( + $hasBackendModel, + $expectsGetBackendModel, + $expectsCreate, + $expectsGetValue, + $expectsSetPath, + $expectsSetScope, + $expectsSetScopeId, + $expectsSetValue, + $expectsAfterLoad, + $expectsValue, + $className, + $value + ) { $scope = 'someScope'; $scopeCode = 'someScopeCode'; - $value = 'someValue'; $path = 'some/config/path'; $oldConfigScope = 'oldConfigScope'; @@ -87,31 +114,31 @@ public function testProcess($hasBackendModel, $expectsGetBackendModel, $expectsC ->method('create') ->willReturn($structureMock); - /** @var Value|\PHPUnit_Framework_MockObject_MockObject $valueMock */ - $backendModelMock = $this->getMockBuilder(Value::class) + /** @var Value|Encrypted|\PHPUnit_Framework_MockObject_MockObject $valueMock */ + $backendModelMock = $this->getMockBuilder($className) ->disableOriginalConstructor() ->setMethods(['setPath', 'setScope', 'setScopeId', 'setValue', 'getValue', 'afterLoad']) ->getMock(); - $backendModelMock->expects($this->once()) + $backendModelMock->expects($expectsSetPath) ->method('setPath') ->with($path) ->willReturnSelf(); - $backendModelMock->expects($this->once()) + $backendModelMock->expects($expectsSetScope) ->method('setScope') ->with($scope) ->willReturnSelf(); - $backendModelMock->expects($this->once()) + $backendModelMock->expects($expectsSetScopeId) ->method('setScopeId') ->with($scopeCode) ->willReturnSelf(); - $backendModelMock->expects($this->once()) + $backendModelMock->expects($expectsSetValue) ->method('setValue') ->with($value) ->willReturnSelf(); - $backendModelMock->expects($this->once()) + $backendModelMock->expects($expectsAfterLoad) ->method('afterLoad') ->willReturnSelf(); - $backendModelMock->expects($this->once()) + $backendModelMock->expects($expectsGetValue) ->method('getValue') ->willReturn($value); @@ -134,17 +161,86 @@ public function testProcess($hasBackendModel, $expectsGetBackendModel, $expectsC ->with($path) ->willReturn($fieldMock); - $this->assertSame($value, $this->valueProcessor->process($scope, $scopeCode, $value, $path)); + $this->assertSame($expectsValue, $this->valueProcessor->process($scope, $scopeCode, $value, $path)); } /** * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function processDataProvider() { return [ - ['hasBackendModel' => true, 'expectsGetBackendModel' => $this->once(), 'expectsCreate' => $this->never()], - ['hasBackendModel' => false, 'expectsGetBackendModel' => $this->never(), 'expectsCreate' => $this->once()], + [ + 'hasBackendModel' => true, + 'expectsGetBackendModel' => $this->once(), + 'expectsCreate' => $this->never(), + 'expectsGetValue' => $this->once(), + 'expectsSetPath' => $this->once(), + 'expectsSetScope' => $this->once(), + 'expectsSetScopeId' => $this->once(), + 'expectsSetValue' => $this->once(), + 'expectsAfterLoad' => $this->once(), + 'expectsValue' => 'someValue', + 'className' => Value::class, + 'value' => 'someValue' + ], + [ + 'hasBackendModel' => false, + 'expectsGetBackendModel' => $this->never(), + 'expectsCreate' => $this->once(), + 'expectsGetValue' => $this->once(), + 'expectsSetPath' => $this->once(), + 'expectsSetScope' => $this->once(), + 'expectsSetScopeId' => $this->once(), + 'expectsSetValue' => $this->once(), + 'expectsAfterLoad' => $this->once(), + 'expectsValue' => 'someValue', + 'className' => Value::class, + 'value' => 'someValue' + ], + [ + 'hasBackendModel' => true, + 'expectsGetBackendModel' => $this->once(), + 'expectsCreate' => $this->never(), + 'expectsGetValue' => $this->never(), + 'expectsSetPath' => $this->never(), + 'expectsSetScope' => $this->never(), + 'expectsSetScopeId' => $this->never(), + 'expectsSetValue' => $this->never(), + 'expectsAfterLoad' => $this->never(), + 'expectsValue' => ValueProcessor::SAFE_PLACEHOLDER, + 'className' => Encrypted::class, + 'value' => 'someValue' + ], + [ + 'hasBackendModel' => true, + 'expectsGetBackendModel' => $this->once(), + 'expectsCreate' => $this->never(), + 'expectsGetValue' => $this->once(), + 'expectsSetPath' => $this->once(), + 'expectsSetScope' => $this->once(), + 'expectsSetScopeId' => $this->once(), + 'expectsSetValue' => $this->once(), + 'expectsAfterLoad' => $this->once(), + 'expectsValue' => null, + 'className' => Value::class, + 'value' => null + ], + [ + 'hasBackendModel' => true, + 'expectsGetBackendModel' => $this->once(), + 'expectsCreate' => $this->never(), + 'expectsGetValue' => $this->never(), + 'expectsSetPath' => $this->never(), + 'expectsSetScope' => $this->never(), + 'expectsSetScopeId' => $this->never(), + 'expectsSetValue' => $this->never(), + 'expectsAfterLoad' => $this->never(), + 'expectsValue' => null, + 'className' => Encrypted::class, + 'value' => null + ], ]; } } diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/PathValidatorTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/PathValidatorTest.php new file mode 100644 index 0000000000000..e6e5e74047440 --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Model/Config/PathValidatorTest.php @@ -0,0 +1,68 @@ +structureMock = $this->getMockBuilder(Structure::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new PathValidator( + $this->structureMock + ); + } + + public function testValidate() + { + $this->structureMock->expects($this->once()) + ->method('getFieldPaths') + ->willReturn([ + 'test/test/test' => [ + 'test/test/test' + ] + ]); + + $this->assertTrue($this->model->validate('test/test/test')); + } + + /** + * @expectedException \Magento\Framework\Exception\ValidatorException + * @expectedExceptionMessage The "test/test/test" path does not exist + */ + public function testValidateWithException() + { + $this->structureMock->expects($this->once()) + ->method('getFieldPaths') + ->willReturn([]); + + $this->assertTrue($this->model->validate('test/test/test')); + } +} diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php index 2f7c2a1e659b4..6442a49dc181e 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php @@ -4,34 +4,44 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Test\Unit\Model\Config; +use Magento\Config\Model\Config\ScopeDefiner; +use Magento\Config\Model\Config\Structure; +use Magento\Config\Model\Config\Structure\Data; +use Magento\Config\Model\Config\Structure\Element\FlyweightFactory; +use Magento\Config\Model\Config\Structure\Element\Iterator\Tab as TabIterator; +use PHPUnit_Framework_MockObject_MockObject as Mock; + +/** + * Test for Structure. + * + * @see Structure + */ class StructureTest extends \PHPUnit_Framework_TestCase { /** - * @var \Magento\Config\Model\Config\Structure + * @var Structure|Mock */ protected $_model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var FlyweightFactory|Mock */ protected $_flyweightFactory; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var TabIterator|Mock */ protected $_tabIteratorMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Data|Mock */ protected $_structureDataMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ScopeDefiner|Mock */ protected $_scopeDefinerMock; @@ -42,46 +52,29 @@ class StructureTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->_flyweightFactory = $this->getMock( - \Magento\Config\Model\Config\Structure\Element\FlyweightFactory::class, - [], - [], - '', - false - ); - $this->_tabIteratorMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Element\Iterator\Tab::class, - [], - [], - '', - false - ); - $this->_structureDataMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Data::class, - [], - [], - '', - false - ); - $this->_scopeDefinerMock = $this->getMock( - \Magento\Config\Model\Config\ScopeDefiner::class, - [], - [], - '', - false - ); - $this->_scopeDefinerMock->expects($this->any())->method('getScope')->will($this->returnValue('scope')); - - $filePath = dirname(__DIR__) . '/_files'; - $this->_structureData = require $filePath . '/converted_config.php'; - $this->_structureDataMock->expects( - $this->once() - )->method( - 'get' - )->will( - $this->returnValue($this->_structureData['config']['system']) - ); - $this->_model = new \Magento\Config\Model\Config\Structure( + $this->_flyweightFactory = $this->getMockBuilder(FlyweightFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->_tabIteratorMock = $this->getMockBuilder(TabIterator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->_structureDataMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->_scopeDefinerMock = $this->getMockBuilder(ScopeDefiner::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->_structureData = require dirname(__DIR__) . '/_files/converted_config.php'; + + $this->_scopeDefinerMock->expects($this->any()) + ->method('getScope') + ->willReturn('scope'); + $this->_structureDataMock->expects($this->once()) + ->method('get') + ->willReturn($this->_structureData['config']['system']); + + $this->_model = new Structure( $this->_structureDataMock, $this->_tabIteratorMock, $this->_flyweightFactory, @@ -89,60 +82,47 @@ protected function setUp() ); } - protected function tearDown() - { - unset($this->_flyweightFactory); - unset($this->_scopeDefinerMock); - unset($this->_structureData); - unset($this->_tabIteratorMock); - unset($this->_structureDataMock); - unset($this->_model); - } - public function testGetTabsBuildsSectionTree() { - $this->_structureDataMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Data::class, - [], - [], - '', - false - ); - $this->_structureDataMock->expects( - $this->any() - )->method( - 'get' - )->will( - $this->returnValue( - ['sections' => ['section1' => ['tab' => 'tab1']], 'tabs' => ['tab1' => []]] - ) - ); $expected = ['tab1' => ['children' => ['section1' => ['tab' => 'tab1']]]]; + + $this->_structureDataMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->_structureDataMock->expects($this->any()) + ->method('get') + ->willReturn( + ['sections' => ['section1' => ['tab' => 'tab1']], 'tabs' => ['tab1' => []]] + ); + $this->_tabIteratorMock->expects($this->once()) + ->method('setElements') + ->with($expected); + $model = new \Magento\Config\Model\Config\Structure( $this->_structureDataMock, $this->_tabIteratorMock, $this->_flyweightFactory, $this->_scopeDefinerMock ); - $this->_tabIteratorMock->expects($this->once())->method('setElements')->with($expected); + $this->assertEquals($this->_tabIteratorMock, $model->getTabs()); } public function testGetSectionList() { - $this->_structureDataMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Data::class, - [], - [], - '', - false - ); - $this->_structureDataMock->expects( - $this->any() - )->method( - 'get' - )->will( - $this->returnValue( + $expected = [ + 'section1_child_id_1' => true, + 'section1_child_id_2' => true, + 'section1_child_id_3' => true, + 'section2_child_id_1' => true + ]; + + $this->_structureDataMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->_structureDataMock->expects($this->any()) + ->method('get') + ->willReturn( [ 'sections' => [ 'section1' => [ @@ -159,14 +139,8 @@ public function testGetSectionList() ], ] ] - ) - ); - $expected = [ - 'section1_child_id_1' => true, - 'section1_child_id_2' => true, - 'section1_child_id_3' => true, - 'section2_child_id_1' => true - ]; + ); + $model = new \Magento\Config\Model\Config\Structure( $this->_structureDataMock, $this->_tabIteratorMock, @@ -191,17 +165,17 @@ public function testGetElementReturnsEmptyElementIfNotExistingElementIsRequested $expectedPath ) { $expectedConfig = ['id' => $expectedId, 'path' => $expectedPath, '_elementType' => $expectedType]; - $elementMock = $this->getMock(\Magento\Config\Model\Config\Structure\ElementInterface::class); - $elementMock->expects($this->once())->method('setData')->with($expectedConfig); - $this->_flyweightFactory->expects( - $this->once() - )->method( - 'create' - )->with( - $expectedType - )->will( - $this->returnValue($elementMock) - ); + + $elementMock = $this->getMockBuilder(Structure\ElementInterface::class) + ->getMockForAbstractClass(); + $elementMock->expects($this->once()) + ->method('setData') + ->with($expectedConfig); + $this->_flyweightFactory->expects($this->once()) + ->method('create') + ->with($expectedType) + ->willReturn($elementMock); + $this->assertEquals($elementMock, $this->_model->getElement($path)); } @@ -217,121 +191,102 @@ public function emptyElementDataProvider() public function testGetElementReturnsProperElementByPath() { - $elementMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Element\Field::class, - [], - [], - '', - false - ); $section = $this->_structureData['config']['system']['sections']['section_1']; $fieldData = $section['children']['group_level_1']['children']['field_3']; - $elementMock->expects($this->once())->method('setData')->with($fieldData, 'scope'); - - $this->_flyweightFactory->expects( - $this->once() - )->method( - 'create' - )->with( - 'field' - )->will( - $this->returnValue($elementMock) - ); + + $elementMock = $this->getMockBuilder(Structure\Element\Field::class) + ->disableOriginalConstructor() + ->getMock(); + + $elementMock->expects($this->once()) + ->method('setData') + ->with($fieldData, 'scope'); + $this->_flyweightFactory->expects($this->once()) + ->method('create') + ->with('field') + ->willReturn($elementMock); + $this->assertEquals($elementMock, $this->_model->getElement('section_1/group_level_1/field_3')); } public function testGetElementByPathPartsIfSectionDataIsEmpty() { - $elementMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Element\Field::class, - [], - [], - '', - false - ); $fieldData = [ 'id' => 'field_3', 'path' => 'section_1/group_level_1', '_elementType' => 'field', ]; - $elementMock->expects($this->once())->method('setData')->with($fieldData, 'scope'); - - $this->_flyweightFactory->expects( - $this->once() - )->method( - 'create' - )->with( - 'field' - )->will( - $this->returnValue($elementMock) - ); + $pathParts = explode('/', 'section_1/group_level_1/field_3'); - $structureDataMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Data::class, - [], - [], - '', - false - ); + $elementMock = $this->getMockBuilder(Structure\Element\Field::class) + ->disableOriginalConstructor() + ->getMock(); + $structureDataMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); - $structureDataMock->expects( - $this->once() - )->method( - 'get' - )->will( - $this->returnValue([]) - ); + $elementMock->expects($this->once()) + ->method('setData') + ->with($fieldData, 'scope'); + $this->_flyweightFactory->expects($this->once()) + ->method('create') + ->with('field') + ->willReturn($elementMock); + $structureDataMock->expects($this->once()) + ->method('get') + ->willReturn([]); - $structureMock = new \Magento\Config\Model\Config\Structure( + $structureMock = new Structure( $structureDataMock, $this->_tabIteratorMock, $this->_flyweightFactory, $this->_scopeDefinerMock ); - $pathParts = explode('/', 'section_1/group_level_1/field_3'); $this->assertEquals($elementMock, $structureMock->getElementByPathParts($pathParts)); } public function testGetFirstSectionReturnsFirstAllowedSection() { - $tabMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Element\Tab::class, - ['current', 'getChildren', 'rewind'], - [], - '', - false - ); - $tabMock->expects($this->any())->method('getChildren')->will($this->returnSelf()); - $tabMock->expects($this->once())->method('rewind'); - $tabMock->expects($this->once())->method('current')->will($this->returnValue('currentSection')); - $this->_tabIteratorMock->expects($this->once())->method('rewind'); - $this->_tabIteratorMock->expects($this->once())->method('current')->will($this->returnValue($tabMock)); + $tabMock = $this->getMockBuilder(Structure\Element\Tab::class) + ->disableOriginalConstructor() + ->setMethods(['current', 'getChildren', 'rewind']) + ->getMock(); + + $tabMock->expects($this->any()) + ->method('getChildren') + ->willReturnSelf(); + $tabMock->expects($this->once()) + ->method('rewind'); + $tabMock->expects($this->once()) + ->method('current') + ->willReturn('currentSection'); + $this->_tabIteratorMock->expects($this->once()) + ->method('rewind'); + $this->_tabIteratorMock->expects($this->once()) + ->method('current') + ->willReturn($tabMock); + $this->assertEquals('currentSection', $this->_model->getFirstSection()); } public function testGetElementReturnsProperElementByPathCachesObject() { - $elementMock = $this->getMock( - \Magento\Config\Model\Config\Structure\Element\Field::class, - [], - [], - '', - false - ); $section = $this->_structureData['config']['system']['sections']['section_1']; $fieldData = $section['children']['group_level_1']['children']['field_3']; - $elementMock->expects($this->once())->method('setData')->with($fieldData, 'scope'); - - $this->_flyweightFactory->expects( - $this->once() - )->method( - 'create' - )->with( - 'field' - )->will( - $this->returnValue($elementMock) - ); + + $elementMock = $this->getMockBuilder(Structure\Element\Field::class) + ->disableOriginalConstructor() + ->getMock(); + + $elementMock->expects($this->once()) + ->method('setData') + ->with($fieldData, 'scope'); + $this->_flyweightFactory->expects($this->once()) + ->method('create') + ->with('field') + ->willReturn($elementMock); + $this->assertEquals($elementMock, $this->_model->getElement('section_1/group_level_1/field_3')); $this->assertEquals($elementMock, $this->_model->getElement('section_1/group_level_1/field_3')); } @@ -350,12 +305,45 @@ public function testGetFieldPathsByAttribute($attributeName, $attributeValue, $p public function getFieldPathsByAttributeDataProvider() { return [ - ['backend_model', \Magento\Config\Model\Config\Backend\Encrypted::class, [ - 'section_1/group_1/field_2', - 'section_1/group_level_1/group_level_2/group_level_3/field_3_1_1', - 'section_2/group_3/field_4', - ]], + [ + 'backend_model', + \Magento\Config\Model\Config\Backend\Encrypted::class, + [ + 'section_1/group_1/field_2', + 'section_1/group_level_1/group_level_2/group_level_3/field_3_1_1', + 'section_2/group_3/field_4', + ] + ], ['attribute_2', 'test_value_2', ['section_2/group_3/field_4']] ]; } + + public function testGetFieldPaths() + { + $expected = [ + 'section/group/field2' => [ + 'field_2' + ], + 'field_3' => [ + 'field_3' + ], + 'field_3_1' => [ + 'field_3_1' + ], + 'field_3_1_1' => [ + 'field_3_1_1' + ], + 'section/group/field4' => [ + 'field_4', + ], + 'field_5' => [ + 'field_5', + ], + ]; + + $this->assertSame( + $expected, + $this->_model->getFieldPaths() + ); + } } diff --git a/app/code/Magento/Config/Test/Unit/Model/_files/converted_config.php b/app/code/Magento/Config/Test/Unit/Model/_files/converted_config.php index fb14e864b4fb0..e94e5bd881240 100644 --- a/app/code/Magento/Config/Test/Unit/Model/_files/converted_config.php +++ b/app/code/Magento/Config/Test/Unit/Model/_files/converted_config.php @@ -29,6 +29,7 @@ 'children' => [ 'field_2' => [ 'id' => 'field_2', + 'config_path' => 'section/group/field2', 'translate' => 'label', 'showInWebsite' => '1', 'type' => 'text', @@ -133,6 +134,7 @@ ], 'field_4' => [ 'id' => 'field_4', + 'config_path' => 'section/group/field4', 'translate' => 'label', 'showInWebsite' => '1', 'type' => 'text', diff --git a/app/code/Magento/Config/Test/Unit/Model/_files/system_2.xml b/app/code/Magento/Config/Test/Unit/Model/_files/system_2.xml index c532d3b784fe9..e04890c2c14e9 100644 --- a/app/code/Magento/Config/Test/Unit/Model/_files/system_2.xml +++ b/app/code/Magento/Config/Test/Unit/Model/_files/system_2.xml @@ -18,6 +18,7 @@ Magento\Config\Model\Config\Backend\Encrypted + section/group/field2 @@ -70,7 +71,9 @@ 0 + + section/group/field4 diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index 3a716d50dbaa0..de1dcbdb4c39b 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -145,7 +145,6 @@ Magento\Framework\App\DeploymentConfig\Reader Magento\Config\App\Config\Type\System::CONFIG_TYPE - Magento\Framework\Config\File\ConfigFilePool::APP_CONFIG @@ -201,7 +200,7 @@ systemConfigInitialDataProvider 1000 - + Magento\Config\App\Config\Source\EnvironmentConfigSource 2000 diff --git a/app/code/Magento/Deploy/Console/Command/App/ApplicationDumpCommand.php b/app/code/Magento/Deploy/Console/Command/App/ApplicationDumpCommand.php index b63357be6bf8a..e2413f8061680 100644 --- a/app/code/Magento/Deploy/Console/Command/App/ApplicationDumpCommand.php +++ b/app/code/Magento/Deploy/Console/Command/App/ApplicationDumpCommand.php @@ -12,6 +12,8 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Magento\Deploy\Model\DeploymentConfig\Hash; +use Magento\Framework\App\ObjectManager; /** * Command for dump application state @@ -28,19 +30,27 @@ class ApplicationDumpCommand extends Command */ private $sources; + /** + * @var Hash + */ + private $configHash; + /** * ApplicationDumpCommand constructor. * * @param Writer $writer * @param array $sources + * @param Hash $configHash */ public function __construct( Writer $writer, - array $sources + array $sources, + Hash $configHash = null ) { parent::__construct(); $this->writer = $writer; $this->sources = $sources; + $this->configHash = $configHash ?: ObjectManager::getInstance()->get(Hash::class); } /** @@ -76,16 +86,19 @@ protected function execute(InputInterface $input, OutputInterface $output) : $sourceData['comment']->get(); } } - $this->writer - ->saveConfig( - [ConfigFilePool::APP_CONFIG => $dump], - true, - ConfigFilePool::LOCAL, - $comments - ); + $this->writer->saveConfig( + [ConfigFilePool::APP_CONFIG => $dump], + true, + null, + $comments + ); if (!empty($comments)) { $output->writeln($comments); } + + // Generate and save new hash of deployment configuration. + $this->configHash->regenerate(); + $output->writeln('Done.'); return Cli::RETURN_SUCCESS; } diff --git a/app/code/Magento/Deploy/Console/Command/App/ConfigImport/Importer.php b/app/code/Magento/Deploy/Console/Command/App/ConfigImport/Importer.php new file mode 100644 index 0000000000000..c8802224aa874 --- /dev/null +++ b/app/code/Magento/Deploy/Console/Command/App/ConfigImport/Importer.php @@ -0,0 +1,111 @@ +configValidator = $configValidator; + $this->configImporterPool = $configImporterPool; + $this->deploymentConfig = $deploymentConfig; + $this->configHash = $configHash; + $this->logger = $logger; + } + + /** + * Runs importing of config data from deployment configuration files. + * + * @param OutputInterface $output the CLI output + * @return void + * @throws LocalizedException + */ + public function import(OutputInterface $output) + { + $output->writeln('Start import:'); + + try { + $importers = $this->configImporterPool->getImporters(); + + if (!$importers || $this->configValidator->isValid()) { + $output->writeln('Nothing to import'); + } else { + /** + * @var string $namespace + * @var ImporterInterface $importer + */ + foreach ($importers as $namespace => $importer) { + $messages = $importer->import($this->deploymentConfig->getConfigData($namespace)); + $output->writeln($messages); + } + + $this->configHash->regenerate(); + } + } catch (LocalizedException $exception) { + $this->logger->error($exception); + throw new LocalizedException(__('Import is failed'), $exception); + } + } +} diff --git a/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php b/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php new file mode 100644 index 0000000000000..809211c2314b8 --- /dev/null +++ b/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php @@ -0,0 +1,71 @@ +importer = $importer; + + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName(self::COMMAND_NAME) + ->setDescription('Import data from shared configuration files to appropriate data storage'); + + parent::configure(); + } + + /** + * Imports data from deployment configuration files to the DB. + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + try { + $this->importer->import($output); + } catch (LocalizedException $e) { + $output->writeln('' . $e->getMessage() . ''); + + return Cli::RETURN_FAILURE; + } + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommand.php b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommand.php index e8555ceb9530f..a1da0db311966 100644 --- a/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommand.php +++ b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommand.php @@ -163,7 +163,7 @@ private function writeSuccessMessage(OutputInterface $output, $isInteractive) $output->writeln(sprintf( 'Configuration value%s saved in app/etc/%s', $isInteractive ? 's' : '', - $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG) + $this->configFilePool->getPath(ConfigFilePool::APP_ENV) )); } @@ -175,7 +175,7 @@ private function writeSuccessMessage(OutputInterface $output, $isInteractive) */ private function getConfigPaths() { - $configFilePath = $this->configFilePool->getPathsByPool(ConfigFilePool::LOCAL)[ConfigFilePool::APP_CONFIG]; + $configFilePath = $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG); try { $configPaths = $this->commentParser->execute($configFilePath); } catch (FileSystemException $e) { diff --git a/app/code/Magento/Deploy/Model/ConfigWriter.php b/app/code/Magento/Deploy/Model/ConfigWriter.php index 7a9b70c528ac9..8713442c9b0b1 100644 --- a/app/code/Magento/Deploy/Model/ConfigWriter.php +++ b/app/code/Magento/Deploy/Model/ConfigWriter.php @@ -55,10 +55,9 @@ public function save(array $values, $scope = ScopeConfigInterface::SCOPE_TYPE_DE $config = $this->setConfig($config, $fullConfigPath, $configValue); } - $this->writer - ->saveConfig( - [ConfigFilePool::APP_CONFIG => $config] - ); + $this->writer->saveConfig( + [ConfigFilePool::APP_ENV => $config] + ); } /** diff --git a/app/code/Magento/Deploy/Model/DeploymentConfig/DataCollector.php b/app/code/Magento/Deploy/Model/DeploymentConfig/DataCollector.php new file mode 100644 index 0000000000000..63fb770d468aa --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeploymentConfig/DataCollector.php @@ -0,0 +1,95 @@ + + * + * + * Magento\Store\Model\StoreImporter + * + * + * + * ``` + * Example, how sections are stored with their config data in configuration files: + * ```php + * [ + * 'scopes' => [...], + * 'system' => [...], + * 'themes' => [...], + * ... + * ] + * ``` + * + * In here we define section "scopes" and its importer Magento\Store\Model\StoreImporter. + * The data of this section will be collected then will be used in importing process from the shared configuration + * files to appropriate application sources. + * + * @see \Magento\Deploy\Console\Command\App\ConfigImport\Importer::import() + * @see \Magento\Deploy\Model\DeploymentConfig\Hash::regenerate() + */ +class DataCollector +{ + /** + * Pool of all deployment configuration importers. + * + * @var ImporterPool + */ + private $configImporterPool; + + /** + * Application deployment configuration. + * + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @param ImporterPool $configImporterPool the pool of all deployment configuration importers + * @param DeploymentConfig $deploymentConfig the application deployment configuration + */ + public function __construct(ImporterPool $configImporterPool, DeploymentConfig $deploymentConfig) + { + $this->configImporterPool = $configImporterPool; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * Retrieves configuration data of specific section from deployment configuration files. + * + * E.g. + * ```php + * [ + * 'scopes' => [...], + * 'system' => [...], + * 'themes' => [...], + * ... + * ] + * ``` + * In this example key of the array is the section name, value of the array is configuration data of the section. + * + * @return array + */ + public function getConfig() + { + $result = []; + + foreach ($this->configImporterPool->getSections() as $section) { + $data = $this->deploymentConfig->getConfigData($section); + if ($data) { + $result[$section] = $data; + } + } + + return $result; + } +} diff --git a/app/code/Magento/Deploy/Model/DeploymentConfig/Hash.php b/app/code/Magento/Deploy/Model/DeploymentConfig/Hash.php new file mode 100644 index 0000000000000..6a904e9004a35 --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeploymentConfig/Hash.php @@ -0,0 +1,103 @@ +deploymentConfig = $deploymentConfig; + $this->writer = $writer; + $this->configHashGenerator = $configHashGenerator; + $this->dataConfigCollector = $dataConfigCollector; + } + + /** + * Updates hash in the storage. + * + * The hash is generated based on data from configuration files + * + * @return void + * @throws LocalizedException is thrown when hash was not saved + */ + public function regenerate() + { + try { + $config = $this->dataConfigCollector->getConfig(); + $hash = $this->configHashGenerator->generate($config); + $this->writer->saveConfig([ConfigFilePool::APP_ENV => [self::CONFIG_KEY => $hash]]); + } catch (FileSystemException $exception) { + throw new LocalizedException(__('Hash has not been saved'), $exception); + } + } + + /** + * Retrieves saved hash from storage. + * + * @return string|null + */ + public function get() + { + return $this->deploymentConfig->getConfigData(self::CONFIG_KEY); + } +} diff --git a/app/code/Magento/Deploy/Model/DeploymentConfig/Hash/Generator.php b/app/code/Magento/Deploy/Model/DeploymentConfig/Hash/Generator.php new file mode 100644 index 0000000000000..896460861ebd9 --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeploymentConfig/Hash/Generator.php @@ -0,0 +1,40 @@ +serializer = $serializer; + } + + /** + * Generates and retrieves hash of deployment configuration data. + * + * @param array|string $data the deployment configuration data from files + * @return string the hash + */ + public function generate($data) + { + return sha1($this->serializer->serialize($data)); + } +} diff --git a/app/code/Magento/Deploy/Model/DeploymentConfig/ImporterPool.php b/app/code/Magento/Deploy/Model/DeploymentConfig/ImporterPool.php new file mode 100644 index 0000000000000..b2b2fece8a192 --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeploymentConfig/ImporterPool.php @@ -0,0 +1,124 @@ + + * + * + * Magento\Store\Model\StoreImporter + * + * + * + * ``` + * + * The example of section in deployment configuration file: + * ```php + * [ + * 'scopes' => [ + * 'websites' => [ + * ... + * ], + * 'groups' => [ + * ... + * ], + * 'stores' => [ + * ... + * ], + * ... + * ] + * ] + * ``` + * + * @var array + */ + private $importers = []; + + /** + * Magento object manager. + * + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param ObjectManagerInterface $objectManager the Magento object manager + * @param array $importers the list of sections and their importers + */ + public function __construct(ObjectManagerInterface $objectManager, array $importers = []) + { + $this->objectManager = $objectManager; + $this->importers = $importers; + } + + /** + * Retrieves names of sections for configuration files whose data is read from these files for import + * to appropriate application sources. + * + * @return array the list of sections + * E.g. + * ```php + * [ + * 'scopes', + * 'themes', + * ... + * ] + * ``` + */ + public function getSections() + { + return array_keys($this->importers); + } + + /** + * Retrieves list of all sections with their importer instances. + * + * E.g. + * ```php + * [ + * 'scopes' => SomeScopeImporter(), + * ... + * ] + * ``` + * + * @return array the list of all sections with their importer instances + * @throws ConfigurationMismatchException is thrown when instance of importer implements a wrong interface + */ + public function getImporters() + { + $result = []; + + foreach ($this->importers as $section => $importer) { + $importerObj = $this->objectManager->get($importer); + if (!$importerObj instanceof ImporterInterface) { + throw new ConfigurationMismatchException(new Phrase( + '%1: Instance of %2 is expected, got %3 instead', + [$section, ImporterInterface::class, get_class($importerObj)] + )); + } + $result[$section] = $importerObj; + } + + return $result; + } +} diff --git a/app/code/Magento/Deploy/Model/DeploymentConfig/Validator.php b/app/code/Magento/Deploy/Model/DeploymentConfig/Validator.php new file mode 100644 index 0000000000000..7f6df6efdec19 --- /dev/null +++ b/app/code/Magento/Deploy/Model/DeploymentConfig/Validator.php @@ -0,0 +1,74 @@ +configHash = $configHash; + $this->hashGenerator = $hashGenerator; + $this->dataConfigCollector = $dataConfigCollector; + } + + /** + * Checks if config data in the deployment configuration files is valid. + * + * Checks if config data was changed based on its hash. + * If the new hash of config data and the saved hash are different returns false. + * If config data is empty always returns true. + * In the other cases returns true. + * + * @return bool + */ + public function isValid() + { + $config = $this->dataConfigCollector->getConfig(); + + if (!$config) { + return true; + } + + return $this->hashGenerator->generate($config) === $this->configHash->get(); + } +} diff --git a/app/code/Magento/Deploy/Model/Mode.php b/app/code/Magento/Deploy/Model/Mode.php index 8052a27e2dbca..ba56a1c9006bf 100644 --- a/app/code/Magento/Deploy/Model/Mode.php +++ b/app/code/Magento/Deploy/Model/Mode.php @@ -122,7 +122,7 @@ public function enableDeveloperMode() */ public function getMode() { - $env = $this->reader->load(ConfigFilePool::APP_ENV); + $env = $this->reader->load(); return isset($env[State::PARAM_MODE]) ? $env[State::PARAM_MODE] : null; } diff --git a/app/code/Magento/Deploy/Model/Plugin/ConfigValidator.php b/app/code/Magento/Deploy/Model/Plugin/ConfigValidator.php new file mode 100644 index 0000000000000..f5fc2bdc46dcb --- /dev/null +++ b/app/code/Magento/Deploy/Model/Plugin/ConfigValidator.php @@ -0,0 +1,57 @@ +configValidator = $configValidator; + } + + /** + * Performs check that config data from deployment configuration files is valid. + * + * @param FrontController $subject the object of controller is wrapped by this plugin + * @param RequestInterface $request the object that contains request params + * @return void + * @throws LocalizedException is thrown if config data from deployment configuration files is not valid + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeDispatch(FrontController $subject, RequestInterface $request) + { + if (!$this->configValidator->isValid()) { + throw new LocalizedException( + __( + 'A change in configuration has been detected.' + . ' Run app:config:import or setup:upgrade command to synchronize configuration.' + ) + ); + } + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImport/ImporterTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImport/ImporterTest.php new file mode 100644 index 0000000000000..58ccf22eda71e --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImport/ImporterTest.php @@ -0,0 +1,191 @@ +configValidatorMock = $this->getMockBuilder(Validator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configImporterPoolMock = $this->getMockBuilder(ImporterPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configHashMock = $this->getMockBuilder(Hash::class) + ->disableOriginalConstructor() + ->getMock(); + $this->loggerMock = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + $this->outputMock = $this->getMockBuilder(OutputInterface::class) + ->getMockForAbstractClass(); + + $this->importer = new Importer( + $this->configValidatorMock, + $this->configImporterPoolMock, + $this->deploymentConfigMock, + $this->configHashMock, + $this->loggerMock + ); + } + + /** + * @return void + */ + public function testImport() + { + $configData = ['some data']; + $messages = ['Import has done']; + $expectsMessages = ['Import has done']; + $importerMock = $this->getMockBuilder(ImporterInterface::class) + ->getMockForAbstractClass(); + $importers = ['someSection' => $importerMock]; + + $this->configImporterPoolMock->expects($this->once()) + ->method('getImporters') + ->willReturn($importers); + $this->configValidatorMock->expects($this->any()) + ->method('isValid') + ->willReturn(false); + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with('someSection') + ->willReturn($configData); + $importerMock->expects($this->once()) + ->method('import') + ->with($configData) + ->willReturn($messages); + $this->configHashMock->expects($this->once()) + ->method('regenerate'); + $this->loggerMock->expects($this->never()) + ->method('error'); + + $this->outputMock->expects($this->at(0)) + ->method('writeln') + ->with('Start import:'); + $this->outputMock->expects($this->at(1)) + ->method('writeln') + ->with($expectsMessages); + + $this->importer->import($this->outputMock); + } + + /** + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Import is failed + */ + public function testImportWithException() + { + $exception = new LocalizedException(__('Some error')); + $this->outputMock->expects($this->at(0)) + ->method('writeln') + ->with('Start import:'); + $this->configImporterPoolMock->expects($this->once()) + ->method('getImporters') + ->willThrowException($exception); + $this->loggerMock->expects($this->once()) + ->method('error') + ->with($exception); + + $this->importer->import($this->outputMock); + } + + /** + * @param array $importers + * @param bool $isValid + * @return void + * @dataProvider importNothingToImportDataProvider + */ + public function testImportNothingToImport(array $importers, $isValid) + { + $this->configImporterPoolMock->expects($this->once()) + ->method('getImporters') + ->willReturn($importers); + $this->configValidatorMock->expects($this->any()) + ->method('isValid') + ->willReturn($isValid); + $this->deploymentConfigMock->expects($this->never()) + ->method('getConfigData'); + $this->configHashMock->expects($this->never()) + ->method('regenerate'); + $this->loggerMock->expects($this->never()) + ->method('error'); + + $this->outputMock->expects($this->at(0)) + ->method('writeln') + ->with('Start import:'); + $this->outputMock->expects($this->at(1)) + ->method('writeln') + ->with('Nothing to import'); + + $this->importer->import($this->outputMock); + } + + /** + * @return array + */ + public function importNothingToImportDataProvider() + { + return [ + ['importers' => [], 'isValid' => true], + ['importers' => [], 'isValid' => false], + ['importers' => ['someImporter'], 'isValid' => true], + ]; + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php new file mode 100644 index 0000000000000..f684615b1fb9a --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php @@ -0,0 +1,65 @@ +importerMock = $this->getMockBuilder(Importer::class) + ->disableOriginalConstructor() + ->getMock(); + + $configImportCommand = new ConfigImportCommand( + $this->importerMock + ); + + $this->commandTester = new CommandTester($configImportCommand); + } + + /** + * @return void + */ + public function testExecute() + { + $this->importerMock->expects($this->once()) + ->method('import'); + + $this->assertSame(Cli::RETURN_SUCCESS, $this->commandTester->execute([])); + } + + /** + * @return void + */ + public function testExecuteWithException() + { + $this->importerMock->expects($this->once()) + ->method('import') + ->willThrowException(new LocalizedException(__('Some error'))); + + $this->assertSame(Cli::RETURN_FAILURE, $this->commandTester->execute([])); + $this->assertContains('Some error', $this->commandTester->getDisplay()); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSetCommandTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSetCommandTest.php index 5a9b82a52d55b..215d3061d445d 100644 --- a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSetCommandTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSetCommandTest.php @@ -86,11 +86,9 @@ public function setUp() public function testConfigFileNotExist() { $this->configFilePoolMock->expects($this->once()) - ->method('getPathsByPool') - ->with(ConfigFilePool::LOCAL) - ->willReturn([ - ConfigFilePool::APP_CONFIG => 'config.local.php' - ]); + ->method('getPath') + ->with(ConfigFilePool::APP_CONFIG) + ->willReturn('config.php'); $this->scopeValidatorMock->expects($this->once()) ->method('isValid') ->with('default', '') @@ -110,7 +108,7 @@ public function testConfigFileNotExist() $tester->getStatusCode() ); $this->assertContains( - 'File app/etc/config.local.php can\'t be read. ' + 'File app/etc/config.php can\'t be read. ' . 'Please check if it exists and has read permissions.', $tester->getDisplay() ); diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/ApplicationDumpCommandTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/ApplicationDumpCommandTest.php index c12c857c6c2c9..99a50d7227dc4 100644 --- a/app/code/Magento/Deploy/Test/Unit/Console/Command/ApplicationDumpCommandTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/ApplicationDumpCommandTest.php @@ -12,6 +12,7 @@ use Magento\Framework\Console\Cli; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Magento\Deploy\Model\DeploymentConfig\Hash; /** * Test command for dump application state @@ -38,6 +39,11 @@ class ApplicationDumpCommandTest extends \PHPUnit_Framework_TestCase */ private $source; + /** + * @var Hash|\PHPUnit_Framework_MockObject_MockObject + */ + private $configHashMock; + /** * @var ApplicationDumpCommand */ @@ -45,6 +51,9 @@ class ApplicationDumpCommandTest extends \PHPUnit_Framework_TestCase public function setUp() { + $this->configHashMock = $this->getMockBuilder(Hash::class) + ->disableOriginalConstructor() + ->getMock(); $this->input = $this->getMockBuilder(InputInterface::class) ->getMockForAbstractClass(); $this->output = $this->getMockBuilder(OutputInterface::class) @@ -56,10 +65,11 @@ public function setUp() ->disableOriginalConstructor() ->getMock(); - $this->command = new ApplicationDumpCommand($this->writer, [[ - 'namespace' => 'system', - 'source' => $this->source - ]]); + $this->command = new ApplicationDumpCommand( + $this->writer, + [['namespace' => 'system', 'source' => $this->source]], + $this->configHashMock + ); } public function testExport() @@ -68,6 +78,8 @@ public function testExport() 'system' => ['systemDATA'] ]; $data = [ConfigFilePool::APP_CONFIG => $dump]; + $this->configHashMock->expects($this->once()) + ->method('regenerate'); $this->source ->expects($this->once()) ->method('get') diff --git a/app/code/Magento/Deploy/Test/Unit/Model/ConfigWriterTest.php b/app/code/Magento/Deploy/Test/Unit/Model/ConfigWriterTest.php index 8dfec06a1c2ae..8764502d80744 100644 --- a/app/code/Magento/Deploy/Test/Unit/Model/ConfigWriterTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Model/ConfigWriterTest.php @@ -65,7 +65,7 @@ public function testSave() ->willReturn($config); $this->writerMock->expects($this->once()) ->method('saveConfig') - ->with([ConfigFilePool::APP_CONFIG => $config]); + ->with([ConfigFilePool::APP_ENV => $config]); $this->model->save($values, 'scope', 'scope_code'); } @@ -89,7 +89,7 @@ public function testSaveDefaultScope() ->willReturn($config); $this->writerMock->expects($this->once()) ->method('saveConfig') - ->with([ConfigFilePool::APP_CONFIG => $config]); + ->with([ConfigFilePool::APP_ENV => $config]); $this->model->save($values); } diff --git a/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/DataCollectorTest.php b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/DataCollectorTest.php new file mode 100644 index 0000000000000..484c7c6407a85 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/DataCollectorTest.php @@ -0,0 +1,59 @@ +configImporterPoolMock = $this->getMockBuilder(ImporterPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->dataCollector = new DataCollector($this->configImporterPoolMock, $this->deploymentConfigMock); + } + + /** + * @return void + */ + public function testGetConfig() + { + $sections = ['first', 'second']; + $this->configImporterPoolMock->expects($this->once()) + ->method('getSections') + ->willReturn($sections); + $this->deploymentConfigMock->expects($this->any()) + ->method('getConfigData') + ->willReturnMap([['first', 'some data']]); + + $this->assertSame(['first' => 'some data'], $this->dataCollector->getConfig()); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/Hash/GeneratorTest.php b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/Hash/GeneratorTest.php new file mode 100644 index 0000000000000..350426825a682 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/Hash/GeneratorTest.php @@ -0,0 +1,50 @@ +serializerMock = $this->getMockBuilder(SerializerInterface::class) + ->getMockForAbstractClass(); + + $this->generator = new Generator($this->serializerMock); + } + + /** + * @return void + */ + public function testGenerate() + { + $data = 'some config'; + $serializedData = 'serialized content'; + $hash = '40c185113eb5154ad9aa5a8854f197c818e17f62'; + + $this->serializerMock->expects($this->once()) + ->method('serialize') + ->with($data) + ->willReturn($serializedData); + + $this->assertSame($hash, $this->generator->generate($data)); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/HashTest.php b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/HashTest.php new file mode 100644 index 0000000000000..74956f98aa817 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/HashTest.php @@ -0,0 +1,133 @@ +deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->writerMock = $this->getMockBuilder(Writer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configHashGeneratorMock = $this->getMockBuilder(Generator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dataConfigCollectorMock = $this->getMockBuilder(DataCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->hash = new Hash( + $this->deploymentConfigMock, + $this->writerMock, + $this->configHashGeneratorMock, + $this->dataConfigCollectorMock + ); + } + + /** + * @return void + */ + public function testGet() + { + $result = 'some data'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Hash::CONFIG_KEY) + ->willReturn($result); + + $this->assertSame($result, $this->hash->get()); + } + + /** + * @return void + */ + public function testRegenerate() + { + $config = 'some config'; + $hash = 'some hash'; + + $this->generalRegenerateMocks($config, $hash); + $this->writerMock->expects($this->once()) + ->method('saveConfig') + ->with([ConfigFilePool::APP_ENV => [Hash::CONFIG_KEY => $hash]]); + + $this->hash->regenerate(); + } + + /** + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Hash has not been saved + */ + public function testRegenerateWithException() + { + $config = 'some config'; + $hash = 'some hash'; + + $this->generalRegenerateMocks($config, $hash); + $this->writerMock->expects($this->once()) + ->method('saveConfig') + ->with([ConfigFilePool::APP_ENV => [Hash::CONFIG_KEY => $hash]]) + ->willThrowException(new FileSystemException(__('Some error'))); + + $this->hash->regenerate(); + } + + /** + * @param string $config + * @param string $hash + * @return void + */ + private function generalRegenerateMocks($config, $hash) + { + $this->dataConfigCollectorMock->expects($this->once()) + ->method('getConfig') + ->willReturn($config); + $this->configHashGeneratorMock->expects($this->once()) + ->method('generate') + ->with($config) + ->willReturn($hash); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/ImporterPoolTest.php b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/ImporterPoolTest.php new file mode 100644 index 0000000000000..f3dcfdda3e04d --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/ImporterPoolTest.php @@ -0,0 +1,89 @@ +importerMock = $this->getMockBuilder(ImporterInterface::class) + ->getMockForAbstractClass(); + $this->wrongImporter = new \StdClass(); + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMockForAbstractClass(); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->willReturnMap([ + ['Magento\Importer\SomeSection', $this->importerMock], + ['Magento\Importer\WrongSection', $this->wrongImporter], + ]); + $this->configImporterPool = new ImporterPool( + $this->objectManagerMock, + ['someSection' => 'Magento\Importer\SomeSection'] + ); + } + + /** + * @return void + */ + public function testGetImporters() + { + $expectedResult = ['someSection' => $this->importerMock]; + $this->assertSame($expectedResult, $this->configImporterPool->getImporters()); + } + + /** + * @return void + * @expectedException \Magento\Framework\Exception\ConfigurationMismatchException + * @codingStandardsIgnoreStart + * @expectedExceptionMessage wrongSection: Instance of Magento\Framework\App\DeploymentConfig\ImporterInterface is expected, got stdClass instead + * @codingStandardsIgnoreEnd + */ + public function testGetImportersWithException() + { + $this->configImporterPool = new ImporterPool( + $this->objectManagerMock, + ['wrongSection' => 'Magento\Importer\WrongSection'] + ); + + $this->configImporterPool->getImporters(); + } + + /** + * @return void + */ + public function testGetSections() + { + $this->assertSame(['someSection'], $this->configImporterPool->getSections()); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/ValidatorTest.php b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/ValidatorTest.php new file mode 100644 index 0000000000000..616d2da9291b9 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/DeploymentConfig/ValidatorTest.php @@ -0,0 +1,94 @@ +configHashMock = $this->getMockBuilder(Hash::class) + ->disableOriginalConstructor() + ->getMock(); + $this->hashGeneratorMock = $this->getMockBuilder(HashGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dataConfigCollectorMock = $this->getMockBuilder(DataCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->validator = new Validator( + $this->configHashMock, + $this->hashGeneratorMock, + $this->dataConfigCollectorMock + ); + } + + /** + * @param string $configData + * @param string $generatedHash + * @param string $savedHash + * @param bool $expectedResult + * @return void + * @dataProvider isValidDataProvider + */ + public function testIsValid($configData, $generatedHash, $savedHash, $expectedResult) + { + $this->dataConfigCollectorMock->expects($this->once()) + ->method('getConfig') + ->willReturn($configData); + $this->hashGeneratorMock->expects($this->any()) + ->method('generate') + ->with($configData) + ->willReturn($generatedHash); + $this->configHashMock->expects($this->any()) + ->method('get') + ->willReturn($savedHash); + + $this->assertSame($expectedResult, $this->validator->isValid()); + } + + /** + * @return array + */ + public function isValidDataProvider() + { + return [ + ['configData' => 'some data', 'generatedHash' => '123', 'savedHash' => '123', 'expectedResult' => true], + ['configData' => 'some data', 'generatedHash' => '321', 'savedHash' => '123', 'expectedResult' => false], + ['configData' => 'some data', 'generatedHash' => '321', 'savedHash' => null, 'expectedResult' => false], + ['configData' => null, 'generatedHash' => '321', 'savedHash' => '123', 'expectedResult' => true], + ['configData' => null, 'generatedHash' => '321', 'savedHash' => null, 'expectedResult' => true], + ]; + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/ModeTest.php b/app/code/Magento/Deploy/Test/Unit/Model/ModeTest.php new file mode 100644 index 0000000000000..4625e12ee1eca --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/ModeTest.php @@ -0,0 +1,99 @@ +inputMock = $this->getMockBuilder(InputInterface::class) + ->getMockForAbstractClass(); + $this->outputMock = $this->getMockBuilder(OutputInterface::class) + ->getMockForAbstractClass(); + $this->writerMock = $this->getMockBuilder(Writer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->readerMock = $this->getMockBuilder(Reader::class) + ->disableOriginalConstructor() + ->getMock(); + $this->maintenanceMock = $this->getMockBuilder(MaintenanceMode::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new Mode( + $this->inputMock, + $this->outputMock, + $this->writerMock, + $this->readerMock, + $this->maintenanceMock, + $this->filesystemMock + ); + } + + public function testGetMode() + { + $this->readerMock->expects($this->exactly(2)) + ->method('load') + ->willReturnOnConsecutiveCalls( + [], + [State::PARAM_MODE => State::MODE_DEVELOPER] + ); + + $this->assertSame(null, $this->model->getMode()); + $this->assertSame(State::MODE_DEVELOPER, $this->model->getMode()); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/Plugin/ConfigValidatorTest.php b/app/code/Magento/Deploy/Test/Unit/Model/Plugin/ConfigValidatorTest.php new file mode 100644 index 0000000000000..5121f373dec8c --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/Plugin/ConfigValidatorTest.php @@ -0,0 +1,77 @@ +configValidatorMock = $this->getMockBuilder(Validator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->frontControllerMock = $this->getMockBuilder(FrontController::class) + ->disableOriginalConstructor() + ->getMock(); + $this->requestMock = $this->getMockBuilder(RequestInterface::class) + ->getMockForAbstractClass(); + + $this->configValidatorPlugin = new ConfigValidator($this->configValidatorMock); + } + + /** + * @return void + */ + public function testBeforeDispatchWithoutException() + { + $this->configValidatorMock->expects($this->once()) + ->method('isValid') + ->willReturn(true); + $this->configValidatorPlugin->beforeDispatch($this->frontControllerMock, $this->requestMock); + } + + /** + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + * @codingStandardsIgnoreStart + * @expectedExceptionMessage A change in configuration has been detected. Run app:config:import or setup:upgrade command to synchronize configuration. + * @codingStandardsIgnoreEnd + */ + public function testBeforeDispatchWithException() + { + $this->configValidatorMock->expects($this->once()) + ->method('isValid') + ->willReturn(false); + $this->configValidatorPlugin->beforeDispatch($this->frontControllerMock, $this->requestMock); + } +} diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index 6751dd1e0b161..86d6ce1da1c75 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -20,6 +20,9 @@ + + + @@ -27,6 +30,7 @@ Magento\Deploy\Console\Command\ShowModeCommand \Magento\Deploy\Console\Command\App\ApplicationDumpCommand \Magento\Deploy\Console\Command\App\SensitiveConfigSetCommand + Magento\Deploy\Console\Command\App\ConfigImportCommand diff --git a/app/code/Magento/User/Block/User/Edit/Tab/Main.php b/app/code/Magento/User/Block/User/Edit/Tab/Main.php index b4a2e2d02d6f7..0e44909ad13a6 100644 --- a/app/code/Magento/User/Block/User/Edit/Tab/Main.php +++ b/app/code/Magento/User/Block/User/Edit/Tab/Main.php @@ -8,6 +8,9 @@ namespace Magento\User\Block\User\Edit\Tab; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Locale\OptionInterface; + /** * Cms page edit form main tab * @@ -27,6 +30,13 @@ class Main extends \Magento\Backend\Block\Widget\Form\Generic */ protected $_LocaleLists; + /** + * Operates with deployed locales. + * + * @var OptionInterface + */ + private $deployedLocales; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -34,6 +44,7 @@ class Main extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Backend\Model\Auth\Session $authSession * @param \Magento\Framework\Locale\ListsInterface $localeLists * @param array $data + * @param OptionInterface $deployedLocales Operates with deployed locales. */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -41,10 +52,13 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Backend\Model\Auth\Session $authSession, \Magento\Framework\Locale\ListsInterface $localeLists, - array $data = [] + array $data = [], + OptionInterface $deployedLocales = null ) { $this->_authSession = $authSession; $this->_LocaleLists = $localeLists; + $this->deployedLocales = $deployedLocales + ?: ObjectManager::getInstance()->get(OptionInterface::class); parent::__construct($context, $registry, $formFactory, $data); } @@ -138,7 +152,7 @@ protected function _prepareForm() 'name' => 'interface_locale', 'label' => __('Interface Locale'), 'title' => __('Interface Locale'), - 'values' => $this->_LocaleLists->getTranslatedOptionLocales(), + 'values' => $this->deployedLocales->getOptionLocales(), 'class' => 'select' ] ); diff --git a/app/etc/di.xml b/app/etc/di.xml index ac08fe64fc34b..feac363ee013f 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -34,6 +34,8 @@ + + diff --git a/dev/tests/functional/.htaccess.sample b/dev/tests/functional/.htaccess.sample index 260b8590125d5..d089eb3c4f848 100644 --- a/dev/tests/functional/.htaccess.sample +++ b/dev/tests/functional/.htaccess.sample @@ -1,6 +1,6 @@ ############################################## -## Allow access to command.php, website.php, export.php, pathChecker.php, deleteMagentoGeneratedCode.php and log.php - +## Allow access to command.php, website.php, export.php, pathChecker.php, locales.php, deleteMagentoGeneratedCode.php and log.php + order allow,deny allow from all diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Locales.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Locales.php new file mode 100644 index 0000000000000..5f91e36101059 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Locales.php @@ -0,0 +1,62 @@ +transport = $transport; + } + + /** + * Returns array of locales depends on fetching type. + * + * @param string $type locales fetching type + * @return array of locale codes, for example: ['en_US', 'fr_FR'] + */ + public function getList($type = self::TYPE_ALL) + { + $url = $_ENV['app_frontend_url'] . self::URL . '?type=' . $type; + $curl = $this->transport; + $curl->write($url, [], CurlInterface::GET); + $result = $curl->read(); + $curl->close(); + + return explode('|', $result); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertInterfaceLocaleAvailableOptions.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertInterfaceLocaleAvailableOptions.php new file mode 100644 index 0000000000000..1dde804ca59fe --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Constraint/AssertInterfaceLocaleAvailableOptions.php @@ -0,0 +1,51 @@ +getList(Locales::TYPE_DEPLOYED), + $dropdownLocales + ); + } else { + \PHPUnit_Framework_Assert::assertEmpty( + array_diff($dropdownLocales, $locales->getList(Locales::TYPE_ALL)) + ); + } + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Interface locales list has correct values.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/SystemAccount.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/SystemAccount.xml new file mode 100644 index 0000000000000..5a0df9e2a77f7 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/SystemAccount.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/VerifyInterfaceLocaleTest.php b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/VerifyInterfaceLocaleTest.php new file mode 100644 index 0000000000000..b52a1d8c25a64 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/VerifyInterfaceLocaleTest.php @@ -0,0 +1,85 @@ +systemAccountPage = $systemAccountPage; + $this->userEditPage = $userEdit; + } + + /** + * Test execution. + * + * @param AssertInterfaceLocaleAvailableOptions $assertInterfaceLocaleAvailableOptions assert that check + * interface locales + * @param Locales $locales utility for work with locales + */ + public function test( + AssertInterfaceLocaleAvailableOptions $assertInterfaceLocaleAvailableOptions, + Locales $locales + ) { + $this->systemAccountPage->open(); + $userForm = $this->systemAccountPage->getForm(); + $assertInterfaceLocaleAvailableOptions->processAssert( + $locales, + $userForm->getInterfaceLocales() + ); + + $this->userEditPage->open(); + $userForm = $this->userEditPage->getUserForm(); + $assertInterfaceLocaleAvailableOptions->processAssert( + $locales, + $userForm->getInterfaceLocales() + ); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/VerifyInterfaceLocaleTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/VerifyInterfaceLocaleTest.xml new file mode 100644 index 0000000000000..c0276b1237dc9 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/VerifyInterfaceLocaleTest.xml @@ -0,0 +1,14 @@ + + + + + + severity:S1 + + + diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Block/Adminhtml/User/UserForm.php b/dev/tests/functional/tests/app/Magento/User/Test/Block/Adminhtml/User/UserForm.php index 89f857823ab2a..f361b693a52e0 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/Block/Adminhtml/User/UserForm.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/Block/Adminhtml/User/UserForm.php @@ -3,16 +3,35 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\User\Test\Block\Adminhtml\User; use Magento\Backend\Test\Block\Widget\FormTabs; /** - * Class Edit * User edit form page */ class UserForm extends FormTabs { - // + /** + * Interface Locale drop-down selector. + * + * @var string + */ + private $interfaceLocaleSelect = 'select[name=interface_locale]'; + + /** + * Gets list of locale codes from "Interface Locale" field. + * + * @return array of locale codes for example ['en_US', 'de_DE'] + */ + public function getInterfaceLocales() + { + $locales = []; + $selectElement = $this->_rootElement->find($this->interfaceLocaleSelect); + foreach ($selectElement->getElements('option') as $option) { + $locales[] = $option->getValue(); + } + + return $locales; + } } diff --git a/dev/tests/functional/utils/locales.php b/dev/tests/functional/utils/locales.php new file mode 100644 index 0000000000000..b3909dc522104 --- /dev/null +++ b/dev/tests/functional/utils/locales.php @@ -0,0 +1,17 @@ +create(\Magento\Framework\Locale\Config::class); + $locales = $localeConfig->getAllowedLocales(); +} + +echo implode('|', $locales); diff --git a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php index 724c379cc9511..2570c2b6c92e5 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php @@ -5,21 +5,23 @@ */ namespace Magento\Config\Console\Command; +use Magento\Config\Model\Config\PathValidator; +use Magento\Config\Model\Config\PathValidatorFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\DeploymentConfig\Reader; +use Magento\Framework\App\DeploymentConfig\FileReader; use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Console\Cli; +use Magento\Framework\Filesystem; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Stdlib\ArrayManager; use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit_Framework_MockObject_MockObject as Mock; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Magento\Framework\Filesystem; -use PHPUnit_Framework_MockObject_MockObject as Mock; /** * Tests the different flows of config:set command. @@ -44,13 +46,23 @@ class ConfigSetCommandTest extends \PHPUnit_Framework_TestCase */ private $outputMock; + /** + * @var PathValidatorFactory|Mock + */ + private $pathValidatorFactoryMock; + + /** + * @var PathValidator|Mock + */ + private $pathValidatorMock; + /** * @var ScopeConfigInterface */ private $scopeConfig; /** - * @var Reader + * @var FileReader */ private $reader; @@ -81,7 +93,7 @@ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); $this->scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $this->reader = $this->objectManager->get(Reader::class); + $this->reader = $this->objectManager->get(FileReader::class); $this->filesystem = $this->objectManager->get(Filesystem::class); $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); $this->arrayManager = $this->objectManager->get(ArrayManager::class); @@ -94,6 +106,18 @@ protected function setUp() ->getMockForAbstractClass(); $this->outputMock = $this->getMockBuilder(OutputInterface::class) ->getMockForAbstractClass(); + $this->pathValidatorFactoryMock = $this->getMockBuilder(PathValidatorFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->pathValidatorMock = $this->getMockBuilder(PathValidator::class) + ->disableOriginalConstructor() + ->setMethods(['validate']) + ->getMock(); + + $this->pathValidatorFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->pathValidatorMock); } /** @@ -102,12 +126,12 @@ protected function setUp() protected function tearDown() { $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( - $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), + $this->configFilePool->getPath(ConfigFilePool::APP_ENV), "objectManager->get(Writer::class); - $writer->saveConfig([ConfigFilePool::APP_CONFIG => $this->config]); + $writer->saveConfig([ConfigFilePool::APP_ENV => $this->config]); } /** @@ -115,11 +139,7 @@ protected function tearDown() */ private function loadConfig() { - return $this->reader->loadConfigFile( - ConfigFilePool::APP_CONFIG, - $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), - true - ); + return $this->reader->load(ConfigFilePool::APP_ENV); } /** @@ -155,7 +175,9 @@ public function testRun($path, $value, $scope = ScopeConfigInterface::SCOPE_TYPE ); /** @var ConfigSetCommand $command */ - $command = $this->objectManager->create(ConfigSetCommand::class); + $command = $this->objectManager->create(ConfigSetCommand::class, [ + 'pathValidatorFactory' => $this->pathValidatorFactoryMock, + ]); $status = $command->run($this->inputMock, $this->outputMock); $this->assertSame(Cli::RETURN_SUCCESS, $status); @@ -212,7 +234,9 @@ public function testRunLock($path, $value, $scope = ScopeConfigInterface::SCOPE_ ); /** @var ConfigSetCommand $command */ - $command = $this->objectManager->create(ConfigSetCommand::class); + $command = $this->objectManager->create(ConfigSetCommand::class, [ + 'pathValidatorFactory' => $this->pathValidatorFactoryMock, + ]); /** @var ConfigPathResolver $resolver */ $resolver = $this->objectManager->get(ConfigPathResolver::class); $status = $command->run($this->inputMock, $this->outputMock); @@ -310,7 +334,9 @@ private function runCommand( ->with($expectedMessage); /** @var ConfigSetCommand $command */ - $command = $this->objectManager->create(ConfigSetCommand::class); + $command = $this->objectManager->create(ConfigSetCommand::class, [ + 'pathValidatorFactory' => $this->pathValidatorFactoryMock, + ]); $status = $command->run($input, $output); $this->assertSame($expectedCode, $status); @@ -360,7 +386,9 @@ public function testRunScopeValidation( $expectations($this->outputMock); /** @var ConfigSetCommand $command */ - $command = $this->objectManager->create(ConfigSetCommand::class); + $command = $this->objectManager->create(ConfigSetCommand::class, [ + 'pathValidatorFactory' => $this->pathValidatorFactoryMock, + ]); $command->run($this->inputMock, $this->outputMock); } @@ -418,4 +446,82 @@ function (Mock $output) { ] ]; } + + /** + * Tests different scenarios for scope options. + * + * @param \Closure $expectations + * @param string $path + * @param string $value + * @param string $scope + * @param string $scopeCode + * @magentoDbIsolation enabled + * @dataProvider getRunPathValidationDataProvider + */ + public function testRunPathValidation( + \Closure $expectations, + $path, + $value, + $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + $scopeCode = null + ) { + $this->inputMock->expects($this->any()) + ->method('getArgument') + ->willReturnMap([ + [ConfigSetCommand::ARG_PATH, $path], + [ConfigSetCommand::ARG_VALUE, $value] + ]); + $this->inputMock->expects($this->any()) + ->method('getOption') + ->willReturnMap([ + [ConfigSetCommand::OPTION_SCOPE, $scope], + [ConfigSetCommand::OPTION_SCOPE_CODE, $scopeCode] + ]); + + $expectations($this->outputMock); + + /** @var ConfigSetCommand $command */ + $command = $this->objectManager->create(ConfigSetCommand::class); + $command->run($this->inputMock, $this->outputMock); + } + + /** + * Retrieves variations with callback, path, value, scope and scope code. + * + * @return array + */ + public function getRunPathValidationDataProvider() + { + return [ + [ + function (Mock $output) { + $output->expects($this->once()) + ->method('writeln') + ->with('Value was saved.'); + }, + 'web/unsecure/base_url', + 'http://magento2.local/', + ], + [ + function (Mock $output) { + $output->expects($this->once()) + ->method('writeln') + ->with( + 'Invalid Base URL. Value must be a URL or one of placeholders: {{base_url}}' + ); + }, + 'web/unsecure/base_url', + 'value', + ], + [ + function (Mock $output) { + $output->expects($this->once()) + ->method('writeln') + ->with('The "test/test/test" path does not exist'); + }, + 'test/test/test', + 'value', + ] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php index 596729fc107ac..658a9148f765b 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Config\Console\Command; +use Magento\Framework\App\DeploymentConfig\FileReader; use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\Console\Cli; @@ -13,7 +14,6 @@ use Magento\Framework\Filesystem; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\App\DeploymentConfig\Reader; use Magento\Framework\App\DeploymentConfig\Writer; class ConfigShowCommandTest extends \PHPUnit_Framework_TestCase @@ -39,7 +39,7 @@ class ConfigShowCommandTest extends \PHPUnit_Framework_TestCase private $configFilePool; /** - * @var Reader + * @var FileReader */ private $reader; @@ -58,25 +58,32 @@ class ConfigShowCommandTest extends \PHPUnit_Framework_TestCase */ private $config; + /** + * @var array + */ + private $envConfig; + + /** + * @inheritdoc + */ public function setUp() { $this->objectManager = Bootstrap::getObjectManager(); $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); $this->filesystem = $this->objectManager->get(Filesystem::class); - $this->reader = $this->objectManager->get(Reader::class); + $this->reader = $this->objectManager->get(FileReader::class); $this->writer = $this->objectManager->get(Writer::class); $this->config = $this->loadConfig(); + $this->envConfig = $this->loadEnvConfig(); $this->env = $_ENV; - $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( - $this->getFileName(), - file_get_contents(__DIR__ . '/../../_files/_config.local.php') - ); - - $config = include(__DIR__ . '/../../_files/_config.php'); + $config = include(__DIR__ . '/../../_files/config.php'); $this->writer->saveConfig([ConfigFilePool::APP_CONFIG => $config]); + $config = include(__DIR__ . '/../../_files/env.php'); + $this->writer->saveConfig([ConfigFilePool::APP_ENV => $config]); + $_ENV['CONFIG__DEFAULT__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.default.test'; $_ENV['CONFIG__WEBSITES__BASE__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.website_base.test'; $_ENV['CONFIG__STORES__DEFAULT__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.store_default.test'; @@ -138,6 +145,7 @@ public function executeDataProvider() 'web/test/test_value_2' => ['value2.local_config.default.test'], 'web/test2/test_value_3' => ['value3.config.default.test'], 'web/test2/test_value_4' => ['value4.env.default.test'], + 'carriers/fedex/account' => ['******'], 'web/test' => [ 'web/test/test_value_1 - value1.db.default.test', 'web/test/test_value_2 - value2.local_config.default.test', @@ -157,6 +165,7 @@ public function executeDataProvider() 'web/test/test_value_2 - value2.local_config.default.test', 'web/test2/test_value_3 - value3.config.default.test', 'web/test2/test_value_4 - value4.env.default.test', + 'carriers/fedex/account - ******', ], ] ], @@ -276,37 +285,35 @@ public function executeDataProvider() } /** - * @return string + * @return array */ - private function getFileName() + private function loadConfig() { - $filePool = $this->configFilePool->getInitialFilePools(); - - return $filePool[ConfigFilePool::LOCAL][ConfigFilePool::APP_CONFIG]; + return $this->reader->load(ConfigFilePool::APP_CONFIG); } /** * @return array */ - private function loadConfig() + private function loadEnvConfig() { - return $this->reader->loadConfigFile( - ConfigFilePool::APP_CONFIG, - $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), - true - ); + return $this->reader->load(ConfigFilePool::APP_ENV); } public function tearDown() { $_ENV = $this->env; - $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->delete( - $this->getFileName() - ); + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), "filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->configFilePool->getPath(ConfigFilePool::APP_ENV), + "writer->saveConfig([ConfigFilePool::APP_CONFIG => $this->config]); + $this->writer->saveConfig([ConfigFilePool::APP_ENV => $this->envConfig]); } } diff --git a/dev/tests/integration/testsuite/Magento/Config/_files/_config.local.php b/dev/tests/integration/testsuite/Magento/Config/_files/config.php similarity index 100% rename from dev/tests/integration/testsuite/Magento/Config/_files/_config.local.php rename to dev/tests/integration/testsuite/Magento/Config/_files/config.php diff --git a/dev/tests/integration/testsuite/Magento/Config/_files/config_data.php b/dev/tests/integration/testsuite/Magento/Config/_files/config_data.php index 9b9c2fe6c826a..f48a932a70009 100644 --- a/dev/tests/integration/testsuite/Magento/Config/_files/config_data.php +++ b/dev/tests/integration/testsuite/Magento/Config/_files/config_data.php @@ -16,6 +16,7 @@ 'web/test/test_value_2' => 'value2.db.default.test', 'web/test2/test_value_3' => 'value3.db.default.test', 'web/test2/test_value_4' => 'value4.db.default.test', + 'carriers/fedex/account' => 'value5.db.hashed.value', ] ], ScopeInterface::SCOPE_WEBSITES => [ diff --git a/dev/tests/integration/testsuite/Magento/Config/_files/_config.php b/dev/tests/integration/testsuite/Magento/Config/_files/env.php similarity index 56% rename from dev/tests/integration/testsuite/Magento/Config/_files/_config.php rename to dev/tests/integration/testsuite/Magento/Config/_files/env.php index 79eb6e7e12db8..99285e9344df5 100644 --- a/dev/tests/integration/testsuite/Magento/Config/_files/_config.php +++ b/dev/tests/integration/testsuite/Magento/Config/_files/env.php @@ -4,6 +4,40 @@ * See COPYING.txt for license details. */ return [ + 'backend' => [ + 'frontName' => 'admin', + ], + 'crypt' => [ + 'key' => 'some_key', + ], + 'session' => [ + 'save' => 'files', + ], + 'db' => [ + 'table_prefix' => '', + 'connection' => [], + ], + 'resource' => [], + 'x-frame-options' => 'SAMEORIGIN', + 'MAGE_MODE' => 'default', + 'cache_types' => [ + 'config' => 1, + 'layout' => 1, + 'block_html' => 1, + 'collections' => 1, + 'reflection' => 1, + 'db_ddl' => 1, + 'eav' => 1, + 'customer_notification' => 1, + 'config_integration' => 1, + 'config_integration_api' => 1, + 'full_page' => 1, + 'translate' => 1, + 'config_webservice' => 1, + ], + 'install' => [ + 'date' => 'Thu, 09 Feb 2017 14:28:00 +0000', + ], 'system' => [ 'default' => [ 'web' => [ diff --git a/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ApplicationDumpCommandTest.php b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ApplicationDumpCommandTest.php index 8d8c16bf3be83..7b32104ca8521 100644 --- a/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ApplicationDumpCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ApplicationDumpCommandTest.php @@ -5,15 +5,19 @@ */ namespace Magento\Deploy\Console\Command\App; +use Magento\Deploy\Model\DeploymentConfig\Hash; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ApplicationDumpCommandTest extends \PHPUnit_Framework_TestCase { /** @@ -22,14 +26,67 @@ class ApplicationDumpCommandTest extends \PHPUnit_Framework_TestCase private $objectManager; /** - * @var DeploymentConfig\Reader + * @var DeploymentConfig\FileReader */ private $reader; + /** + * @var ConfigFilePool + */ + private $configFilePool; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var DeploymentConfig\Writer + */ + private $writer; + + /** + * @var array + */ + private $config; + + /** + * @var array + */ + private $envConfig; + + /** + * @inheritdoc + */ public function setUp() { $this->objectManager = Bootstrap::getObjectManager(); + $this->reader = $this->objectManager->get(DeploymentConfig\FileReader::class); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); $this->reader = $this->objectManager->get(DeploymentConfig\Reader::class); + $this->writer = $this->objectManager->get(DeploymentConfig\Writer::class); + $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); + + // Snapshot of configuration. + $this->config = $this->loadConfig(); + $this->envConfig = $this->loadEnvConfig(); + } + + /** + * @return array + */ + private function loadConfig() + { + return $this->reader->load(ConfigFilePool::APP_CONFIG); + } + + /** + * @return array + */ + private function loadEnvConfig() + { + return $this->reader->load(ConfigFilePool::APP_ENV); } /** @@ -38,6 +95,7 @@ public function setUp() */ public function testExecute() { + $this->assertArrayNotHasKey(Hash::CONFIG_KEY, $this->envConfig); $this->objectManager->configure([ \Magento\Config\Model\Config\Export\ExcludeList::class => [ 'arguments' => [ @@ -65,10 +123,11 @@ public function testExecute() $command = $this->objectManager->create(ApplicationDumpCommand::class); $command->run($this->getMock(InputInterface::class), $outputMock); - $config = $this->reader->loadConfigFile(ConfigFilePool::APP_CONFIG, $this->getFileName()); + $config = $this->loadConfig(); $this->validateSystemSection($config); $this->validateThemesSection($config); + $this->assertArrayHasKey(Hash::CONFIG_KEY, $this->loadEnvConfig()); } /** @@ -139,31 +198,27 @@ private function validateThemesSection(array $config) ); } + /** + * @inheritdoc + */ public function tearDown() { - $file = $this->getFileName(); - /** @var DirectoryList $dirList */ - $dirList = $this->objectManager->get(DirectoryList::class); - $path = $dirList->getPath(DirectoryList::CONFIG); - $driverPool = $this->objectManager->get(DriverPool::class); - $fileDriver = $driverPool->getDriver(DriverPool::FILE); - if ($fileDriver->isExists($path . '/' . $file)) { - unlink($path . '/' . $file); - } + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), + "objectManager->get(DeploymentConfig\Writer::class); + $writer->saveConfig([ConfigFilePool::APP_CONFIG => $this->config]); + /** @var DeploymentConfig $deploymentConfig */ $deploymentConfig = $this->objectManager->get(DeploymentConfig::class); $deploymentConfig->resetData(); - } - /** - * @return string - */ - private function getFileName() - { - /** @var ConfigFilePool $configFilePool */ - $configFilePool = $this->objectManager->get(ConfigFilePool::class); - $filePool = $configFilePool->getInitialFilePools(); - - return $filePool[ConfigFilePool::LOCAL][ConfigFilePool::APP_CONFIG]; + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->configFilePool->getPath(ConfigFilePool::APP_ENV), + "writer->saveConfig([ConfigFilePool::APP_ENV => $this->envConfig]); } } diff --git a/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ConfigImportCommand/IntegrationTestImporter.php b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ConfigImportCommand/IntegrationTestImporter.php new file mode 100644 index 0000000000000..131be392d00c9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ConfigImportCommand/IntegrationTestImporter.php @@ -0,0 +1,22 @@ +Integration test data is imported!'; + + return $messages; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ConfigImportCommandTest.php b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ConfigImportCommandTest.php new file mode 100644 index 0000000000000..6c32edee01200 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/ConfigImportCommandTest.php @@ -0,0 +1,142 @@ +objectManager = Bootstrap::getObjectManager(); + $this->objectManager->configure([ + ImporterPool::class => [ + 'arguments' => [ + 'importers' => [ + 'integrationTestImporter' => IntegrationTestImporter::class + ] + ] + ] + ]); + $this->reader = $this->objectManager->get(DeploymentConfig\Reader::class); + $this->writer = $this->objectManager->get(DeploymentConfig\Writer::class); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); + + $this->envConfig = $this->loadEnvConfig(); + $this->config = $this->loadConfig(); + } + + public function tearDown() + { + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), + "objectManager->get(DeploymentConfig\Writer::class); + $writer->saveConfig([ConfigFilePool::APP_CONFIG => $this->config]); + + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->configFilePool->getPath(ConfigFilePool::APP_ENV), + "writer->saveConfig([ConfigFilePool::APP_ENV => $this->envConfig]); + } + + public function testExecuteNothingImport() + { + $this->assertArrayNotHasKey(Hash::CONFIG_KEY, $this->envConfig); + $command = $this->objectManager->create(ConfigImportCommand::class); + $commandTester = new CommandTester($command); + $commandTester->execute([]); + $this->assertSame(Cli::RETURN_SUCCESS, $commandTester->getStatusCode()); + $this->assertContains('Start import', $commandTester->getDisplay()); + $this->assertContains('Nothing to import', $commandTester->getDisplay()); + $this->assertArrayNotHasKey(Hash::CONFIG_KEY, $this->loadEnvConfig()); + } + + public function testExecuteWithImport() + { + $this->assertArrayNotHasKey(Hash::CONFIG_KEY, $this->envConfig); + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), + file_get_contents(__DIR__ . '/../../../_files/config.php') + ); + $command = $this->objectManager->create(ConfigImportCommand::class); + $commandTester = new CommandTester($command); + $commandTester->execute([]); + $this->assertSame(Cli::RETURN_SUCCESS, $commandTester->getStatusCode()); + $this->assertContains('Start import', $commandTester->getDisplay()); + $this->assertContains('Integration test data is imported!', $commandTester->getDisplay()); + $this->assertArrayHasKey(Hash::CONFIG_KEY, $this->loadEnvConfig()); + } + + /** + * @return array + */ + private function loadConfig() + { + return $this->reader->load(ConfigFilePool::APP_CONFIG); + } + + /** + * @return array + */ + private function loadEnvConfig() + { + return $this->reader->load(ConfigFilePool::APP_ENV); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommandTest.php b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommandTest.php index ad51c02eaeda6..c2ea062de0543 100644 --- a/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommandTest.php @@ -8,7 +8,7 @@ use Magento\Deploy\Console\Command\App\SensitiveConfigSet\CollectorFactory; use Magento\Deploy\Console\Command\App\SensitiveConfigSet\InteractiveCollector; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\DeploymentConfig\Reader; +use Magento\Framework\App\DeploymentConfig\FileReader; use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\File\ConfigFilePool; @@ -30,15 +30,25 @@ class SensitiveConfigSetCommandTest extends \PHPUnit_Framework_TestCase private $objectManager; /** - * @var Reader + * @var FileReader */ private $reader; + /** + * @var Writer + */ + private $writer; + /** * @var ConfigFilePool */ private $configFilePool; + /** + * @var array + */ + private $envConfig; + /** * @var array */ @@ -55,13 +65,17 @@ class SensitiveConfigSetCommandTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $this->reader = $this->objectManager->get(Reader::class); + $this->reader = $this->objectManager->get(FileReader::class); + $this->writer = $this->objectManager->get(Writer::class); $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); - $this->config = $this->loadConfig(); $this->filesystem = $this->objectManager->get(Filesystem::class); + + $this->envConfig = $this->loadEnvConfig(); + $this->config = $this->loadConfig(); + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( - $this->getFileName(), - file_get_contents(__DIR__ . '/../../../_files/_config.local.php') + $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), + file_get_contents(__DIR__ . '/../../../_files/config.php') ); } @@ -71,14 +85,14 @@ public function setUp() * @param callable $assertCallback * @magentoDataFixture Magento/Store/_files/website.php * @magentoDbIsolation enabled - * @dataProvider testExecuteDataProvider + * @dataProvider executeDataProvider */ public function testExecute($scope, $scopeCode, callable $assertCallback) { $outputMock = $this->getMock(OutputInterface::class); $outputMock->expects($this->at(0)) ->method('writeln') - ->with('Configuration value saved in app/etc/config.php'); + ->with('Configuration value saved in app/etc/env.php'); $inputMock = $this->getMock(InputInterface::class); $inputMock->expects($this->exactly(2)) @@ -108,12 +122,12 @@ public function testExecute($scope, $scopeCode, callable $assertCallback) $command = $this->objectManager->create(SensitiveConfigSetCommand::class); $command->run($inputMock, $outputMock); - $config = $this->loadConfig(); + $config = $this->loadEnvConfig(); $assertCallback($config); } - public function testExecuteDataProvider() + public function executeDataProvider() { return [ [ @@ -147,7 +161,7 @@ function (array $config) { * @param callable $assertCallback * @magentoDataFixture Magento/Store/_files/website.php * @magentoDbIsolation enabled - * @dataProvider testExecuteInteractiveDataProvider + * @dataProvider executeInteractiveDataProvider */ public function testExecuteInteractive($scope, $scopeCode, callable $assertCallback) { @@ -158,7 +172,7 @@ public function testExecuteInteractive($scope, $scopeCode, callable $assertCallb ->with('Please set configuration values or skip them by pressing [Enter]:'); $outputMock->expects($this->at(1)) ->method('writeln') - ->with('Configuration values saved in app/etc/config.php'); + ->with('Configuration values saved in app/etc/env.php'); $inputMock->expects($this->exactly(3)) ->method('getOption') ->withConsecutive( @@ -201,12 +215,12 @@ public function testExecuteInteractive($scope, $scopeCode, callable $assertCallb ); $command->run($inputMock, $outputMock); - $config = $this->loadConfig(); + $config = $this->loadEnvConfig(); $assertCallback($config); } - public function testExecuteInteractiveDataProvider() + public function executeInteractiveDataProvider() { return [ [ @@ -259,28 +273,30 @@ function (array $config) { */ public function tearDown() { - $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->delete( - $this->getFileName() - ); $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), "filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->configFilePool->getPath(ConfigFilePool::APP_ENV), + "objectManager->get(Writer::class); + $writer->saveConfig([ConfigFilePool::APP_ENV => $this->envConfig]); + /** @var Writer $writer */ $writer = $this->objectManager->get(Writer::class); $writer->saveConfig([ConfigFilePool::APP_CONFIG => $this->config]); } /** - * @return string + * @return array */ - private function getFileName() + private function loadEnvConfig() { - /** @var ConfigFilePool $configFilePool */ - $configFilePool = $this->objectManager->get(ConfigFilePool::class); - $filePool = $configFilePool->getInitialFilePools(); - - return $filePool[ConfigFilePool::LOCAL][ConfigFilePool::APP_CONFIG]; + return $this->reader->load(ConfigFilePool::APP_ENV); } /** @@ -288,10 +304,6 @@ private function getFileName() */ private function loadConfig() { - return $this->reader->loadConfigFile( - ConfigFilePool::APP_CONFIG, - $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), - true - ); + return $this->reader->load(ConfigFilePool::APP_CONFIG); } } diff --git a/dev/tests/integration/testsuite/Magento/Deploy/_files/_config.local.php b/dev/tests/integration/testsuite/Magento/Deploy/_files/config.php similarity index 84% rename from dev/tests/integration/testsuite/Magento/Deploy/_files/_config.local.php rename to dev/tests/integration/testsuite/Magento/Deploy/_files/config.php index ae4630b1a2b07..1a392714716c2 100644 --- a/dev/tests/integration/testsuite/Magento/Deploy/_files/_config.local.php +++ b/dev/tests/integration/testsuite/Magento/Deploy/_files/config.php @@ -19,5 +19,10 @@ 'web' => [], 'general' => [] ] - ] + ], + 'integrationTestImporter' => [ + 'someGroup' => [ + 'someField' => 'testValue', + ] + ], ]; diff --git a/lib/internal/Magento/Framework/App/Config/InitialConfigSource.php b/lib/internal/Magento/Framework/App/Config/InitialConfigSource.php index 1c1b3e9272a13..1b018a650b546 100644 --- a/lib/internal/Magento/Framework/App/Config/InitialConfigSource.php +++ b/lib/internal/Magento/Framework/App/Config/InitialConfigSource.php @@ -25,6 +25,7 @@ class InitialConfigSource implements ConfigSourceInterface /** * @var string + * @deprecated Initial configs can not be separated since 2.2.0 version */ private $fileKey; @@ -35,7 +36,7 @@ class InitialConfigSource implements ConfigSourceInterface * @param string $configType * @param string $fileKey */ - public function __construct(Reader $reader, $configType, $fileKey) + public function __construct(Reader $reader, $configType, $fileKey = null) { $this->reader = $reader; $this->configType = $configType; @@ -47,7 +48,7 @@ public function __construct(Reader $reader, $configType, $fileKey) */ public function get($path = '') { - $data = new DataObject($this->reader->load($this->fileKey)); + $data = new DataObject($this->reader->load()); if ($path !== '' && $path !== null) { $path = '/' . $path; } diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig/FileReader.php b/lib/internal/Magento/Framework/App/DeploymentConfig/FileReader.php new file mode 100644 index 0000000000000..e38f850999fbb --- /dev/null +++ b/lib/internal/Magento/Framework/App/DeploymentConfig/FileReader.php @@ -0,0 +1,76 @@ +dirList = $dirList; + $this->configFilePool = $configFilePool; + $this->driverPool = $driverPool; + } + + /** + * Loads the configuration file. + * + * @param string $fileKey The file key + * @return array The configurations array + * @throws FileSystemException If file can not be read + * @throws \Exception If file key is not correct + */ + public function load($fileKey) + { + $path = $this->dirList->getPath(DirectoryList::CONFIG); + $fileDriver = $this->driverPool->getDriver(DriverPool::FILE); + $filePath = $path . '/' . $this->configFilePool->getPath($fileKey); + + if ($fileDriver->isExists($filePath)) { + return include $filePath; + } + + return []; + } +} diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig/ImporterInterface.php b/lib/internal/Magento/Framework/App/DeploymentConfig/ImporterInterface.php new file mode 100644 index 0000000000000..81617f00c5b49 --- /dev/null +++ b/lib/internal/Magento/Framework/App/DeploymentConfig/ImporterInterface.php @@ -0,0 +1,24 @@ +dirList->getPath(DirectoryList::CONFIG); - $fileDriver = $this->driverPool->getDriver(DriverPool::FILE); - $initialFilePools = $this->configFilePool->getInitialFilePools(); - - $files = []; - foreach ($this->files as $fileKey => $filePath) { - $files[$fileKey] = $filePath; - if (!$fileDriver->isExists($path . "/" . $filePath)) { - foreach ($initialFilePools as $initialFiles) { - if ( - isset($initialFiles[$fileKey]) - && $fileDriver->isExists($path . '/' . $initialFiles[$fileKey]) - ) { - $files[$fileKey] = $initialFiles[$fileKey]; - } - } - } - } - - return $files; + return $this->files; } /** - * Loads the configuration file + * Method loads merged configuration within all configuration files. + * To retrieve specific file configuration, use FileReader. + * $fileKey option is deprecated since version 2.2.0. * - * @param string $fileKey + * @param string $fileKey The file key (deprecated) * @return array - * @throws \Exception + * @throws FileSystemException If file can not be read + * @throws \Exception If file key is not correct + * @see FileReader */ public function load($fileKey = null) { + $path = $this->dirList->getPath(DirectoryList::CONFIG); + $fileDriver = $this->driverPool->getDriver(DriverPool::FILE); + $result = []; if ($fileKey) { - $pathConfig = $this->configFilePool->getPath($fileKey); - return $this->loadConfigFile($fileKey, $pathConfig); + $filePath = $path . '/' . $this->configFilePool->getPath($fileKey); + if ($fileDriver->isExists($filePath)) { + $result = include $filePath; + } } else { $configFiles = $this->configFilePool->getPaths(); $allFilesData = []; $result = []; - foreach ($configFiles as $fileKey => $pathConfig) { - $fileData = $this->loadConfigFile($fileKey, $pathConfig); - if (!$fileData) { + foreach (array_keys($configFiles) as $fileKey) { + $configFile = $path . '/' . $this->configFilePool->getPath($fileKey); + if ($fileDriver->isExists($configFile)) { + $fileData = include $configFile; + } else { continue; } - $allFilesData[$fileKey] = $fileData; + $allFilesData[$configFile] = $fileData; if (!empty($fileData)) { - $intersection = array_intersect_key($result, $fileData); - if (!empty($intersection)) { - $displayMessage = $this->findFilesWithKeys(array_keys($intersection), $allFilesData); - throw new \Exception( - "Key collision! The following keys occur in multiple config files:" - . PHP_EOL . $displayMessage - ); - } - $result = array_merge($result, $fileData); + $result = array_replace_recursive($result, $fileData); } } - return $result; } + return $result ?: []; } /** - * @param string $fileKey - * @param string $pathConfig - * @param bool $ignoreInitialConfigFiles + * Loads the configuration file. + * + * @param string $fileKey The file key + * @param string $pathConfig The path config + * @param bool $ignoreInitialConfigFiles Whether ignore custom pools * @return array + * @deprecated Magento does not support custom config file pools since 2.2.0 version + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function loadConfigFile($fileKey, $pathConfig, $ignoreInitialConfigFiles = false) { - $result = []; - $initialFilePools = $this->configFilePool->getInitialFilePools(); - $path = $this->dirList->getPath(DirectoryList::CONFIG); - $fileDriver = $this->driverPool->getDriver(DriverPool::FILE); - - if (!$ignoreInitialConfigFiles) { - foreach ($initialFilePools as $initialFiles) { - if (isset($initialFiles[$fileKey]) && $fileDriver->isExists($path . '/' . $initialFiles[$fileKey])) { - $fileBuffer = include $path . '/' . $initialFiles[$fileKey]; - if (is_array($fileBuffer)) { - $result = array_replace_recursive($result, $fileBuffer); - } - } - } - } - - if ($fileDriver->isExists($path . '/' . $pathConfig)) { - $fileBuffer = include $path . '/' . $pathConfig; - $result = array_replace_recursive($result, $fileBuffer); - } - - if ($fileDriver->isExists($path . '/' . $pathConfig)) { - $configResult = include $path . '/' . $pathConfig; - if (is_array($configResult)) { - $result = array_replace_recursive($result, $configResult); - } - } - - return $result; - } - - /** - * Finds list of files that has the key - * - * @param array $keys - * @param array $allFilesData - * @return string - */ - private function findFilesWithKeys(array $keys, array $allFilesData) - { - $displayMessage = ''; - foreach ($keys as $key) { - $foundConfigFiles = []; - foreach ($allFilesData as $fileName => $fileValues) { - if (isset($fileValues[$key])) { - $foundConfigFiles[] = $fileName; - } - } - $displayMessage .= 'Key "' . $key . '" found in ' . implode(', ', $foundConfigFiles) . PHP_EOL; - } - return $displayMessage; + return $this->load($fileKey); } } diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig/Writer.php b/lib/internal/Magento/Framework/App/DeploymentConfig/Writer.php index badbea2b8b934..2db72e51281b0 100644 --- a/lib/internal/Magento/Framework/App/DeploymentConfig/Writer.php +++ b/lib/internal/Magento/Framework/App/DeploymentConfig/Writer.php @@ -3,18 +3,17 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; -use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Phrase; /** - * Deployment configuration writer to files: env.php, config.php (config.local.php, config.dist.php) + * Deployment configuration writer to files: env.php, config.php. */ class Writer { @@ -50,8 +49,6 @@ class Writer private $deploymentConfig; /** - * Constructor - * * @param Reader $reader * @param Filesystem $filesystem * @param ConfigFilePool $configFilePool @@ -67,9 +64,9 @@ public function __construct( ) { $this->reader = $reader; $this->filesystem = $filesystem; - $this->formatter = $formatter ?: new Writer\PhpFormatter(); $this->configFilePool = $configFilePool; $this->deploymentConfig = $deploymentConfig; + $this->formatter = $formatter ?: new Writer\PhpFormatter(); } /** @@ -89,22 +86,36 @@ public function checkIfWritable() } /** - * Saves config + * Saves config in specified file. + * $pool option is deprecated since version 2.2.0. + * + * Usage: + * ```php + * saveConfig( + * [ + * ConfigFilePool::APP_ENV => ['some' => 'value'], + * ], + * true, + * null, + * [] + * ) + * ``` * - * @param array $data - * @param bool $override - * @param string $pool - * @param array $comments + * @param array $data The data to be saved + * @param bool $override Whether values should be overridden + * @param string $pool The file pool (deprecated) + * @param array $comments The array of comments * @return void * @throws FileSystemException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function saveConfig(array $data, $override = false, $pool = null, array $comments = []) { foreach ($data as $fileKey => $config) { - $paths = $pool ? $this->configFilePool->getPathsByPool($pool) : $this->configFilePool->getPaths(); + $paths = $this->configFilePool->getPaths(); if (isset($paths[$fileKey])) { - $currentData = $this->reader->loadConfigFile($fileKey, $paths[$fileKey], true); + $currentData = $this->reader->load($fileKey); if ($currentData) { if ($override) { $config = array_merge($currentData, $config); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Config/InitialConfigSourceTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Config/InitialConfigSourceTest.php index cb92ac4ab405a..99fe505433281 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Config/InitialConfigSourceTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Config/InitialConfigSourceTest.php @@ -22,11 +22,6 @@ class InitialConfigSourceTest extends \PHPUnit_Framework_TestCase */ private $configType; - /** - * @var string - */ - private $fileKey; - /** * @var InitialConfigSource */ @@ -38,8 +33,7 @@ public function setUp() ->disableOriginalConstructor() ->getMock(); $this->configType = 'configType'; - $this->fileKey = 'file.php'; - $this->source = new InitialConfigSource($this->reader, $this->configType, $this->fileKey); + $this->source = new InitialConfigSource($this->reader, $this->configType); } public function testGet() @@ -47,7 +41,6 @@ public function testGet() $path = 'path'; $this->reader->expects($this->once()) ->method('load') - ->with($this->fileKey) ->willReturn([$this->configType => [$path => 'value']]); $this->assertEquals('value', $this->source->get($path)); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/FileReaderTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/FileReaderTest.php new file mode 100644 index 0000000000000..dd679c64ce322 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/FileReaderTest.php @@ -0,0 +1,92 @@ +dirListMock = $this->getMockBuilder(DirectoryList::class) + ->disableOriginalConstructor() + ->getMock(); + $this->driverPoolMock = $this->getMockBuilder(DriverPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configFilePool = $this->getMockBuilder(ConfigFilePool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->driverMock = $this->getMockBuilder(DriverInterface::class) + ->getMockForAbstractClass(); + + $this->model = new FileReader( + $this->dirListMock, + $this->driverPoolMock, + $this->configFilePool + ); + } + + public function testLoad() + { + $fileKey = 'configKeyOne'; + + $this->dirListMock->expects($this->exactly(2)) + ->method('getPath') + ->with(DirectoryList::CONFIG) + ->willReturn(__DIR__ . '/_files'); + $this->driverPoolMock->expects($this->exactly(2)) + ->method('getDriver') + ->with(DriverPool::FILE) + ->willReturn($this->driverMock); + $this->configFilePool->expects($this->exactly(2)) + ->method('getPath') + ->willReturnMap([['configKeyOne', 'config.php']]); + $this->driverMock->expects($this->exactly(2)) + ->method('isExists') + ->willReturnOnConsecutiveCalls(true, false); + $this->configFilePool + ->expects($this->any()) + ->method('getPath') + ->willReturnMap([['configKeyOne', 'config.php']]); + + $this->assertSame(['fooKey' => 'foo', 'barKey' => 'bar'], $this->model->load($fileKey)); + $this->assertSame([], $this->model->load($fileKey)); + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/ReaderTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/ReaderTest.php index 387d3b3ed7467..cdf4f3228b355 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/ReaderTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/ReaderTest.php @@ -61,10 +61,6 @@ protected function setUp() ->expects($this->any()) ->method('getPaths') ->willReturn(['configKeyOne' => 'config.php', 'configKeyTwo' => 'env.php']); - $this->configFilePool - ->expects($this->any()) - ->method('getInitialFilePools') - ->willReturn([]); } public function testGetFile() @@ -107,7 +103,6 @@ public function testCustomLoad($file, $expected) $configFilePool = $this->getMock(\Magento\Framework\Config\File\ConfigFilePool::class, [], [], '', false); $configFilePool->expects($this->any())->method('getPaths')->willReturn([$file]); $configFilePool->expects($this->any())->method('getPath')->willReturn($file); - $configFilePool->expects($this->any())->method('getInitialFilePools')->willReturn([]); $object = new Reader($this->dirList, $this->driverPool, $configFilePool, $file); $this->assertSame($expected, $object->load($file)); } @@ -122,50 +117,4 @@ public function loadCustomDataProvider() ['nonexistent.php', []], ]; } - - /** - * @expectedException \Exception - * @expectedExceptionMessage Key collision - */ - public function testMerging() - { - $configFilePool = $this->getMock(\Magento\Framework\Config\File\ConfigFilePool::class, [], [], '', false); - $files = [['configKeyOne', 'mergeOne.php'], ['configKeyTwo','mergeTwo.php']]; - $configFilePool - ->expects($this->any()) - ->method('getPath') - ->will($this->returnValueMap($files)); - $configFilePool->expects($this->any()) - ->method('getInitialFilePools') - ->willReturn([]); - $configFilePool - ->expects($this->any()) - ->method('getPaths') - ->willReturn(['configKeyOne' => 'mergeOne.php', 'configKeyTwo' => 'mergeTwo.php']); - $object = new Reader($this->dirList, $this->driverPool, $configFilePool); - $object->load(); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage Key collision - */ - public function testMergingWithDuplicateEndValues() - { - $configFilePool = $this->getMock(\Magento\Framework\Config\File\ConfigFilePool::class, [], [], '', false); - $files = [['configKeyOne', 'config.php'], ['configKeyTwo','duplicateConfig.php']]; - $configFilePool - ->expects($this->any()) - ->method('getPath') - ->will($this->returnValueMap($files)); - $configFilePool->expects($this->any()) - ->method('getInitialFilePools') - ->willReturn([]); - $configFilePool - ->expects($this->any()) - ->method('getPaths') - ->willReturn(['configKeyOne' => 'config.php', 'configKeyTwo' => 'duplicateConfig.php']); - $object = new Reader($this->dirList, $this->driverPool, $configFilePool); - $object->load(); - } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/WriterTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/WriterTest.php index adb43c3b74a30..76301ea44bb79 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/WriterTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/WriterTest.php @@ -3,7 +3,6 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\App\Test\Unit\DeploymentConfig; use Magento\Framework\App\DeploymentConfig; @@ -17,45 +16,71 @@ use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Phrase; +use PHPUnit_Framework_MockObject_MockObject as Mock; /** - * @covers \Magento\Framework\App\DeploymentConfig\Writer * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @package Magento\Framework\App\Test\Unit\DeploymentConfig */ class WriterTest extends \PHPUnit_Framework_TestCase { - /** @var Writer */ + /** + * @var Writer + */ private $object; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** + * @var DeploymentConfig\Reader|Mock + */ private $reader; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** + * @var WriteInterface|Mock + */ private $dirWrite; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** + * @var ReadInterface|Mock + */ private $dirRead; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** + * @var FormatterInterface|Mock + */ protected $formatter; - /** @var ConfigFilePool */ + /** + * @var ConfigFilePool|Mock + */ private $configFilePool; - /** @var DeploymentConfig */ + /** + * @var DeploymentConfig|Mock + */ private $deploymentConfig; - /** @var Filesystem */ + /** + * @var Filesystem|Mock + */ private $filesystem; protected function setUp() { - $this->reader = $this->getMock(Reader::class, [], [], '', false); - $this->filesystem = $this->getMock(Filesystem::class, [], [], '', false); + $this->reader = $this->getMockBuilder(Reader::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystem = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); $this->formatter = $this->getMockForAbstractClass(FormatterInterface::class); - $this->configFilePool = $this->getMock(ConfigFilePool::class, [], [], '', false); - $this->deploymentConfig = $this->getMock(DeploymentConfig::class, [], [], '', false); + $this->configFilePool = $this->getMockBuilder(ConfigFilePool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->deploymentConfig = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dirWrite = $this->getMockForAbstractClass(WriteInterface::class); + $this->dirRead = $this->getMockForAbstractClass(ReadInterface::class); + $this->object = new Writer( $this->reader, $this->filesystem, @@ -63,19 +88,6 @@ protected function setUp() $this->deploymentConfig, $this->formatter ); - $this->reader->expects($this->any())->method('getFiles')->willReturn('test.php'); - $this->dirWrite = $this->getMockForAbstractClass(WriteInterface::class); - $this->dirRead = $this->getMockForAbstractClass(ReadInterface::class); - $this->dirRead->expects($this->any()) - ->method('getAbsolutePath'); - $this->filesystem->expects($this->any()) - ->method('getDirectoryWrite') - ->with(DirectoryList::CONFIG) - ->willReturn($this->dirWrite); - $this->filesystem->expects($this->any()) - ->method('getDirectoryRead') - ->with(DirectoryList::CONFIG) - ->willReturn($this->dirRead); } public function testSaveConfig() @@ -83,7 +95,6 @@ public function testSaveConfig() $configFiles = [ ConfigFilePool::APP_CONFIG => 'config.php' ]; - $testSetExisting = [ ConfigFilePool::APP_CONFIG => [ 'foo' => 'bar', @@ -94,7 +105,6 @@ public function testSaveConfig() ] ], ]; - $testSetUpdate = [ ConfigFilePool::APP_CONFIG => [ 'baz' => [ @@ -102,7 +112,6 @@ public function testSaveConfig() ] ], ]; - $testSetExpected = [ ConfigFilePool::APP_CONFIG => [ 'foo' => 'bar', @@ -114,17 +123,37 @@ public function testSaveConfig() ], ]; - $this->deploymentConfig->expects($this->once())->method('resetData'); - $this->configFilePool->expects($this->once())->method('getPaths')->willReturn($configFiles); - $this->dirWrite->expects($this->any())->method('isExist')->willReturn(true); - $this->reader->expects($this->once())->method('loadConfigFile') + $this->deploymentConfig->expects($this->once()) + ->method('resetData'); + $this->configFilePool->expects($this->once()) + ->method('getPaths') + ->willReturn($configFiles); + $this->dirWrite->expects($this->any()) + ->method('isExist') + ->willReturn(true); + $this->reader->expects($this->once()) + ->method('load') ->willReturn($testSetExisting[ConfigFilePool::APP_CONFIG]); - $this->formatter - ->expects($this->once()) + $this->formatter->expects($this->once()) ->method('format') ->with($testSetExpected[ConfigFilePool::APP_CONFIG]) ->willReturn([]); - $this->dirWrite->expects($this->once())->method('writeFile')->with('config.php', []); + $this->dirWrite->expects($this->once()) + ->method('writeFile') + ->with('config.php', []); + $this->reader->expects($this->any()) + ->method('getFiles') + ->willReturn('test.php'); + $this->dirRead->expects($this->any()) + ->method('getAbsolutePath'); + $this->filesystem->expects($this->any()) + ->method('getDirectoryWrite') + ->with(DirectoryList::CONFIG) + ->willReturn($this->dirWrite); + $this->filesystem->expects($this->any()) + ->method('getDirectoryRead') + ->with(DirectoryList::CONFIG) + ->willReturn($this->dirRead); $this->object->saveConfig($testSetUpdate); } @@ -134,7 +163,6 @@ public function testSaveConfigOverride() $configFiles = [ ConfigFilePool::APP_CONFIG => 'config.php' ]; - $testSetUpdate = [ ConfigFilePool::APP_CONFIG => [ 'baz' => [ @@ -142,7 +170,6 @@ public function testSaveConfigOverride() ] ], ]; - $testSetExpected = [ ConfigFilePool::APP_CONFIG => [ 'baz' => [ @@ -151,15 +178,34 @@ public function testSaveConfigOverride() ], ]; - $this->deploymentConfig->expects($this->once())->method('resetData'); - $this->configFilePool->expects($this->once())->method('getPaths')->willReturn($configFiles); - $this->dirWrite->expects($this->any())->method('isExist')->willReturn(true); - $this->formatter - ->expects($this->once()) + $this->deploymentConfig->expects($this->once()) + ->method('resetData'); + $this->configFilePool->expects($this->once()) + ->method('getPaths') + ->willReturn($configFiles); + $this->dirWrite->expects($this->any()) + ->method('isExist') + ->willReturn(true); + $this->formatter->expects($this->once()) ->method('format') ->with($testSetExpected[ConfigFilePool::APP_CONFIG]) ->willReturn([]); - $this->dirWrite->expects($this->once())->method('writeFile')->with('config.php', []); + $this->dirWrite->expects($this->once()) + ->method('writeFile') + ->with('config.php', []); + $this->reader->expects($this->any()) + ->method('getFiles') + ->willReturn('test.php'); + $this->dirRead->expects($this->any()) + ->method('getAbsolutePath'); + $this->filesystem->expects($this->any()) + ->method('getDirectoryWrite') + ->with(DirectoryList::CONFIG) + ->willReturn($this->dirWrite); + $this->filesystem->expects($this->any()) + ->method('getDirectoryRead') + ->with(DirectoryList::CONFIG) + ->willReturn($this->dirRead); $this->object->saveConfig($testSetUpdate, true); } @@ -170,9 +216,29 @@ public function testSaveConfigOverride() */ public function testSaveConfigException() { - $this->configFilePool->method('getPaths')->willReturn([ConfigFilePool::APP_ENV => 'env.php']); $exception = new FileSystemException(new Phrase('error when writing file config file')); - $this->dirWrite->method('writeFile')->willThrowException($exception); + + $this->configFilePool->method('getPaths') + ->willReturn([ConfigFilePool::APP_ENV => 'env.php']); + $this->dirWrite->method('writeFile') + ->willThrowException($exception); + $this->reader->expects($this->any()) + ->method('getFiles') + ->willReturn('test.php'); + $this->dirRead->expects($this->any()) + ->method('getAbsolutePath'); + $this->filesystem->expects($this->any()) + ->method('getDirectoryWrite') + ->with(DirectoryList::CONFIG) + ->willReturn($this->dirWrite); + $this->filesystem->expects($this->any()) + ->method('getDirectoryRead') + ->with(DirectoryList::CONFIG) + ->willReturn($this->dirRead); + $this->dirWrite->expects($this->any()) + ->method('isExist') + ->willReturn(true); + $this->object->saveConfig([ConfigFilePool::APP_ENV => ['key' => 'value']]); } } diff --git a/lib/internal/Magento/Framework/Config/File/ConfigFilePool.php b/lib/internal/Magento/Framework/Config/File/ConfigFilePool.php index 8f39247426015..ec505a7c6ef27 100644 --- a/lib/internal/Magento/Framework/Config/File/ConfigFilePool.php +++ b/lib/internal/Magento/Framework/Config/File/ConfigFilePool.php @@ -14,7 +14,14 @@ class ConfigFilePool const APP_CONFIG = 'app_config'; const APP_ENV = 'app_env'; + /** + * @deprecated Magento does not support custom config file pools since 2.2.0 version + */ const LOCAL = 'local'; + + /** + * @deprecated Magento does not support custom config file pools since 2.2.0 version + */ const DIST = 'dist'; /** @@ -31,6 +38,7 @@ class ConfigFilePool * Initial files for configuration * * @var array + * @deprecated Magento does not support custom config file pools since 2.2.0 version */ private $initialConfigFiles = [ self::DIST => [ @@ -82,6 +90,7 @@ public function getPath($fileKey) * Returns application initial config files. * * @return array + * @deprecated Magento does not support custom config file pools since 2.2.0 version */ public function getInitialFilePools() { @@ -93,6 +102,7 @@ public function getInitialFilePools() * * @param string $pool * @return array + * @deprecated Magento does not support custom config file pools since 2.2.0 version */ public function getPathsByPool($pool) { diff --git a/lib/internal/Magento/Framework/Locale/AvailableLocalesInterface.php b/lib/internal/Magento/Framework/Locale/AvailableLocalesInterface.php new file mode 100644 index 0000000000000..111232365086a --- /dev/null +++ b/lib/internal/Magento/Framework/Locale/AvailableLocalesInterface.php @@ -0,0 +1,23 @@ +fileSystem = $fileSystem; + $this->flyweightFactory = $flyweightFactory; + } + + /** + * {@inheritdoc} + * + * If theme or file directory for theme static content does not exist then return an empty array. + */ + public function getList($code, $area = DesignInterface::DEFAULT_AREA) + { + try { + $theme = $this->flyweightFactory->create($code, $area); + $reader = $this->fileSystem->getDirectoryRead(DirectoryList::STATIC_VIEW); + $dirs = $reader->read($theme->getFullPath()); + } catch (\Exception $e) { + return []; + } + + return array_map('basename', $dirs); + } +} diff --git a/lib/internal/Magento/Framework/Locale/Deployed/Options.php b/lib/internal/Magento/Framework/Locale/Deployed/Options.php new file mode 100644 index 0000000000000..f2e05407cb87c --- /dev/null +++ b/lib/internal/Magento/Framework/Locale/Deployed/Options.php @@ -0,0 +1,133 @@ +localeLists = $localeLists; + $this->state = $state; + $this->availableLocales = $availableLocales; + $this->design = $design; + } + + /** + * {@inheritdoc} + */ + public function getOptionLocales() + { + return $this->filterLocales($this->localeLists->getOptionLocales()); + } + + /** + * {@inheritdoc} + */ + public function getTranslatedOptionLocales() + { + return $this->filterLocales($this->localeLists->getTranslatedOptionLocales()); + } + + /** + * Filter list of locales by available locales for current theme and depends on running application mode. + * + * Applies filters only in production mode. + * For example, if the current design theme has only one generated locale en_GB then for given array of locales: + * ```php + * $locales = [ + * 0 => [ + * 'value' => 'da_DK' + * 'label' => 'Danish (Denmark)' + * ], + * 1 => [ + * 'value' => 'de_DE' + * 'label' => 'German (Germany)' + * ], + * 2 => [ + * 'value' => 'en_GB' + * 'label' => 'English (United Kingdom)' + * ], + * ] + * ``` + * result will be: + * ```php + * [ + * 2 => [ + * 'value' => 'en_GB' + * 'label' => 'English (United Kingdom)' + * ], + * ] + * ``` + * + * @param array $locales list of locales for filtering + * @return array of filtered locales + */ + private function filterLocales(array $locales) + { + if ($this->state->getMode() != State::MODE_PRODUCTION) { + return $locales; + } + + $theme = $this->design->getDesignTheme(); + try { + $availableLocales = $this->availableLocales->getList($theme->getCode(), $theme->getArea()); + } catch (LocalizedException $e) { + $availableLocales = []; + } + + return array_filter($locales, function ($localeData) use ($availableLocales) { + return in_array($localeData['value'], $availableLocales); + }); + } +} diff --git a/lib/internal/Magento/Framework/Locale/ListsInterface.php b/lib/internal/Magento/Framework/Locale/ListsInterface.php index e94856db84ac6..cc4d3ee3383a5 100644 --- a/lib/internal/Magento/Framework/Locale/ListsInterface.php +++ b/lib/internal/Magento/Framework/Locale/ListsInterface.php @@ -5,22 +5,8 @@ */ namespace Magento\Framework\Locale; -interface ListsInterface +interface ListsInterface extends OptionInterface { - /** - * Get options array for locale dropdown in current locale - * - * @return array - */ - public function getOptionLocales(); - - /** - * Get translated to original locale options array for locale dropdown - * - * @return array - */ - public function getTranslatedOptionLocales(); - /** * Retrieve timezone option list * diff --git a/lib/internal/Magento/Framework/Locale/OptionInterface.php b/lib/internal/Magento/Framework/Locale/OptionInterface.php new file mode 100644 index 0000000000000..0e1b7cd8a9d52 --- /dev/null +++ b/lib/internal/Magento/Framework/Locale/OptionInterface.php @@ -0,0 +1,54 @@ + [ + * 'value' => 'de_DE' + * 'label' => 'German (Germany)' + * ], + * 1 => [ + * 'value' => 'en_GB' + * 'label' => 'English (United Kingdom)' + * ], + * ] + * ``` + * + * @return array + */ + public function getOptionLocales(); + + /** + * Get array of deployed locales with translation. + * + * Function result has next format: + * ```php + * [ + * 0 => [ + * 'value' => 'de_DE' + * 'label' => 'Deutsch (Deutschland) / German (Germany)' + * ], + * 1 => [ + * 'value' => 'en_GB' + * 'label' => 'English (United Kingdom) / English (United Kingdom)' + * ], + * ] + * ``` + * + * @return array + */ + public function getTranslatedOptionLocales(); +} diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/Deployed/CodesTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/Deployed/CodesTest.php new file mode 100644 index 0000000000000..1190188000f40 --- /dev/null +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/Deployed/CodesTest.php @@ -0,0 +1,95 @@ +fileSystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $this->flyweightFactoryMock = $this->getMockBuilder(FlyweightFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new Codes( + $this->flyweightFactoryMock, + $this->fileSystemMock + ); + } + + public function testGetList() + { + $code = 'code'; + $area = 'area'; + $fullPath = 'some/full/path'; + + $themeMock = $this->getMockBuilder(ThemeInterface::class) + ->getMockForAbstractClass(); + $themeMock->expects($this->once()) + ->method('getFullPath') + ->willReturn($fullPath); + $this->flyweightFactoryMock->expects($this->once()) + ->method('create') + ->with($code, $area) + ->willReturn($themeMock); + $reader = $this->getMockBuilder(ReadInterface::class) + ->getMockForAbstractClass(); + $reader->expects($this->once()) + ->method('read') + ->with($fullPath) + ->willReturn([ + $fullPath . '/de_DE', + $fullPath . '/en_US', + $fullPath . '/fr_FR' + ]); + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::STATIC_VIEW) + ->willReturn($reader); + + $this->assertEquals( + [ + 'de_DE', + 'en_US', + 'fr_FR' + ], + $this->model->getList($code, $area) + ); + } +} diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/Deployed/OptionsTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/Deployed/OptionsTest.php new file mode 100644 index 0000000000000..caf1bb91b752e --- /dev/null +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/Deployed/OptionsTest.php @@ -0,0 +1,206 @@ +stateMock = $this->getMockBuilder(State::class) + ->disableOriginalConstructor() + ->getMock(); + $this->availableLocalesMock = $this->getMockBuilder(AvailableLocalesInterface::class) + ->getMockForAbstractClass(); + $this->designMock = $this->getMockBuilder(DesignInterface::class) + ->getMockForAbstractClass(); + $this->localeListsMock = $this->getMockBuilder(ListsInterface::class) + ->getMockForAbstractClass(); + + $this->model = new Options( + $this->localeListsMock, + $this->stateMock, + $this->availableLocalesMock, + $this->designMock + ); + } + + /** + * @param string $mode + * @param array $locales + * @param array $expectedLocales + * @param array $deployedCodes + * @dataProvider getLocaleDataProvider + */ + public function testGetOptionLocales($mode, $locales, $expectedLocales, $deployedCodes) + { + $this->localeListsMock->expects($this->once()) + ->method('getOptionLocales') + ->willReturn($locales); + + $this->prepareGetLocales($mode, $deployedCodes); + + $this->assertEquals($expectedLocales, array_values($this->model->getOptionLocales())); + } + + /** + * @param string $mode + * @param array $locales + * @param array $expectedLocales + * @param array $deployedCodes + * @dataProvider getLocaleDataProvider + */ + public function testGetTranslatedOptionLocales($mode, $locales, $expectedLocales, $deployedCodes) + { + $this->localeListsMock->expects($this->once()) + ->method('getTranslatedOptionLocales') + ->willReturn($locales); + + $this->prepareGetLocales($mode, $deployedCodes); + + $this->assertEquals($expectedLocales, array_values($this->model->getTranslatedOptionLocales())); + } + + /** + * @param $mode + * @param $deployedCodes + * @return void + */ + private function prepareGetLocales($mode, $deployedCodes) + { + $this->stateMock->expects($this->once()) + ->method('getMode') + ->willReturn($mode); + + if ($mode == State::MODE_PRODUCTION) { + $area = 'area'; + $code = 'code'; + $themeMock = $this->getMockBuilder(ThemeInterface::class) + ->getMockForAbstractClass(); + $themeMock->expects($this->once()) + ->method('getCode') + ->willReturn($code); + $themeMock->expects($this->once()) + ->method('getArea') + ->willReturn($area); + $this->designMock->expects($this->once()) + ->method('getDesignTheme') + ->willReturn($themeMock); + $this->availableLocalesMock->expects($this->once()) + ->method('getList') + ->with($code, $area) + ->willReturn($deployedCodes); + } + } + + /** + * @return array + */ + public function getLocaleDataProvider() + { + return [ + [ + State::MODE_PRODUCTION, + [ + [ + 'value' => 'da_DK', + 'label' => 'Danish (Denmark)' + ], + [ + 'value' => 'de_DE', + 'label' => 'German (German)' + ] + ], + [ + [ + 'value' => 'de_DE', + 'label' => 'German (German)' + ] + ], + [ + 'de_DE' + ] + ], + [ + State::MODE_PRODUCTION, + [ + [ + 'value' => 'de_DE', + 'label' => 'German (German)' + ] + ], + [], + [] + ], + [ + State::MODE_DEVELOPER, + [ + [ + 'value' => 'da_DK', + 'label' => 'Danish (Denmark)' + ], + [ + 'value' => 'de_DE', + 'label' => 'German (German)' + ] + ], + [ + [ + 'value' => 'da_DK', + 'label' => 'Danish (Denmark)' + ], + [ + 'value' => 'de_DE', + 'label' => 'German (German)' + ] + ], + [ + 'de_DE' + ] + ], + ]; + } +} diff --git a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php index 4d485fc93368d..d4e4be4a58790 100644 --- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php @@ -7,9 +7,13 @@ use Magento\Framework\Setup\ConsoleLogger; use Magento\Setup\Model\InstallerFactory; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Magento\Deploy\Console\Command\App\ConfigImportCommand; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; /** * Command for updating installed application after the code base has changed @@ -28,14 +32,21 @@ class UpgradeCommand extends AbstractSetupCommand */ private $installerFactory; + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + /** * Constructor * * @param InstallerFactory $installerFactory + * @param DeploymentConfig $deploymentConfig */ - public function __construct(InstallerFactory $installerFactory) + public function __construct(InstallerFactory $installerFactory, DeploymentConfig $deploymentConfig = null) { $this->installerFactory = $installerFactory; + $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); parent::__construct(); } @@ -65,13 +76,25 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $keepGenerated = $input->getOption(self::INPUT_KEY_KEEP_GENERATED); - $installer = $this->installerFactory->create(new ConsoleLogger($output)); - $installer->updateModulesSequence($keepGenerated); - $installer->installSchema(); - $installer->installDataFixtures(); - if (!$keepGenerated) { - $output->writeln('Please re-run Magento compile command. Use the command "setup:di:compile"'); + try { + $keepGenerated = $input->getOption(self::INPUT_KEY_KEEP_GENERATED); + $installer = $this->installerFactory->create(new ConsoleLogger($output)); + $installer->updateModulesSequence($keepGenerated); + $installer->installSchema(); + $installer->installDataFixtures(); + if (!$keepGenerated) { + $output->writeln( + 'Please re-run Magento compile command. Use the command "setup:di:compile"' + ); + } + + if ($this->deploymentConfig->isAvailable()) { + $importConfigCommand = $this->getApplication()->find(ConfigImportCommand::COMMAND_NAME); + $importConfigCommand->run(new ArrayInput([]), $output); + } + } catch (\Exception $e) { + $output->writeln($e->getMessage()); + return \Magento\Framework\Console\Cli::RETURN_FAILURE; } return \Magento\Framework\Console\Cli::RETURN_SUCCESS; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/UpgradeCommandTest.php b/setup/src/Magento/Setup/Test/Unit/Console/Command/UpgradeCommandTest.php index 2277a4b0a1bc0..1119d08c57086 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/UpgradeCommandTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Console/Command/UpgradeCommandTest.php @@ -8,25 +8,73 @@ use Magento\Setup\Console\Command\UpgradeCommand; use Symfony\Component\Console\Tester\CommandTester; use Magento\Framework\Console\Cli; +use Magento\Framework\App\DeploymentConfig; +use Magento\Setup\Model\InstallerFactory; +use Magento\Setup\Model\Installer; class UpgradeCommandTest extends \PHPUnit_Framework_TestCase { /** - * @param array $options - * @param string $expectedString + * @var DeploymentConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $deploymentConfigMock; + + /** + * @var InstallerFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $installerFactoryMock; + + /** + * @var Installer|\PHPUnit_Framework_MockObject_MockObject + */ + private $installerMock; + + /** + * @var UpgradeCommand + */ + private $upgradeCommand; + + /** + * @var CommandTester + */ + private $commandTester; + + /** + * @return void + */ + protected function setUp() + { + $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->installerFactoryMock = $this->getMockBuilder(InstallerFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->installerMock = $this->getMockBuilder(Installer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->installerFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->installerMock); + + $this->upgradeCommand = new UpgradeCommand($this->installerFactoryMock, $this->deploymentConfigMock); + $this->commandTester = new CommandTester($this->upgradeCommand); + } + + /** * @dataProvider executeDataProvider */ - public function testExecute($options = [], $expectedString = '') + public function testExecute($options, $expectedString = '') { - $installerFactory = $this->getMock(\Magento\Setup\Model\InstallerFactory::class, [], [], '', false); - $installer = $this->getMock(\Magento\Setup\Model\Installer::class, [], [], '', false); - $installer->expects($this->at(0))->method('updateModulesSequence'); - $installer->expects($this->at(1))->method('installSchema'); - $installer->expects($this->at(2))->method('installDataFixtures'); - $installerFactory->expects($this->once())->method('create')->willReturn($installer); - $commandTester = new CommandTester(new UpgradeCommand($installerFactory)); - $this->assertSame(Cli::RETURN_SUCCESS, $commandTester->execute($options)); - $this->assertEquals($expectedString, $commandTester->getDisplay()); + $this->installerMock->expects($this->at(0)) + ->method('updateModulesSequence'); + $this->installerMock->expects($this->at(1)) + ->method('installSchema'); + $this->installerMock->expects($this->at(2)) + ->method('installDataFixtures'); + + $this->assertSame(Cli::RETURN_SUCCESS, $this->commandTester->execute($options)); + $this->assertEquals($expectedString, $this->commandTester->getDisplay()); } /**