Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
project
address
Commits
34060936
Commit
34060936
authored
May 16, 2017
by
bojanz
Browse files
Issue
#2872570
by bojanz: Provide a zone field type
parent
e7ed929d
Changes
6
Hide whitespace changes
Inline
Side-by-side
config/schema/address.schema.yml
View file @
34060936
...
...
@@ -85,7 +85,7 @@ field.field_settings.address:
field.widget.settings.address_default
:
type
:
mapping
label
:
'
Default
address
formatter
settings'
label
:
'
Default
address
widget
settings'
mapping
:
default_country
:
type
:
string
...
...
@@ -109,6 +109,37 @@ field.field_settings.address_country:
sequence
:
-
type
:
string
field.value.address_zone
:
type
:
mapping
label
:
'
Default
value'
mapping
:
label
:
type
:
label
label
:
'
Label'
territories
:
type
:
sequence
label
:
'
Territories'
sequence
:
-
type
:
zone_territory
field.field_settings.address_zone
:
type
:
mapping
label
:
'
Zone
field
settings'
mapping
:
available_countries
:
type
:
sequence
label
:
'
Available
countries'
sequence
:
-
type
:
string
field.widget.settings.address_zone_default
:
type
:
mapping
label
:
'
Default
zone
widget
settings'
mapping
:
show_label_field
:
type
:
boolean
label
:
'
Show
the
zone
label
field'
views.filter.country_code
:
type
:
views.filter.in_operator
label
:
'
Country'
src/Element/Zone.php
0 → 100644
View file @
34060936
<?php
namespace
Drupal\address\Element
;
use
Drupal\Core\Form\FormStateInterface
;
use
Drupal\Core\Render\Element\FormElement
;
use
Drupal\Component\Utility\Html
;
use
Drupal\Component\Utility\NestedArray
;
/**
* Provides a zone form element.
*
* Use it to populate a \CommerceGuys\Addressing\Zone\Zone object.
*
* Note that the default value does not need to contain a 'label'
* property if #show_label_field is FALSE.
*
* Usage example:
* @code
* $form['zone'] = [
* '#type' => 'address_zone',
* '#default_value' => [
* 'label' => t('California and Nevada'),
* 'territories' => [
* ['country_code' => 'US', 'administrative_area' => 'CA'],
* ['country_code' => 'US', 'administrative_area' => 'NV'],
* ],
* ],
* '#show_label_field' => TRUE,
* '#available_countries' => ['US', 'FR'],
* ];
* @endcode
*
* @FormElement("address_zone")
*/
class
Zone
extends
FormElement
{
/**
* {@inheritdoc}
*/
public
function
getInfo
()
{
$class
=
get_called_class
();
return
[
'#show_label_field'
=>
FALSE
,
// List of country codes. If empty, all countries will be available.
'#available_countries'
=>
[],
'#input'
=>
TRUE
,
'#multiple'
=>
FALSE
,
'#default_value'
=>
NULL
,
'#process'
=>
[
[
$class
,
'processZone'
],
[
$class
,
'processGroup'
],
],
'#element_validate'
=>
[
[
$class
,
'validateZone'
],
],
'#theme_wrappers'
=>
[
'container'
],
];
}
/**
* {@inheritdoc}
*/
public
static
function
valueCallback
(
&
$element
,
$input
,
FormStateInterface
$form_state
)
{
if
(
is_array
(
$input
))
{
$value
=
$input
;
}
else
{
if
(
!
is_array
(
$element
[
'#default_value'
]))
{
$element
[
'#default_value'
]
=
[];
}
$value
=
$element
[
'#default_value'
];
}
// Initialize default keys.
foreach
([
'label'
,
'territories'
]
as
$property
)
{
if
(
!
isset
(
$value
[
$property
]))
{
$value
[
$property
]
=
NULL
;
}
}
return
$value
;
}
/**
* Processes the zone form element.
*
* @param array $element
* The form element to process.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*
* @throws \InvalidArgumentException
* Thrown when the #default_value is malformed.
*/
public
static
function
processZone
(
array
&
$element
,
FormStateInterface
$form_state
,
array
&
$complete_form
)
{
if
(
!
empty
(
$element
[
'#default_value'
][
'territories'
])
&&
!
is_array
(
$element
[
'#default_value'
][
'territories'
]))
{
throw
new
\
InvalidArgumentException
(
'The #default_value "territories" property must be an array.'
);
}
$id_prefix
=
implode
(
'-'
,
$element
[
'#parents'
]);
$wrapper_id
=
Html
::
getUniqueId
(
$id_prefix
.
'-ajax-wrapper'
);
$button_id_prefix
=
implode
(
'_'
,
$element
[
'#parents'
]);
$value
=
$element
[
'#value'
];
$element_state
=
self
::
getElementState
(
$element
[
'#parents'
],
$form_state
);
if
(
!
isset
(
$element_state
[
'territories'
]))
{
// Default to a single empty row if no other value was provided.
$element_state
[
'territories'
]
=
$value
[
'territories'
];
$element_state
[
'territories'
]
=
$element_state
[
'territories'
]
?:
[
NULL
];
self
::
setElementState
(
$element
[
'#parents'
],
$form_state
,
$element_state
);
}
$element
[
'#required'
]
=
TRUE
;
$element
=
[
'#tree'
=>
TRUE
,
'#prefix'
=>
'<div id="'
.
$wrapper_id
.
'">'
,
'#suffix'
=>
'</div>'
,
]
+
$element
;
$element
[
'label'
]
=
[
'#type'
=>
'textfield'
,
'#title'
=>
t
(
'Zone label'
),
'#default_value'
=>
$value
[
'label'
],
'#access'
=>
$element
[
'#show_label_field'
],
];
$element
[
'territories'
]
=
[
'#type'
=>
'table'
,
'#header'
=>
[
t
(
'Territory'
),
t
(
'Operations'
),
],
'#input'
=>
FALSE
,
];
foreach
(
$element_state
[
'territories'
]
as
$index
=>
$territory
)
{
$territory_form
=
&
$element
[
'territories'
][
$index
];
$territory_form
[
'territory'
]
=
[
'#type'
=>
'address_zone_territory'
,
'#default_value'
=>
$territory
,
'#available_countries'
=>
$element
[
'#available_countries'
],
'#required'
=>
$element
[
'#required'
],
// Remove the 'territory' level from form state values.
'#parents'
=>
array_merge
(
$element
[
'#parents'
],
[
'territories'
,
$index
]),
];
$territory_form
[
'remove'
]
=
[
'#type'
=>
'submit'
,
'#name'
=>
$button_id_prefix
.
'_remove_territory'
.
$index
,
'#value'
=>
t
(
'Remove'
),
'#limit_validation_errors'
=>
[],
'#submit'
=>
[[
get_called_class
(),
'removeTerritorySubmit'
]],
'#territory_index'
=>
$index
,
'#ajax'
=>
[
'callback'
=>
[
get_called_class
(),
'ajaxRefresh'
],
'wrapper'
=>
$wrapper_id
,
],
];
}
$element
[
'territories'
][]
=
[
'add_territory'
=>
[
'#type'
=>
'submit'
,
'#name'
=>
$button_id_prefix
.
'_add_territory'
,
'#value'
=>
t
(
'Add territory'
),
'#submit'
=>
[[
get_called_class
(),
'addTerritorySubmit'
]],
'#limit_validation_errors'
=>
[],
'#ajax'
=>
[
'callback'
=>
[
get_called_class
(),
'ajaxRefresh'
],
'wrapper'
=>
$wrapper_id
,
],
],
];
return
$element
;
}
/**
* Validates the zone.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public
static
function
validateZone
(
array
$element
,
FormStateInterface
$form_state
)
{
$value
=
$form_state
->
getValue
(
$element
[
'#parents'
]);
// Remove empty territories, unneeded keys.
foreach
(
$value
[
'territories'
]
as
$index
=>
$territory
)
{
if
(
empty
(
$territory
[
'country_code'
]))
{
unset
(
$value
[
'territories'
][
$index
]);
}
unset
(
$territory
[
'remove'
]);
unset
(
$territory
[
'add_territory'
]);
}
$value
[
'territories'
]
=
array_filter
(
$value
[
'territories'
]);
$form_state
->
setValue
(
$element
[
'#parents'
],
$value
);
// Required zones must always have a territory.
// @todo Invent a nicer UX for optional zones.
if
(
$element
[
'#required'
]
&&
empty
(
$value
[
'territories'
]))
{
$form_state
->
setError
(
$element
[
'territories'
],
t
(
'Please add at least one territory.'
));
}
}
/**
* Ajax callback.
*/
public
static
function
ajaxRefresh
(
array
$form
,
FormStateInterface
$form_state
)
{
$triggering_element
=
$form_state
->
getTriggeringElement
();
return
NestedArray
::
getValue
(
$form
,
array_slice
(
$triggering_element
[
'#array_parents'
],
0
,
-
3
));
}
/**
* Submit callback for adding a new territory.
*/
public
static
function
addTerritorySubmit
(
array
$form
,
FormStateInterface
$form_state
)
{
$triggering_element
=
$form_state
->
getTriggeringElement
();
$element_parents
=
array_slice
(
$triggering_element
[
'#parents'
],
0
,
-
3
);
$element_state
=
self
::
getElementState
(
$element_parents
,
$form_state
);
$element_state
[
'territories'
][]
=
NULL
;
self
::
setElementState
(
$element_parents
,
$form_state
,
$element_state
);
$form_state
->
setRebuild
();
}
/**
* Submit callback for removing a territory.
*/
public
static
function
removeTerritorySubmit
(
array
$form
,
FormStateInterface
$form_state
)
{
$triggering_element
=
$form_state
->
getTriggeringElement
();
$element_parents
=
array_slice
(
$triggering_element
[
'#parents'
],
0
,
-
3
);
$element_state
=
self
::
getElementState
(
$element_parents
,
$form_state
);
$territory_index
=
$triggering_element
[
'#territory_index'
];
unset
(
$element_state
[
'territories'
][
$territory_index
]);
self
::
setElementState
(
$element_parents
,
$form_state
,
$element_state
);
$form_state
->
setRebuild
();
}
/**
* Gets the element state.
*
* @param array $parents
* The element parents.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The element state.
*/
public
static
function
getElementState
(
array
$parents
,
FormStateInterface
$form_state
)
{
$parents
=
array_merge
([
'element_state'
,
'#parents'
],
$parents
);
return
NestedArray
::
getValue
(
$form_state
->
getStorage
(),
$parents
);
}
/**
* Sets the element state.
*
* @param array $parents
* The element parents.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $element_state
* The element state.
*/
public
static
function
setElementState
(
array
$parents
,
FormStateInterface
$form_state
,
array
$element_state
)
{
$parents
=
array_merge
([
'element_state'
,
'#parents'
],
$parents
);
NestedArray
::
setValue
(
$form_state
->
getStorage
(),
$parents
,
$element_state
);
}
}
src/Element/ZoneTerritory.php
View file @
34060936
...
...
@@ -19,7 +19,7 @@ use Drupal\Component\Utility\NestedArray;
* Usage example:
* @code
* $form['territory'] = [
* '#type' => 'zone_territory',
* '#type' => '
address_
zone_territory',
* '#default_value' => [
* 'country_code' => 'US',
* 'administrative_area' => 'CA',
...
...
@@ -28,7 +28,7 @@ use Drupal\Component\Utility\NestedArray;
* ];
* @endcode
*
* @FormElement("zone_territory")
* @FormElement("
address_
zone_territory")
*/
class
ZoneTerritory
extends
FormElement
{
...
...
src/Plugin/Field/FieldType/ZoneItem.php
0 → 100644
View file @
34060936
<?php
namespace
Drupal\address\Plugin\Field\FieldType
;
use
CommerceGuys\Addressing\Zone\Zone
;
use
Drupal\Core\Field\FieldItemBase
;
use
Drupal\Core\Field\FieldStorageDefinitionInterface
;
use
Drupal\Core\Form\FormStateInterface
;
use
Drupal\Core\TypedData\DataDefinition
;
/**
* Plugin implementation of the 'zone' field type.
*
* @FieldType(
* id = "address_zone",
* label = @Translation("Zone"),
* description = @Translation("An entity field containing a zone"),
* category = @Translation("Address"),
* default_widget = "address_zone_default",
* cardinality = 1,
* )
*/
class
ZoneItem
extends
FieldItemBase
{
use
AvailableCountriesTrait
;
/**
* {@inheritdoc}
*/
public
static
function
schema
(
FieldStorageDefinitionInterface
$field_definition
)
{
return
[
'columns'
=>
[
'value'
=>
[
'description'
=>
'The serialized zone.'
,
'type'
=>
'blob'
,
'not null'
=>
TRUE
,
'serialize'
=>
TRUE
,
],
],
];
}
/**
* {@inheritdoc}
*/
public
static
function
propertyDefinitions
(
FieldStorageDefinitionInterface
$field_definition
)
{
$properties
=
[];
$properties
[
'value'
]
=
DataDefinition
::
create
(
'any'
)
->
setLabel
(
t
(
'Value'
))
->
setRequired
(
TRUE
);
return
$properties
;
}
/**
* {@inheritdoc}
*/
public
static
function
defaultFieldSettings
()
{
return
self
::
defaultCountrySettings
();
}
/**
* {@inheritdoc}
*/
public
function
fieldSettingsForm
(
array
$form
,
FormStateInterface
$form_state
)
{
return
$this
->
countrySettingsForm
(
$form
,
$form_state
);
}
/**
* {@inheritdoc}
*/
public
function
isEmpty
()
{
return
$this
->
value
===
NULL
||
!
$this
->
value
instanceof
Zone
;
}
/**
* {@inheritdoc}
*/
public
function
setValue
(
$values
,
$notify
=
TRUE
)
{
if
(
is_array
(
$values
))
{
// The property definition causes the zone to be in 'value' key.
$values
=
reset
(
$values
);
}
if
(
!
$values
instanceof
Zone
)
{
$values
=
NULL
;
}
parent
::
setValue
(
$values
,
$notify
);
}
}
src/Plugin/Field/FieldWidget/ZoneDefaultWidget.php
0 → 100644
View file @
34060936
<?php
namespace
Drupal\address\Plugin\Field\FieldWidget
;
use
CommerceGuys\Addressing\Zone\Zone
;
use
Drupal\Component\Utility\NestedArray
;
use
Drupal\Core\Field\FieldItemListInterface
;
use
Drupal\Core\Field\WidgetBase
;
use
Drupal\Core\Form\FormStateInterface
;
use
Symfony\Component\Validator\ConstraintViolationInterface
;
/**
* Plugin implementation of the 'address_zone_default' widget.
*
* @FieldWidget(
* id = "address_zone_default",
* label = @Translation("Zone"),
* field_types = {
* "address_zone"
* },
* )
*/
class
ZoneDefaultWidget
extends
WidgetBase
{
/**
* {@inheritdoc}
*/
public
static
function
defaultSettings
()
{
return
[
'show_label_field'
=>
FALSE
,
]
+
parent
::
defaultSettings
();
}
/**
* {@inheritdoc}
*/
public
function
settingsForm
(
array
$form
,
FormStateInterface
$form_state
)
{
$element
=
[];
$element
[
'show_label_field'
]
=
[
'#type'
=>
'checkbox'
,
'#title'
=>
$this
->
t
(
'Show the zone label field'
),
'#default_value'
=>
$this
->
getSetting
(
'show_label_field'
),
];
return
$element
;
}
/**
* {@inheritdoc}
*/
public
function
settingsSummary
()
{
$summary
=
[];
$summary
[
'show_label_field'
]
=
$this
->
t
(
'Zone label field: @status'
,
[
'@status'
=>
$this
->
getSetting
(
'show_label_field'
)
?
$this
->
t
(
'Shown'
)
:
$this
->
t
(
'Hidden'
),
]);
return
$summary
;
}
/**
* {@inheritdoc}
*/
public
function
formElement
(
FieldItemListInterface
$items
,
$delta
,
array
$element
,
array
&
$form
,
FormStateInterface
$form_state
)
{
$item
=
$items
[
$delta
];
$value
=
[];
if
(
!
$item
->
isEmpty
())
{
/** @var \CommerceGuys\Addressing\Zone\Zone $zone */
$zone
=
$item
->
value
;
$value
=
[
'label'
=>
$zone
->
getLabel
(),
'territories'
=>
[],
];
foreach
(
$zone
->
getTerritories
()
as
$territory
)
{
$value
[
'territories'
][]
=
[
'country_code'
=>
$territory
->
getCountryCode
(),
'administrative_area'
=>
$territory
->
getAdministrativeArea
(),
'locality'
=>
$territory
->
getLocality
(),
'dependent_locality'
=>
$territory
->
getDependentLocality
(),
'included_postal_codes'
=>
$territory
->
getIncludedPostalCodes
(),
'excluded_postal_codes'
=>
$territory
->
getExcludedPostalCodes
(),
];
}
}
$element
+=
[
'#type'
=>
'details'
,
'#collapsible'
=>
TRUE
,
'#open'
=>
TRUE
,
];
$element
[
'zone'
]
=
[
'#type'
=>
'address_zone'
,
'#default_value'
=>
$value
,
'#required'
=>
$this
->
fieldDefinition
->
isRequired
(),
'#show_label_field'
=>
$this
->
getSetting
(
'show_label_field'
),
'#available_countries'
=>
$item
->
getAvailableCountries
(),
];
return
$element
;
}
/**
* {@inheritdoc}
*/
public
function
errorElement
(
array
$element
,
ConstraintViolationInterface
$violation
,
array
$form
,
FormStateInterface
$form_state
)
{
$error_element
=
NestedArray
::
getValue
(
$element
[
'zone'
],
$violation
->
arrayPropertyPath
);
return
is_array
(
$error_element
)
?
$error_element
:
FALSE
;
}
/**
* {@inheritdoc}
*/
public
function
massageFormValues
(
array
$values
,
array
$form
,
FormStateInterface
$form_state
)
{
$new_values
=
[];
foreach
(
$values
as
$delta
=>
$value
)
{
if
(
empty
(
$value
[
'zone'
][
'territories'
]))
{
// Zones with no territories are considered empty.
continue
;
}
$new_values
[
$delta
]
=
new
Zone
([
'id'
=>
$this
->
fieldDefinition
->
getName
(),
'label'
=>
$value
[
'zone'
][
'label'
]
?:
$this
->
fieldDefinition
->
getLabel
(),
'territories'
=>
$value
[
'zone'
][
'territories'
],
]);
}
return
$new_values
;
}
}
tests/src/Kernel/ZoneItemTest.php
0 → 100644
View file @
34060936
<?php
namespace
Drupal\Tests\address\Kernel
;
use
CommerceGuys\Addressing\Zone\Zone
;
use
Drupal\entity_test
\
Entity\EntityTest
;
use
Drupal\field\Entity\FieldConfig
;
use
Drupal\field\Entity\FieldStorageConfig
;
use
Drupal\KernelTests\Core\Entity\EntityKernelTestBase
;
/**
* Tests the address_zone field.
*
* @group commerce
*/
class
ZoneItemTest
extends
EntityKernelTestBase
{
/**
* @var array
*/
public
static
$modules
=
[
'address'
,
];
/**
* The test entity.
*
* @var \Drupal\entity_test\Entity\EntityTest
*/
protected
$testEntity
;
/**
* {@inheritdoc}
*/
protected
function
setUp
()
{
parent
::
setUp
();
$field_storage
=
FieldStorageConfig
::
create
([
'field_name'
=>
'field_zone'
,
'entity_type'
=>
'entity_test'
,
'type'
=>
'address_zone'
,
'cardinality'
=>
1
,
]);
$field_storage
->
save
();
$field
=
FieldConfig
::
create
([
'field_name'
=>
'field_zone'
,
'entity_type'
=>
'entity_test'
,
'bundle'
=>
'entity_test'
,
]);
$field
->
save
();
$entity
=
EntityTest
::
create
([
'name'
=>
'Test'
,
]);
$entity
->
save
();
$this
->
testEntity
=
$entity
;
}
/**
* Tests storing and retrieving a zone from the field.
*/
public
function
testZone
()
{
$zone
=
new
Zone
([
'id'
=>
'test'
,
'label'
=>
'Test'
,
'territories'
=>
[