turnstile

Drupal paywall plugin
Log | Files | Refs | README | LICENSE

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 }