commerce_donation_flow-1.0.x-dev/src/Plugin/Field/FieldWidget/DonationLevelWidget.php
src/Plugin/Field/FieldWidget/DonationLevelWidget.php
<?php namespace Drupal\commerce_donation_flow\Plugin\Field\FieldWidget; use CommerceGuys\Intl\Formatter\CurrencyFormatterInterface; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\commerce_store\CurrentStore; use Symfony\Component\Validator\ConstraintViolationInterface; /** * A price field widget for donation levels. * * @FieldWidget( * id = "commerce_donation_flow_level_widget", * label = @Translation("Donation level"), * field_types = { * "commerce_price" * } * ) */ class DonationLevelWidget extends WidgetBase implements ContainerFactoryPluginInterface { /** * Injected service. * * @var \Drupal\commerce_store\CurrentStore */ protected $store; /** * Injected service. * * @var \CommerceGuys\Intl\Formatter\CurrencyFormatterInterface */ protected $currencyFormatter; /** * Level keys. * * @var array */ protected $levels; /** * The current radio options. * * @var array */ protected $options; /** * The current default value. * * @var array */ protected $defaultValue; /** * {@inheritdoc} */ public function __construct( $plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, CurrentStore $currentStore, CurrencyFormatterInterface $currency_formatter ) { parent::__construct( $plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings ); $this->store = $currentStore; $this->currencyFormatter = $currency_formatter; $this->levels = [ 'level_1', 'level_2', 'level_3', 'level_4', 'level_5', 'custom_amount', ]; } /** * {@inheritdoc} */ public static function create( ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition ) { return new static( $plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], $container->get('commerce_store.current_store'), $container->get('commerce_price.currency_formatter') ); } /** * {@inheritdoc} */ public static function defaultSettings() { return [ 'currency' => \Drupal::service('commerce_store.current_store') ->getStore() ->getDefaultCurrencyCode(), 'levels' => [], ] + parent::defaultSettings(); } /** * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { $levels = $this->getSetting('levels'); $types = [ 'single' => $this->t('Single Donation'), 'monthly' => $this->t('Monthly Donation'), ]; $form['levels'] = [ '#type' => 'fieldset', '#title' => $this->t('Preset donation levels'), '#description' => $this->t( 'These are fixed amounts offered for choice to a user.' ), ]; foreach ($types as $type => $label) { $form['levels'][$type] = [ '#type' => 'fieldset', '#title' => $label, ]; foreach ($this->levels as $level) { if ($level == 'custom_amount') { continue; } $form['levels'][$type][$level] = [ '#type' => 'number', '#title' => $this ->t('Amount (@level):', ['@level' => $level]), '#default_value' => isset($levels[$type][$level]) ? $levels[$type][$level] : 5, '#min' => 5, '#step' => 5, ]; } } return $form; } /** * Helper function for filtering empty values. * * @param mixed $value * The value to check. * * @return bool * Returns true if the variable stores a value. */ protected function filterEmptyAmount($value) { return !empty($value); } /** * {@inheritdoc} */ public function settingsSummary() { $class = get_class($this); $summary = parent::settingsSummary(); $levels = $this->getSetting('levels'); $singleValues = isset($levels["single"]) ? implode(', ', array_filter($levels["single"], [ $class, 'filterEmptyAmount', ])) : ''; $monthlyValues = isset($levels["monthly"]) ? implode(', ', array_filter($levels["monthly"], [ $class, 'filterEmptyAmount', ])) : ''; $summary[] = $this->t('Single: @values', ['@values' => $singleValues]); $summary[] = $this->t('Monthly: @values', ['@values' => $monthlyValues]); return $summary; } /** * {@inheritdoc} */ public function formElement( FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state ) { $this->init($items, $delta, $element, $form, $form_state); $customLevelId = $this->getCustomLevelId(); $element = [ 'donation_level' => [ '#type' => 'fieldset', '#title' => $this ->t('Amount'), '#required' => $element['#required'], '#attributes' => [ 'data-donation-level' => 'fieldset', ], 'value' => [ '#type' => 'radios', '#default_value' => empty($this->defaultValue) ? NULL : $this->defaultValue, '#options' => $this->options, '#attributes' => [ 'data-donation-level' => 'options', ], ], ], ]; if (!empty($customLevelId)) { $label = $this->t('Other Amount'); $incomingValue = $items->first()->getValue(); $useCustom = $this->defaultValue == $customLevelId; $element['donation_level']['amount'] = [ '#type' => 'number', '#title' => $label, '#placeholder' => $label, '#title_display' => 'before', '#step' => 'any', '#min' => 5, '#default_value' => $useCustom && isset($incomingValue['number']) ? $this->formatAmount($incomingValue['number'], FALSE, FALSE) : NULL, '#attributes' => [ 'data-donation-level' => 'custom-amount', ], ]; } return $element; } /** * {@inheritdoc} */ public function massageFormValues( array $values, array $form, FormStateInterface $form_state ) { $customLevelId = $this->getCustomLevelId(); $gift_type = reset($form_state->getValue([ 'commerce_donation_pane', 'field_gift_type', ])); $isMonthly = reset($gift_type) == 'recurring'; $options = $this->calculateOptions($form_state, $isMonthly); foreach ($values as &$item) { if ($item['donation_level']['value'] == $customLevelId) { // A custom amount is used. // Force empty amount to 0. if ($item['donation_level']['amount'] === '') { $item['donation_level']['amount'] = 0; } // Add currency code. $value = [ 'number' => $item['donation_level']['amount'], 'currency_code' => $this->getSetting('currency'), ]; } else { // Get amount from the selected option. $value = [ 'number' => preg_replace('/\D/', '', $options[$item['donation_level']['value']]), 'currency_code' => $this->getSetting('currency'), ]; } $item = $value; } return $values; } /** * {@inheritdoc} */ public function errorElement( array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state ) { $error_element = NestedArray::getValue( $element['donation_level'], $error->arrayPropertyPath ); return is_array($error_element) ? $error_element : FALSE; } /** * Utility function to build the options array and default value for the form. */ protected function init( FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state ) { /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $entity = $items->getEntity(); $isMonthly = FALSE; $incomingValue = $items->first()->getValue(); $userInput = $form_state->getUserInput(); if (empty($userInput) && $entity->hasField('field_gift_type') && !$entity->field_gift_type->isEmpty()) { $isMonthly = $entity->field_gift_type->first()->value == 'recurring'; } elseif (!empty($userInput)) { // Clear any set values as we are changing the options. $incomingValue = []; $userInput["commerce_donation_pane"]["field_donation_amount"][0]["donation_level"] = NULL; $form_state->setUserInput($userInput); // Check if now Monthly is selected. if (isset($userInput["commerce_donation_pane"]["field_gift_type"])) { $isMonthly = $userInput["commerce_donation_pane"]["field_gift_type"] == 'recurring'; } } $this->options = $this->calculateOptions($form_state, $isMonthly); $default = $this->calculateDefault($form_state, $incomingValue, $isMonthly); $this->defaultValue = empty($default) ? $this->levels[1] : $default; } /** * Helper method to calculate options. * * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state. * @param bool $isMonthly * Flag to use monthly amounts. * * @return array * An array of options. */ protected function calculateOptions(FormStateInterface $form_state, $isMonthly = FALSE) { $options = []; $presetLevels = $this->getSetting('levels'); $levels = $isMonthly ? $presetLevels['monthly'] : $presetLevels['single']; foreach ($levels as $level_id => $level_amount) { if (empty($level_amount)) { // Skip the option if there is no amount. continue; } $options[$level_id] = $this->formatAmount($level_amount, $isMonthly); } $options[$this->getCustomLevelId()] = 'Enter amount below:'; return $options; } /** * Helper method to calculate default value. * * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state. * @param array $value * The value to array to use in calculation. * @param bool $isMonthly * Flag to use monthly amounts. * * @return null|string * Returns null if no default determined. */ protected function calculateDefault(FormStateInterface $form_state, array $value, $isMonthly = FALSE) { $default = NULL; $incomingNumber = isset($value['number']) ? (int) $value['number'] : NULL; $presetLevels = $this->getSetting('levels'); $levels = $isMonthly ? $presetLevels['monthly'] : $presetLevels['single']; foreach ($levels as $level_id => $level_amount) { if (empty($level_amount)) { // Skip the option if there is no amount. continue; } if ($level_amount == $incomingNumber) { $default = $level_id; } } if (empty($default) && !empty($incomingNumber)) { $default = $this->getCustomLevelId(); } return $default; } /** * Utility function to extract the custom level id from settings. * * @return string * The level id. */ protected function getCustomLevelId() { return 'custom_amount'; } /** * Helper method to format the amount. * * @param string $amount * The amount to format. * @param bool $addMonthly * Should the /mo. string be added? * @param bool $symbol * If a currency symbol should be used. * * @return string * The formatted output. */ protected function formatAmount($amount, $addMonthly = FALSE, $symbol = TRUE) { $formatOptions = [ 'currency_display' => 'symbol', 'minimum_fraction_digits' => 0, ]; if (!$symbol) { $formatOptions['currency_display'] = 'none'; } $formatted = $this->currencyFormatter->format( $amount, $this->getSetting('currency'), $formatOptions ); if ($addMonthly) { $formatted .= '/mo'; } return $formatted; } }