Skip to content

Commit ab45e11

Browse files
committed
Issue #2844917 by bojanz: Create a commerce_profile_select form element and field widget
1 parent ddb4287 commit ab45e11

File tree

17 files changed

+442
-181
lines changed

17 files changed

+442
-181
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"license": "GPL-2.0+",
77
"require": {
88
"drupal/core": "~8.2",
9-
"drupal/address": "~1.0",
9+
"drupal/address": "1.x-dev",
1010
"drupal/entity": "1.x-dev",
1111
"drupal/entity_reference_revisions": "~1.0",
1212
"drupal/inline_entity_form": "~1.0",

modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php

+9-24
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane;
44

55
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
6-
use Drupal\Core\Entity\Entity\EntityFormDisplay;
76
use Drupal\Core\Entity\EntityTypeManagerInterface;
87
use Drupal\Core\Form\FormStateInterface;
98
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
@@ -90,6 +89,7 @@ public function buildPaneSummary() {
9089
* {@inheritdoc}
9190
*/
9291
public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
92+
$store = $this->order->getStore();
9393
$billing_profile = $this->order->getBillingProfile();
9494
if (!$billing_profile) {
9595
$profile_storage = $this->entityTypeManager->getStorage('profile');
@@ -98,37 +98,22 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state,
9898
'uid' => $this->order->getCustomerId(),
9999
]);
100100
}
101-
$form_display = EntityFormDisplay::collectRenderDisplay($billing_profile, 'default');
102-
$form_display->buildForm($billing_profile, $pane_form, $form_state);
103-
// Remove the details wrapper from the address field.
104-
if (!empty($pane_form['address']['widget'][0])) {
105-
$pane_form['address']['widget'][0]['#type'] = 'container';
106-
}
107-
// Store the billing profile for the validate/submit methods.
108-
$pane_form['#billing_profile'] = $billing_profile;
109101

110-
return $pane_form;
111-
}
102+
$pane_form['profile'] = [
103+
'#type' => 'commerce_profile_select',
104+
'#default_value' => $billing_profile,
105+
'#default_country' => $store->getAddress()->getCountryCode(),
106+
'#available_countries' => $store->getBillingCountries(),
107+
];
112108

113-
/**
114-
* {@inheritdoc}
115-
*/
116-
public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
117-
$billing_profile = clone $pane_form['#billing_profile'];
118-
$form_display = EntityFormDisplay::collectRenderDisplay($billing_profile, 'default');
119-
$form_display->extractFormValues($billing_profile, $pane_form, $form_state);
120-
$form_display->validateFormValues($billing_profile, $pane_form, $form_state);
109+
return $pane_form;
121110
}
122111

123112
/**
124113
* {@inheritdoc}
125114
*/
126115
public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
127-
$billing_profile = clone $pane_form['#billing_profile'];
128-
$form_display = EntityFormDisplay::collectRenderDisplay($billing_profile, 'default');
129-
$form_display->extractFormValues($billing_profile, $pane_form, $form_state);
130-
$billing_profile->save();
131-
$this->order->setBillingProfile($billing_profile);
116+
$this->order->setBillingProfile($pane_form['profile']['#profile']);
132117
}
133118

134119
}

modules/checkout/tests/src/Functional/CheckoutOrderTest.php

+24-12
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ class CheckoutOrderTest extends CommerceBrowserTestBase {
3434
*/
3535
protected $product;
3636

37+
/**
38+
* The store.
39+
*
40+
* @var \Drupal\commerce_store\Entity\StoreInterface
41+
*/
42+
protected $store;
43+
3744
/**
3845
* Modules to enable.
3946
*
@@ -65,7 +72,7 @@ protected function setUp() {
6572

6673
$this->placeBlock('commerce_cart');
6774

68-
$store = $this->createStore('Demo', '[email protected]', 'default', TRUE);
75+
$this->store = $this->createStore('Demo', '[email protected]', 'default', TRUE);
6976

7077
$variation = $this->createEntity('commerce_product_variation', [
7178
'type' => 'default',
@@ -81,7 +88,7 @@ protected function setUp() {
8188
'type' => 'default',
8289
'title' => 'My product',
8390
'variations' => [$variation],
84-
'stores' => [$store],
91+
'stores' => [$this->store],
8592
]);
8693
}
8794

@@ -117,6 +124,7 @@ public function testOrderAccess() {
117124
$order_item->save();
118125
$order = Order::create([
119126
'type' => 'default',
127+
'store_id' => $this->store,
120128
'state' => 'in_checkout',
121129
'order_number' => '6',
122130
'mail' => '[email protected]',
@@ -171,11 +179,13 @@ public function testGuestOrderCheckout() {
171179
$this->submitForm([
172180
'contact_information[email]' => '[email protected]',
173181
'contact_information[email_confirm]' => '[email protected]',
174-
'billing_information[address][0][given_name]' => $this->randomString(),
175-
'billing_information[address][0][family_name]' => $this->randomString(),
176-
'billing_information[address][0][organization]' => $this->randomString(),
177-
'billing_information[address][0][address_line1]' => $this->randomString(),
178-
'billing_information[address][0][locality]' => $this->randomString(),
182+
'billing_information[profile][address][0][address][given_name]' => $this->randomString(),
183+
'billing_information[profile][address][0][address][family_name]' => $this->randomString(),
184+
'billing_information[profile][address][0][address][organization]' => $this->randomString(),
185+
'billing_information[profile][address][0][address][address_line1]' => $this->randomString(),
186+
'billing_information[profile][address][0][address][postal_code]' => '94043',
187+
'billing_information[profile][address][0][address][locality]' => 'Mountain View',
188+
'billing_information[profile][address][0][address][administrative_area]' => 'CA',
179189
], 'Continue to review');
180190
$this->assertSession()->pageTextContains('Contact information');
181191
$this->assertSession()->pageTextContains('Billing information');
@@ -195,11 +205,13 @@ public function testGuestOrderCheckout() {
195205
$this->submitForm([
196206
'contact_information[email]' => '[email protected]',
197207
'contact_information[email_confirm]' => '[email protected]',
198-
'billing_information[address][0][given_name]' => $this->randomString(),
199-
'billing_information[address][0][family_name]' => $this->randomString(),
200-
'billing_information[address][0][organization]' => $this->randomString(),
201-
'billing_information[address][0][address_line1]' => $this->randomString(),
202-
'billing_information[address][0][locality]' => $this->randomString(),
208+
'billing_information[profile][address][0][address][given_name]' => $this->randomString(),
209+
'billing_information[profile][address][0][address][family_name]' => $this->randomString(),
210+
'billing_information[profile][address][0][address][organization]' => $this->randomString(),
211+
'billing_information[profile][address][0][address][address_line1]' => $this->randomString(),
212+
'billing_information[profile][address][0][address][postal_code]' => '94043',
213+
'billing_information[profile][address][0][address][locality]' => 'Mountain View',
214+
'billing_information[profile][address][0][address][administrative_area]' => 'CA',
203215
], 'Continue to review');
204216
$this->assertSession()->pageTextContains('Contact information');
205217
$this->assertSession()->pageTextContains('Billing information');

modules/order/commerce_order.post_update.php

+31
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,34 @@ function commerce_order_post_update_4() {
118118

119119
return $message;
120120
}
121+
122+
/**
123+
* Revert the Order entity form display.
124+
*/
125+
function commerce_order_post_update_5() {
126+
/** @var \Drupal\commerce\Config\ConfigUpdaterInterface $config_updater */
127+
$config_updater = \Drupal::service('commerce.config_updater');
128+
129+
$views = [
130+
'core.entity_form_display.commerce_order.default.default',
131+
];
132+
$result = $config_updater->revert($views, FALSE);
133+
134+
$success_results = $result->getSucceeded();
135+
$failure_results = $result->getFailed();
136+
if ($success_results) {
137+
$message = t('Succeeded:') . '<br>';
138+
foreach ($success_results as $success_message) {
139+
$message .= $success_message . '<br>';
140+
}
141+
$message .= '<br>';
142+
}
143+
if ($failure_results) {
144+
$message .= t('Failed:') . '<br>';
145+
foreach ($failure_results as $failure_message) {
146+
$message .= $failure_message . '<br>';
147+
}
148+
}
149+
150+
return $message;
151+
}

modules/order/commerce_order.services.yml

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ services:
2626
tags:
2727
- { name: commerce_order.order_processor, priority: 100 }
2828

29+
commerce_order.profile_label_subscriber:
30+
class: Drupal\commerce_order\EventSubscriber\ProfileLabelSubscriber
31+
arguments: ['@entity_type.manager']
32+
tags:
33+
- { name: event_subscriber }
34+
2935
commerce_order.timestamp_event_subscriber:
3036
class: Drupal\commerce_order\EventSubscriber\TimestampEventSubscriber
3137
arguments: ['@entity_type.manager']

modules/order/config/install/core.entity_form_display.commerce_order.default.default.yml

+13-6
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,40 @@ dependencies:
55
- commerce_order.commerce_order_type.default
66
- field.field.commerce_order.default.order_items
77
module:
8+
- commerce_order
89
- inline_entity_form
910
id: commerce_order.default.default
1011
targetEntityType: commerce_order
1112
bundle: default
1213
mode: default
1314
content:
15+
adjustments:
16+
type: commerce_adjustment_default
17+
weight: 2
18+
settings: { }
19+
third_party_settings: { }
1420
billing_profile:
15-
type: options_select
21+
type: commerce_billing_profile
1622
weight: 0
1723
settings: { }
1824
third_party_settings: { }
1925
order_items:
2026
type: inline_entity_form_complex
21-
weight: 0
27+
weight: 1
2228
settings:
2329
override_labels: true
2430
label_singular: 'order item'
2531
label_plural: 'order items'
2632
allow_new: true
2733
match_operator: CONTAINS
2834
allow_existing: false
35+
form_mode: default
2936
third_party_settings: { }
3037
hidden:
31-
mail: true
32-
store_id: true
33-
uid: true
38+
created: true
3439
ip_address: true
40+
mail: true
3541
order_number: true
3642
state: true
37-
created: true
43+
store_id: true
44+
uid: true
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
namespace Drupal\commerce_order\Element;
4+
5+
use Drupal\commerce\Element\CommerceElementBase;
6+
use Drupal\Core\Entity\Entity\EntityFormDisplay;
7+
use Drupal\Core\Form\FormStateInterface;
8+
use Drupal\profile\Entity\ProfileInterface;
9+
10+
/**
11+
* Provides a form element for selecting a customer profile.
12+
*
13+
* Usage example:
14+
* @code
15+
* $form['billing_profile'] = [
16+
* '#type' => 'commerce_profile_select',
17+
* '#default_value' => $profile,
18+
* '#default_country' => 'FR',
19+
* '#available_countries' => ['US', 'FR'],
20+
* ];
21+
* @endcode
22+
* To access the profile in validation or submission callbacks, use
23+
* $form['billing_profile']['#profile']. Due to Drupal core limitations the
24+
* profile can't be accessed via $form_state->getValue('billing_profile').
25+
*
26+
* @RenderElement("commerce_profile_select")
27+
*/
28+
class ProfileSelect extends CommerceElementBase {
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function getInfo() {
34+
$class = get_class($this);
35+
return [
36+
// The country to select if the address widget doesn't have a default.
37+
'#default_country' => NULL,
38+
// A list of country codes. If empty, all countries will be available.
39+
'#available_countries' => [],
40+
41+
// The profile entity operated on. Required.
42+
'#default_value' => NULL,
43+
'#process' => [
44+
[$class, 'attachElementSubmit'],
45+
[$class, 'processForm'],
46+
],
47+
'#element_validate' => [
48+
[$class, 'validateElementSubmit'],
49+
[$class, 'validateForm'],
50+
],
51+
'#commerce_element_submit' => [
52+
[$class, 'submitForm'],
53+
],
54+
'#theme_wrappers' => ['container'],
55+
];
56+
}
57+
58+
/**
59+
* Builds the element form.
60+
*
61+
* @param array $element
62+
* The form element being processed.
63+
* @param \Drupal\Core\Form\FormStateInterface $form_state
64+
* The current state of the form.
65+
* @param array $complete_form
66+
* The complete form structure.
67+
*
68+
* @throws \InvalidArgumentException
69+
* Thrown when #default_value is empty or not an entity, or when
70+
* #available_countries is not an array of country codes.
71+
*
72+
* @return array
73+
* The processed form element.
74+
*/
75+
public static function processForm(array $element, FormStateInterface $form_state, array &$complete_form) {
76+
if (empty($element['#default_value'])) {
77+
throw new \InvalidArgumentException('The commerce_profile_select element requires the #default_value property.');
78+
}
79+
elseif (isset($element['#default_value']) && !($element['#default_value'] instanceof ProfileInterface)) {
80+
throw new \InvalidArgumentException('The commerce_profile_select #default_value property must be a profile entity.');
81+
}
82+
if (!is_array($element['#available_countries'])) {
83+
throw new \InvalidArgumentException('The commerce_profile_select #available_countries property must be an array.');
84+
}
85+
86+
$element['#profile'] = $element['#default_value'];
87+
$form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
88+
$form_display->buildForm($element['#profile'], $element, $form_state);
89+
if (!empty($element['address']['widget'][0])) {
90+
$widget_element = &$element['address']['widget'][0];
91+
// Remove the details wrapper from the address widget.
92+
$widget_element['#type'] = 'container';
93+
// Provide a default country.
94+
if (!empty($element['#default_country']) && empty($widget_element['address']['#default_value']['country_code'])) {
95+
$widget_element['address']['#default_value']['country_code'] = $element['#default_country'];
96+
}
97+
// Limit the available countries.
98+
if (!empty($element['#available_countries'])) {
99+
$widget_element['address']['#available_countries'] = $element['#available_countries'];
100+
}
101+
}
102+
103+
return $element;
104+
}
105+
106+
/**
107+
* Validates the element form.
108+
*
109+
* @param array $element
110+
* The form element.
111+
* @param \Drupal\Core\Form\FormStateInterface $form_state
112+
* The current state of the form.
113+
*
114+
* @throws \Exception
115+
* Thrown if button-level #validate handlers are detected on the parent
116+
* form, as a protection against buggy behavior.
117+
*/
118+
public static function validateForm(array &$element, FormStateInterface $form_state) {
119+
$form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
120+
$form_display->extractFormValues($element['#profile'], $element, $form_state);
121+
$form_display->validateFormValues($element['#profile'], $element, $form_state);
122+
}
123+
124+
/**
125+
* Submits the element form.
126+
*
127+
* @param array $element
128+
* The form element.
129+
* @param \Drupal\Core\Form\FormStateInterface $form_state
130+
* The current state of the form.
131+
*/
132+
public static function submitForm(array &$element, FormStateInterface $form_state) {
133+
$form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
134+
$form_display->extractFormValues($element['#profile'], $element, $form_state);
135+
$element['#profile']->save();
136+
}
137+
138+
}

modules/order/src/Entity/Order.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -630,14 +630,14 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
630630
->setDisplayConfigurable('view', TRUE);
631631

632632
$fields['billing_profile'] = BaseFieldDefinition::create('entity_reference_revisions')
633-
->setLabel(t('Billing profile'))
633+
->setLabel(t('Billing information'))
634634
->setDescription(t('Billing profile'))
635635
->setSetting('target_type', 'profile')
636636
->setSetting('handler', 'default')
637637
->setSetting('handler_settings', ['target_bundles' => ['customer']])
638638
->setTranslatable(TRUE)
639639
->setDisplayOptions('form', [
640-
'type' => 'options_select',
640+
'type' => 'commerce_billing_profile',
641641
'weight' => 0,
642642
'settings' => [],
643643
])

0 commit comments

Comments
 (0)