PriceCategoryForm.php (7311B)
1 <?php 2 3 /** 4 * @file 5 * Location: src/Form/PriceCategoryForm.php 6 * 7 * Form handler for price category add and edit forms. 8 */ 9 10 namespace Drupal\taler_turnstile\Form; 11 12 use Drupal\Core\Entity\EntityForm; 13 use Drupal\Core\Form\FormStateInterface; 14 use Drupal\taler_turnstile\TalerBackendErrorKind; 15 use Drupal\taler_turnstile\TalerMerchantApiService; 16 use Symfony\Component\DependencyInjection\ContainerInterface; 17 18 /** 19 * Form handler for the price category add and edit forms. 20 */ 21 class PriceCategoryForm extends EntityForm { 22 23 /** 24 * The Turnstile API service. 25 * 26 * @var \Drupal\taler_turnstile\TalerMerchantApiService 27 */ 28 protected $apiService; 29 30 /** 31 * Constructs a PriceCategoryForm object. 32 * 33 * @param \Drupal\taler_turnstile\TalerMerchantApiService $api_service 34 * The API service. 35 */ 36 public function __construct(TalerMerchantApiService $api_service) { 37 $this->apiService = $api_service; 38 } 39 40 /** 41 * {@inheritdoc} 42 */ 43 public static function create(ContainerInterface $container) { 44 return new static( 45 $container->get('taler_turnstile.api_service') 46 ); 47 } 48 49 /** 50 * {@inheritdoc} 51 */ 52 public function form(array $form, FormStateInterface $form_state) { 53 $form = parent::form($form, $form_state); 54 /** @var TurnstilePriceCategory $price_category */ 55 $price_category = $this->entity; 56 57 $form['label'] = [ 58 '#type' => 'textfield', 59 '#title' => $this->t('Name'), 60 '#maxlength' => 255, 61 '#default_value' => $price_category->label(), 62 '#description' => $this->t('The name of the price category.'), 63 '#required' => TRUE, 64 ]; 65 66 $form['id'] = [ 67 '#type' => 'machine_name', 68 '#default_value' => $price_category->id(), 69 '#machine_name' => [ 70 'exists' => '\Drupal\taler_turnstile\Entity\TurnstilePriceCategory::load', 71 ], 72 '#disabled' => !$price_category->isNew(), 73 ]; 74 75 $form['description'] = [ 76 '#type' => 'textarea', 77 '#title' => $this->t('Description'), 78 '#default_value' => $price_category->getDescription(), 79 '#description' => $this->t('A description of this price category.'), 80 ]; 81 82 // Get subscriptions and currencies from API. Show the specific 83 // error and disable the form if either call failed: saving a 84 // half-built price category would publish a broken template. 85 $backend_url = (string) ($this->config('taler_turnstile.settings')->get('payment_backend_url') ?? ''); 86 $subs_result = $this->apiService->getSubscriptions(); 87 $cur_result = $this->apiService->getCurrencies(); 88 foreach ([$subs_result, $cur_result] as $r) { 89 if (!$r->isOk()) { 90 $this->messenger()->addError($r->toUserMessage($backend_url)); 91 } 92 } 93 // A currency list is mandatory; without it we cannot render any 94 // meaningful price inputs. Subscriptions can be empty (no token 95 // families configured yet), so only currency failure is fatal. 96 if (!$cur_result->isOk()) { 97 $form['#disabled'] = TRUE; 98 $form['actions']['#access'] = FALSE; 99 return $form; 100 } 101 $subscriptions = is_array($subs_result->data) ? $subs_result->data : []; 102 $currencies = is_array($cur_result->data) ? $cur_result->data : []; 103 104 $form['prices'] = [ 105 '#type' => 'fieldset', 106 '#title' => $this->t('Prices'), 107 '#tree' => TRUE, 108 ]; 109 110 $existing_prices = $price_category->getPrices(); 111 112 foreach ($subscriptions as $subscription_id => $subscription) { 113 $subscription_label = $subscription['label'] ?? $subscription['name']; 114 115 $form['prices'][$subscription_id] = [ 116 '#type' => 'details', 117 '#title' => $subscription_label, 118 '#open' => ($subscription_id === '%none%'), 119 ]; 120 121 foreach ($currencies as $currency) { 122 $currency_code = $currency['code'] ?? $currency['name']; 123 $currency_label = $currency['label'] ?? $currency['code']; 124 125 $form['prices'][$subscription_id][$currency_code] = [ 126 '#type' => 'number', 127 '#title' => $currency_label, 128 '#default_value' => $existing_prices[$subscription_id][$currency_code] ?? '', 129 '#min' => 0, 130 // HTML's step attribute wants a bare decimal ("0.01"), 131 // not a Taler amount ("EUR:0.01"). 132 '#step' => isset($currency['step']) 133 ? $currency['step']->numericString() 134 : '0.01', 135 '#size' => 20, 136 '#description' => $this->t('Leave empty for no price.'), 137 ]; 138 } 139 } 140 141 return $form; 142 } 143 144 145 /** 146 * {@inheritdoc} 147 */ 148 public function validateForm(array &$form, FormStateInterface $form_state) { 149 parent::validateForm($form, $form_state); 150 151 $prices = $form_state->getValue('prices'); 152 $ok = FALSE; 153 if (is_array($prices)) { 154 foreach ($prices as $subscription_id => $currencies) { 155 if (is_array($currencies)) { 156 foreach ($currencies as $currency_code => $price) { 157 // Skip empty values as they are allowed. 158 if ($price === '' || $price === NULL) { 159 continue; 160 } 161 162 // Validate that the price is a valid non-negative number. 163 if (!is_numeric($price) || $price < 0) { 164 $form_state->setErrorByName( 165 "prices][{$subscription_id}][{$currency_code}", 166 $this->t('Prices cannot be negative.') 167 ); 168 } 169 $ok = TRUE; 170 } 171 } 172 } 173 } 174 if (! $ok) { 175 $form_state->setErrorByName( 176 "", 177 $this->t('At least one price must be set.') 178 ); 179 } 180 } 181 182 183 /** 184 * {@inheritdoc} 185 */ 186 public function save(array $form, FormStateInterface $form_state) { 187 $price_category = $this->entity; 188 189 // Filter out empty prices. 190 $prices = $form_state->getValue('prices'); 191 $filtered_prices = []; 192 foreach ($prices as $subscription_id => $currencies) { 193 foreach ($currencies as $currency_code => $price) { 194 if ($price !== '') { 195 $filtered_prices[$subscription_id][$currency_code] = $price; 196 } 197 } 198 } 199 200 $price_category->setPrices($filtered_prices); 201 $status = $price_category->save(); 202 203 if ($status === SAVED_NEW) { 204 $this->messenger()->addStatus($this->t('Created the %label price category.', [ 205 '%label' => $price_category->label(), 206 ])); 207 } 208 else { 209 $this->messenger()->addStatus($this->t('Updated the %label price category.', [ 210 '%label' => $price_category->label(), 211 ])); 212 } 213 214 // postSave() stashes the merchant-backend sync result on the 215 // entity. The local config save has already succeeded; if the 216 // template did NOT make it to the backend, the admin needs to 217 // know — otherwise the paywall will quote stale prices. 218 $sync = $price_category->lastSyncResult; 219 if ($sync !== NULL && !$sync->isOk() 220 && $sync->kind !== TalerBackendErrorKind::NOT_CONFIGURED) { 221 $backend_url = (string) ($this->config('taler_turnstile.settings')->get('payment_backend_url') ?? ''); 222 $this->messenger()->addError($this->t( 223 'Saved locally, but failed to publish the price category to the merchant backend: @err', 224 ['@err' => $sync->toUserMessage($backend_url)] 225 )); 226 } 227 228 $form_state->setRedirectUrl($price_category->toUrl('collection')); 229 } 230 231 }