LayoutProcessor.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  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\Checkout\Helper\Data;
  8. use Magento\Framework\App\ObjectManager;
  9. use Magento\Store\Model\StoreManagerInterface;
  10. /**
  11. * Class LayoutProcessor
  12. */
  13. class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcessorInterface
  14. {
  15. /**
  16. * @var \Magento\Customer\Model\AttributeMetadataDataProvider
  17. */
  18. private $attributeMetadataDataProvider;
  19. /**
  20. * @var \Magento\Ui\Component\Form\AttributeMapper
  21. */
  22. protected $attributeMapper;
  23. /**
  24. * @var AttributeMerger
  25. */
  26. protected $merger;
  27. /**
  28. * @var \Magento\Customer\Model\Options
  29. */
  30. private $options;
  31. /**
  32. * @var Data
  33. */
  34. private $checkoutDataHelper;
  35. /**
  36. * @var StoreManagerInterface
  37. */
  38. private $storeManager;
  39. /**
  40. * @var \Magento\Shipping\Model\Config
  41. */
  42. private $shippingConfig;
  43. /**
  44. * @param \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider
  45. * @param \Magento\Ui\Component\Form\AttributeMapper $attributeMapper
  46. * @param AttributeMerger $merger
  47. * @param \Magento\Customer\Model\Options|null $options
  48. * @param Data|null $checkoutDataHelper
  49. * @param \Magento\Shipping\Model\Config|null $shippingConfig
  50. * @param StoreManagerInterface|null $storeManager
  51. */
  52. public function __construct(
  53. \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider,
  54. \Magento\Ui\Component\Form\AttributeMapper $attributeMapper,
  55. AttributeMerger $merger,
  56. \Magento\Customer\Model\Options $options = null,
  57. Data $checkoutDataHelper = null,
  58. \Magento\Shipping\Model\Config $shippingConfig = null,
  59. StoreManagerInterface $storeManager = null
  60. ) {
  61. $this->attributeMetadataDataProvider = $attributeMetadataDataProvider;
  62. $this->attributeMapper = $attributeMapper;
  63. $this->merger = $merger;
  64. $this->options = $options ?: \Magento\Framework\App\ObjectManager::getInstance()
  65. ->get(\Magento\Customer\Model\Options::class);
  66. $this->checkoutDataHelper = $checkoutDataHelper ?: \Magento\Framework\App\ObjectManager::getInstance()
  67. ->get(Data::class);
  68. $this->shippingConfig = $shippingConfig ?: \Magento\Framework\App\ObjectManager::getInstance()
  69. ->get(\Magento\Shipping\Model\Config::class);
  70. $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance()
  71. ->get(StoreManagerInterface::class);
  72. }
  73. /**
  74. * Get address attributes.
  75. *
  76. * @return array
  77. */
  78. private function getAddressAttributes()
  79. {
  80. /** @var \Magento\Eav\Api\Data\AttributeInterface[] $attributes */
  81. $attributes = $this->attributeMetadataDataProvider->loadAttributesCollection(
  82. 'customer_address',
  83. 'customer_register_address'
  84. );
  85. $elements = [];
  86. foreach ($attributes as $attribute) {
  87. $code = $attribute->getAttributeCode();
  88. if ($attribute->getIsUserDefined()) {
  89. continue;
  90. }
  91. $elements[$code] = $this->attributeMapper->map($attribute);
  92. if (isset($elements[$code]['label'])) {
  93. $label = $elements[$code]['label'];
  94. $elements[$code]['label'] = __($label);
  95. }
  96. }
  97. return $elements;
  98. }
  99. /**
  100. * Convert elements(like prefix and suffix) from inputs to selects when necessary
  101. *
  102. * @param array $elements address attributes
  103. * @param array $attributesToConvert fields and their callbacks
  104. * @return array
  105. */
  106. private function convertElementsToSelect($elements, $attributesToConvert)
  107. {
  108. $codes = array_keys($attributesToConvert);
  109. foreach (array_keys($elements) as $code) {
  110. if (!in_array($code, $codes)) {
  111. continue;
  112. }
  113. $options = call_user_func($attributesToConvert[$code]);
  114. if (!is_array($options)) {
  115. continue;
  116. }
  117. $elements[$code]['dataType'] = 'select';
  118. $elements[$code]['formElement'] = 'select';
  119. foreach ($options as $key => $value) {
  120. $elements[$code]['options'][] = [
  121. 'value' => $key,
  122. 'label' => $value,
  123. ];
  124. }
  125. }
  126. return $elements;
  127. }
  128. /**
  129. * Process js Layout of block
  130. *
  131. * @param array $jsLayout
  132. * @return array
  133. */
  134. public function process($jsLayout)
  135. {
  136. $attributesToConvert = [
  137. 'prefix' => [$this->options, 'getNamePrefixOptions'],
  138. 'suffix' => [$this->options, 'getNameSuffixOptions'],
  139. ];
  140. $elements = $this->getAddressAttributes();
  141. $elements = $this->convertElementsToSelect($elements, $attributesToConvert);
  142. // The following code is a workaround for custom address attributes
  143. if (isset($jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
  144. ['payment']['children'])) {
  145. $jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
  146. ['payment']['children'] = $this->processPaymentChildrenComponents(
  147. $jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
  148. ['payment']['children'],
  149. $elements
  150. );
  151. }
  152. if (isset($jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
  153. ['step-config']['children']['shipping-rates-validation']['children'])) {
  154. $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
  155. ['step-config']['children']['shipping-rates-validation']['children'] =
  156. $this->processShippingChildrenComponents(
  157. $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
  158. ['step-config']['children']['shipping-rates-validation']['children']
  159. );
  160. }
  161. if (isset($jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']
  162. ['children']['shippingAddress']['children']['shipping-address-fieldset']['children'])) {
  163. $fields = $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']
  164. ['children']['shippingAddress']['children']['shipping-address-fieldset']['children'];
  165. $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']
  166. ['children']['shippingAddress']['children']['shipping-address-fieldset']['children'] = $this->merger->merge(
  167. $elements,
  168. 'checkoutProvider',
  169. 'shippingAddress',
  170. $fields
  171. );
  172. }
  173. return $jsLayout;
  174. }
  175. /**
  176. * Process shipping configuration to exclude inactive carriers.
  177. *
  178. * @param array $shippingRatesLayout
  179. * @return array
  180. */
  181. private function processShippingChildrenComponents($shippingRatesLayout)
  182. {
  183. $activeCarriers = $this->shippingConfig->getActiveCarriers(
  184. $this->storeManager->getStore()->getId()
  185. );
  186. foreach (array_keys($shippingRatesLayout) as $carrierName) {
  187. $carrierKey = str_replace('-rates-validation', '', $carrierName);
  188. if (!array_key_exists($carrierKey, $activeCarriers)) {
  189. unset($shippingRatesLayout[$carrierName]);
  190. }
  191. }
  192. return $shippingRatesLayout;
  193. }
  194. /**
  195. * Appends billing address form component to payment layout
  196. *
  197. * @param array $paymentLayout
  198. * @param array $elements
  199. * @return array
  200. */
  201. private function processPaymentChildrenComponents(array $paymentLayout, array $elements)
  202. {
  203. if (!isset($paymentLayout['payments-list']['children'])) {
  204. $paymentLayout['payments-list']['children'] = [];
  205. }
  206. if (!isset($paymentLayout['afterMethods']['children'])) {
  207. $paymentLayout['afterMethods']['children'] = [];
  208. }
  209. // The if billing address should be displayed on Payment method or page
  210. if ($this->checkoutDataHelper->isDisplayBillingOnPaymentMethodAvailable()) {
  211. $paymentLayout['payments-list']['children'] =
  212. array_merge_recursive(
  213. $paymentLayout['payments-list']['children'],
  214. $this->processPaymentConfiguration(
  215. $paymentLayout['renders']['children'],
  216. $elements
  217. )
  218. );
  219. } else {
  220. $component['billing-address-form'] = $this->getBillingAddressComponent('shared', $elements);
  221. $paymentLayout['afterMethods']['children'] =
  222. array_merge_recursive(
  223. $component,
  224. $paymentLayout['afterMethods']['children']
  225. );
  226. }
  227. return $paymentLayout;
  228. }
  229. /**
  230. * Inject billing address component into every payment component
  231. *
  232. * @param array $configuration list of payment components
  233. * @param array $elements attributes that must be displayed in address form
  234. * @return array
  235. */
  236. private function processPaymentConfiguration(array &$configuration, array $elements)
  237. {
  238. $output = [];
  239. foreach ($configuration as $paymentGroup => $groupConfig) {
  240. foreach ($groupConfig['methods'] as $paymentCode => $paymentComponent) {
  241. if (empty($paymentComponent['isBillingAddressRequired'])) {
  242. continue;
  243. }
  244. $output[$paymentCode . '-form'] = $this->getBillingAddressComponent($paymentCode, $elements);
  245. }
  246. unset($configuration[$paymentGroup]['methods']);
  247. }
  248. return $output;
  249. }
  250. /**
  251. * Gets billing address component details
  252. *
  253. * @param string $paymentCode
  254. * @param array $elements
  255. * @return array
  256. */
  257. private function getBillingAddressComponent($paymentCode, $elements)
  258. {
  259. return [
  260. 'component' => 'Magento_Checkout/js/view/billing-address',
  261. 'displayArea' => 'billing-address-form-' . $paymentCode,
  262. 'provider' => 'checkoutProvider',
  263. 'deps' => 'checkoutProvider',
  264. 'dataScopePrefix' => 'billingAddress' . $paymentCode,
  265. 'sortOrder' => 1,
  266. 'children' => [
  267. 'form-fields' => [
  268. 'component' => 'uiComponent',
  269. 'displayArea' => 'additional-fieldsets',
  270. 'children' => $this->merger->merge(
  271. $elements,
  272. 'checkoutProvider',
  273. 'billingAddress' . $paymentCode,
  274. [
  275. 'country_id' => [
  276. 'sortOrder' => 115,
  277. ],
  278. 'region' => [
  279. 'visible' => false,
  280. ],
  281. 'region_id' => [
  282. 'component' => 'Magento_Ui/js/form/element/region',
  283. 'config' => [
  284. 'template' => 'ui/form/field',
  285. 'elementTmpl' => 'ui/form/element/select',
  286. 'customEntry' => 'billingAddress' . $paymentCode . '.region',
  287. ],
  288. 'validation' => [
  289. 'required-entry' => true,
  290. ],
  291. 'filterBy' => [
  292. 'target' => '${ $.provider }:${ $.parentScope }.country_id',
  293. 'field' => 'country_id',
  294. ],
  295. ],
  296. 'postcode' => [
  297. 'component' => 'Magento_Ui/js/form/element/post-code',
  298. 'validation' => [
  299. 'required-entry' => true,
  300. ],
  301. ],
  302. 'company' => [
  303. 'validation' => [
  304. 'min_text_length' => 0,
  305. ],
  306. ],
  307. 'fax' => [
  308. 'validation' => [
  309. 'min_text_length' => 0,
  310. ],
  311. ],
  312. 'telephone' => [
  313. 'config' => [
  314. 'tooltip' => [
  315. 'description' => __('For delivery questions.'),
  316. ],
  317. ],
  318. ],
  319. ]
  320. ),
  321. ],
  322. ],
  323. ];
  324. }
  325. }