AttributeMerger.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Checkout\Block\Checkout;
  7. use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository;
  8. use Magento\Customer\Api\Data\CustomerInterface;
  9. use Magento\Customer\Helper\Address as AddressHelper;
  10. use Magento\Customer\Model\Session;
  11. use Magento\Directory\Helper\Data as DirectoryHelper;
  12. use Magento\Framework\Exception\LocalizedException;
  13. use Magento\Framework\Exception\NoSuchEntityException;
  14. /**
  15. * Fields attribute merger.
  16. *
  17. * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
  18. */
  19. class AttributeMerger
  20. {
  21. /**
  22. * Map form element
  23. *
  24. * @var array
  25. */
  26. protected $formElementMap = [
  27. 'checkbox' => 'Magento_Ui/js/form/element/select',
  28. 'select' => 'Magento_Ui/js/form/element/select',
  29. 'textarea' => 'Magento_Ui/js/form/element/textarea',
  30. 'multiline' => 'Magento_Ui/js/form/components/group',
  31. 'multiselect' => 'Magento_Ui/js/form/element/multiselect',
  32. 'image' => 'Magento_Ui/js/form/element/media',
  33. 'file' => 'Magento_Ui/js/form/element/media',
  34. ];
  35. /**
  36. * Map template
  37. *
  38. * @var array
  39. */
  40. protected $templateMap = [
  41. 'image' => 'media',
  42. 'file' => 'media',
  43. ];
  44. /**
  45. * Map input_validation and validation rule from js
  46. *
  47. * @var array
  48. */
  49. protected $inputValidationMap = [
  50. 'alpha' => 'validate-alpha',
  51. 'numeric' => 'validate-number',
  52. 'alphanumeric' => 'validate-alphanum',
  53. 'alphanum-with-spaces' => 'validate-alphanum-with-spaces',
  54. 'url' => 'validate-url',
  55. 'email' => 'email2',
  56. 'length' => 'validate-length',
  57. ];
  58. /**
  59. * @var AddressHelper
  60. */
  61. private $addressHelper;
  62. /**
  63. * @var Session
  64. */
  65. private $customerSession;
  66. /**
  67. * @var CustomerRepository
  68. */
  69. private $customerRepository;
  70. /**
  71. * @var CustomerInterface
  72. */
  73. private $customer;
  74. /**
  75. * @var \Magento\Directory\Helper\Data
  76. */
  77. private $directoryHelper;
  78. /**
  79. * List of codes of countries that must be shown on the top of country list
  80. *
  81. * @var array
  82. */
  83. private $topCountryCodes;
  84. /**
  85. * @param AddressHelper $addressHelper
  86. * @param Session $customerSession
  87. * @param CustomerRepository $customerRepository
  88. * @param DirectoryHelper $directoryHelper
  89. */
  90. public function __construct(
  91. AddressHelper $addressHelper,
  92. Session $customerSession,
  93. CustomerRepository $customerRepository,
  94. DirectoryHelper $directoryHelper
  95. ) {
  96. $this->addressHelper = $addressHelper;
  97. $this->customerSession = $customerSession;
  98. $this->customerRepository = $customerRepository;
  99. $this->directoryHelper = $directoryHelper;
  100. $this->topCountryCodes = $directoryHelper->getTopCountryCodes();
  101. }
  102. /**
  103. * Merge additional address fields for given provider
  104. *
  105. * @param array $elements
  106. * @param string $providerName name of the storage container used by UI component
  107. * @param string $dataScopePrefix
  108. * @param array $fields
  109. * @return array
  110. */
  111. public function merge($elements, $providerName, $dataScopePrefix, array $fields = [])
  112. {
  113. foreach ($elements as $attributeCode => $attributeConfig) {
  114. $additionalConfig = isset($fields[$attributeCode]) ? $fields[$attributeCode] : [];
  115. if (!$this->isFieldVisible($attributeCode, $attributeConfig, $additionalConfig)) {
  116. continue;
  117. }
  118. $fields[$attributeCode] = $this->getFieldConfig(
  119. $attributeCode,
  120. $attributeConfig,
  121. $additionalConfig,
  122. $providerName,
  123. $dataScopePrefix
  124. );
  125. }
  126. return $fields;
  127. }
  128. /**
  129. * Retrieve UI field configuration for given attribute
  130. *
  131. * @param string $attributeCode
  132. * @param array $attributeConfig
  133. * @param array $additionalConfig field configuration provided via layout XML
  134. * @param string $providerName name of the storage container used by UI component
  135. * @param string $dataScopePrefix
  136. * @return array
  137. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  138. * @SuppressWarnings(PHPMD.NPathComplexity)
  139. */
  140. protected function getFieldConfig(
  141. $attributeCode,
  142. array $attributeConfig,
  143. array $additionalConfig,
  144. $providerName,
  145. $dataScopePrefix
  146. ) {
  147. // street attribute is unique in terms of configuration, so it has its own configuration builder
  148. if (isset($attributeConfig['validation']['input_validation'])) {
  149. $validationRule = $attributeConfig['validation']['input_validation'];
  150. $attributeConfig['validation'][$this->inputValidationMap[$validationRule]] = true;
  151. unset($attributeConfig['validation']['input_validation']);
  152. }
  153. if ($attributeConfig['formElement'] == 'multiline') {
  154. return $this->getMultilineFieldConfig($attributeCode, $attributeConfig, $providerName, $dataScopePrefix);
  155. }
  156. $uiComponent = isset($this->formElementMap[$attributeConfig['formElement']])
  157. ? $this->formElementMap[$attributeConfig['formElement']]
  158. : 'Magento_Ui/js/form/element/abstract';
  159. $elementTemplate = isset($this->templateMap[$attributeConfig['formElement']])
  160. ? 'ui/form/element/' . $this->templateMap[$attributeConfig['formElement']]
  161. : 'ui/form/element/' . $attributeConfig['formElement'];
  162. $element = [
  163. 'component' => isset($additionalConfig['component']) ? $additionalConfig['component'] : $uiComponent,
  164. 'config' => $this->mergeConfigurationNode(
  165. 'config',
  166. $additionalConfig,
  167. [
  168. 'config' => [
  169. // customScope is used to group elements within a single
  170. // form (e.g. they can be validated separately)
  171. 'customScope' => $dataScopePrefix,
  172. 'template' => 'ui/form/field',
  173. 'elementTmpl' => $elementTemplate,
  174. ],
  175. ]
  176. ),
  177. 'dataScope' => $dataScopePrefix . '.' . $attributeCode,
  178. 'label' => $attributeConfig['label'],
  179. 'provider' => $providerName,
  180. 'sortOrder' => isset($additionalConfig['sortOrder'])
  181. ? $additionalConfig['sortOrder']
  182. : $attributeConfig['sortOrder'],
  183. 'validation' => $this->mergeConfigurationNode('validation', $additionalConfig, $attributeConfig),
  184. 'options' => $this->getFieldOptions($attributeCode, $attributeConfig),
  185. 'filterBy' => isset($additionalConfig['filterBy']) ? $additionalConfig['filterBy'] : null,
  186. 'customEntry' => isset($additionalConfig['customEntry']) ? $additionalConfig['customEntry'] : null,
  187. 'visible' => isset($additionalConfig['visible']) ? $additionalConfig['visible'] : true,
  188. ];
  189. if ($attributeCode === 'region_id' || $attributeCode === 'country_id') {
  190. unset($element['options']);
  191. $element['deps'] = [$providerName];
  192. $element['imports'] = [
  193. 'initialOptions' => 'index = ' . $providerName . ':dictionaries.' . $attributeCode,
  194. 'setOptions' => 'index = ' . $providerName . ':dictionaries.' . $attributeCode
  195. ];
  196. }
  197. if (isset($attributeConfig['value']) && $attributeConfig['value'] != null) {
  198. $element['value'] = $attributeConfig['value'];
  199. } elseif (isset($attributeConfig['default']) && $attributeConfig['default'] != null) {
  200. $element['value'] = $attributeConfig['default'];
  201. } else {
  202. $defaultValue = $this->getDefaultValue($attributeCode);
  203. if (null !== $defaultValue) {
  204. $element['value'] = $defaultValue;
  205. }
  206. }
  207. return $element;
  208. }
  209. /**
  210. * Merge two configuration nodes recursively
  211. *
  212. * @param string $nodeName
  213. * @param array $mainSource
  214. * @param array $additionalSource
  215. * @return array
  216. */
  217. protected function mergeConfigurationNode($nodeName, array $mainSource, array $additionalSource)
  218. {
  219. $mainData = isset($mainSource[$nodeName]) ? $mainSource[$nodeName] : [];
  220. $additionalData = isset($additionalSource[$nodeName]) ? $additionalSource[$nodeName] : [];
  221. return array_replace_recursive($additionalData, $mainData);
  222. }
  223. /**
  224. * Check if address attribute is visible on frontend
  225. *
  226. * @param string $attributeCode
  227. * @param array $attributeConfig
  228. * @param array $additionalConfig field configuration provided via layout XML
  229. * @return bool
  230. */
  231. protected function isFieldVisible($attributeCode, array $attributeConfig, array $additionalConfig = [])
  232. {
  233. // TODO move this logic to separate model so it can be customized
  234. if ($attributeConfig['visible'] == false
  235. || (isset($additionalConfig['visible']) && $additionalConfig['visible'] == false)
  236. ) {
  237. return false;
  238. }
  239. if ($attributeCode == 'vat_id' && !$this->addressHelper->isVatAttributeVisible()) {
  240. return false;
  241. }
  242. return true;
  243. }
  244. /**
  245. * Retrieve field configuration for street address attribute
  246. *
  247. * @param string $attributeCode
  248. * @param array $attributeConfig
  249. * @param string $providerName name of the storage container used by UI component
  250. * @param string $dataScopePrefix
  251. * @return array
  252. */
  253. protected function getMultilineFieldConfig($attributeCode, array $attributeConfig, $providerName, $dataScopePrefix)
  254. {
  255. $lines = [];
  256. unset($attributeConfig['validation']['required-entry']);
  257. for ($lineIndex = 0; $lineIndex < (int)$attributeConfig['size']; $lineIndex++) {
  258. $isFirstLine = $lineIndex === 0;
  259. $line = [
  260. 'component' => 'Magento_Ui/js/form/element/abstract',
  261. 'config' => [
  262. // customScope is used to group elements within a single form e.g. they can be validated separately
  263. 'customScope' => $dataScopePrefix,
  264. 'template' => 'ui/form/field',
  265. 'elementTmpl' => 'ui/form/element/input'
  266. ],
  267. 'dataScope' => $lineIndex,
  268. 'provider' => $providerName,
  269. 'validation' => $isFirstLine
  270. ? array_merge(
  271. ['required-entry' => (bool)$attributeConfig['required']],
  272. $attributeConfig['validation']
  273. )
  274. : $attributeConfig['validation'],
  275. 'additionalClasses' => $isFirstLine ? 'field' : 'additional'
  276. ];
  277. if ($isFirstLine && isset($attributeConfig['default']) && $attributeConfig['default'] != null) {
  278. $line['value'] = $attributeConfig['default'];
  279. }
  280. $lines[] = $line;
  281. }
  282. return [
  283. 'component' => 'Magento_Ui/js/form/components/group',
  284. 'label' => $attributeConfig['label'],
  285. 'required' => (bool)$attributeConfig['required'],
  286. 'dataScope' => $dataScopePrefix . '.' . $attributeCode,
  287. 'provider' => $providerName,
  288. 'sortOrder' => $attributeConfig['sortOrder'],
  289. 'type' => 'group',
  290. 'config' => [
  291. 'template' => 'ui/group/group',
  292. 'additionalClasses' => $attributeCode
  293. ],
  294. 'children' => $lines,
  295. ];
  296. }
  297. /**
  298. * Returns default attribute value.
  299. *
  300. * @param string $attributeCode
  301. * @throws NoSuchEntityException
  302. * @throws LocalizedException
  303. * @return null|string
  304. */
  305. protected function getDefaultValue($attributeCode): ?string
  306. {
  307. if ($attributeCode === 'country_id') {
  308. return $this->directoryHelper->getDefaultCountry();
  309. }
  310. $customer = $this->getCustomer();
  311. if ($customer === null) {
  312. return null;
  313. }
  314. $attributeValue = null;
  315. switch ($attributeCode) {
  316. case 'prefix':
  317. $attributeValue = $customer->getPrefix();
  318. break;
  319. case 'firstname':
  320. $attributeValue = $customer->getFirstname();
  321. break;
  322. case 'middlename':
  323. $attributeValue = $customer->getMiddlename();
  324. break;
  325. case 'lastname':
  326. $attributeValue = $customer->getLastname();
  327. break;
  328. case 'suffix':
  329. $attributeValue = $customer->getSuffix();
  330. break;
  331. }
  332. return $attributeValue;
  333. }
  334. /**
  335. * Returns logged customer.
  336. *
  337. * @throws NoSuchEntityException
  338. * @throws LocalizedException
  339. * @return CustomerInterface|null
  340. */
  341. protected function getCustomer(): ?CustomerInterface
  342. {
  343. if (!$this->customer) {
  344. if ($this->customerSession->isLoggedIn()) {
  345. $this->customer = $this->customerRepository->getById($this->customerSession->getCustomerId());
  346. } else {
  347. return null;
  348. }
  349. }
  350. return $this->customer;
  351. }
  352. /**
  353. * Retrieve field options from attribute configuration
  354. *
  355. * @param string $attributeCode
  356. * @param array $attributeConfig
  357. * @return array
  358. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  359. */
  360. protected function getFieldOptions($attributeCode, array $attributeConfig)
  361. {
  362. return isset($attributeConfig['options']) ? $attributeConfig['options'] : [];
  363. }
  364. /**
  365. * Order country options. Move top countries to the beginning of the list.
  366. *
  367. * @param array $countryOptions
  368. * @return array
  369. * @deprecated 100.1.7
  370. */
  371. protected function orderCountryOptions(array $countryOptions)
  372. {
  373. if (empty($this->topCountryCodes)) {
  374. return $countryOptions;
  375. }
  376. $headOptions = [];
  377. $tailOptions = [[
  378. 'value' => 'delimiter',
  379. 'label' => '──────────',
  380. 'disabled' => true,
  381. ]];
  382. foreach ($countryOptions as $countryOption) {
  383. if (empty($countryOption['value']) || in_array($countryOption['value'], $this->topCountryCodes)) {
  384. $headOptions[] = $countryOption;
  385. } else {
  386. $tailOptions[] = $countryOption;
  387. }
  388. }
  389. return array_merge($headOptions, $tailOptions);
  390. }
  391. }