You are here

abstract class CheckoutFlowBase in Commerce Core 8.2

Provides the base checkout flow class.

Checkout flows should extend this class only if they don't want to use checkout panes. Otherwise they should extend CheckoutFlowWithPanesBase.

Hierarchy

Expanded class hierarchy of CheckoutFlowBase

File

modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php, line 28

Namespace

Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow
View source
abstract class CheckoutFlowBase extends PluginBase implements CheckoutFlowInterface, ContainerFactoryPluginInterface {
  use AjaxFormTrait;

  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * The current order.
   *
   * @var \Drupal\commerce_order\Entity\OrderInterface
   */
  protected $order;

  /**
   * The parent config entity.
   *
   * Not available while the plugin is being configured.
   *
   * @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface
   */
  protected $parentEntity;

  /**
   * Static cache of visible steps.
   *
   * @var array
   */
  protected $visibleSteps = [];

  /**
   * Constructs a new CheckoutFlowBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The route match.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->entityTypeManager = $entity_type_manager;
    $this->eventDispatcher = $event_dispatcher;
    $this->order = $route_match
      ->getParameter('commerce_order');
    if (array_key_exists('_entity', $configuration)) {
      $this->parentEntity = $configuration['_entity'];
      unset($configuration['_entity']);
    }
    $this
      ->setConfiguration($configuration);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static($configuration, $plugin_id, $plugin_definition, $container
      ->get('entity_type.manager'), $container
      ->get('event_dispatcher'), $container
      ->get('current_route_match'));
  }

  /**
   * {@inheritdoc}
   */
  public function __sleep() {
    if (!empty($this->parentEntity)) {
      $this->_parentEntityId = $this->parentEntity
        ->id();
      unset($this->parentEntity);
    }
    if (!empty($this->order)) {
      $this->_orderId = $this->order
        ->id();
      unset($this->order);
    }
    return parent::__sleep();
  }

  /**
   * {@inheritdoc}
   */
  public function __wakeup() {
    parent::__wakeup();
    if (!empty($this->_parentEntityId)) {
      $checkout_flow_storage = $this->entityTypeManager
        ->getStorage('commerce_checkout_flow');
      $this->parentEntity = $checkout_flow_storage
        ->load($this->_parentEntityId);
      unset($this->_parentEntityId);
    }
    if (!empty($this->_orderId)) {
      $order_storage = $this->entityTypeManager
        ->getStorage('commerce_order');
      $this->order = $order_storage
        ->load($this->_orderId);
      unset($this->_orderId);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getOrder() {
    return $this->order;
  }

  /**
   * {@inheritdoc}
   */
  public function getPreviousStepId($step_id) {
    $step_ids = array_keys($this
      ->getSteps());
    $previous_step_index = array_search($step_id, $step_ids) - 1;
    while (isset($step_ids[$previous_step_index])) {
      if (!$this
        ->isStepVisible($step_ids[$previous_step_index])) {
        $previous_step_index--;
        continue;
      }
      return $step_ids[$previous_step_index];
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getNextStepId($step_id) {
    $step_ids = array_keys($this
      ->getSteps());
    $next_step_index = array_search($step_id, $step_ids) + 1;
    while (isset($step_ids[$next_step_index])) {
      if (!$this
        ->isStepVisible($step_ids[$next_step_index])) {
        $next_step_index++;
        continue;
      }
      return $step_ids[$next_step_index];
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function redirectToStep($step_id) {
    if (!$this
      ->isStepVisible($step_id)) {
      throw new \InvalidArgumentException(sprintf('Invalid step ID "%s" passed to redirectToStep().', $step_id));
    }
    $this->order
      ->set('checkout_step', $step_id);
    $this
      ->onStepChange($step_id);
    $this->order
      ->save();
    throw new NeedsRedirectException(Url::fromRoute('commerce_checkout.form', [
      'commerce_order' => $this->order
        ->id(),
      'step' => $step_id,
    ])
      ->toString());
  }

  /**
   * {@inheritdoc}
   */
  public function getSteps() {

    // Each checkout flow plugin defines its own steps.
    // These two steps are always expected to be present.
    return [
      'payment' => [
        'label' => $this
          ->t('Payment'),
        'next_label' => $this
          ->t('Pay and complete purchase'),
        'has_sidebar' => FALSE,
        'hidden' => TRUE,
      ],
      'complete' => [
        'label' => $this
          ->t('Complete'),
        'next_label' => $this
          ->t('Complete checkout'),
        'has_sidebar' => FALSE,
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getVisibleSteps() {
    if (empty($this->visibleSteps)) {
      $steps = $this
        ->getSteps();
      foreach ($steps as $step_id => $step) {
        if (!$this
          ->isStepVisible($step_id)) {
          unset($steps[$step_id]);
        }
      }
      $this->visibleSteps = $steps;
    }
    return $this->visibleSteps;
  }

  /**
   * Gets whether the given step is visible.
   *
   * @param string $step_id
   *   The step ID.
   *
   * @return bool
   *   TRUE if the step is visible, FALSE otherwise.
   */
  protected function isStepVisible($step_id) {

    // All available steps are visible by default.
    $step_ids = array_keys($this
      ->getSteps());
    return in_array($step_id, $step_ids);
  }

  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function getConfiguration() {
    return $this->configuration;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfiguration(array $configuration) {
    $this->configuration = NestedArray::mergeDeep($this
      ->defaultConfiguration(), $configuration);
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'display_checkout_progress' => TRUE,
      'display_checkout_progress_breadcrumb_links' => FALSE,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form['display_checkout_progress'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Display checkout progress'),
      '#description' => $this
        ->t('Used by the checkout progress block to determine visibility.'),
      '#default_value' => $this->configuration['display_checkout_progress'],
    ];
    $form['display_checkout_progress_breadcrumb_links'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Display checkout progress breadcrumb as links'),
      '#description' => $this
        ->t('Let the checkout progress block render the breadcrumb as links.'),
      '#default_value' => $this->configuration['display_checkout_progress_breadcrumb_links'],
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    if (!$form_state
      ->getErrors()) {
      $values = $form_state
        ->getValue($form['#parents']);
      $this->configuration = [];
      $this->configuration['display_checkout_progress'] = $values['display_checkout_progress'];
      $this->configuration['display_checkout_progress_breadcrumb_links'] = $values['display_checkout_progress_breadcrumb_links'];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getBaseFormId() {
    return 'commerce_checkout_flow';
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'commerce_checkout_flow_' . $this->pluginId;
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, $step_id = NULL) {

    // The $step_id argument is optional only because PHP disallows adding
    // required arguments to an existing interface's method.
    if (empty($step_id)) {
      throw new \InvalidArgumentException('The $step_id cannot be empty.');
    }
    if ($form_state
      ->isRebuilding()) {

      // Ensure a fresh order, in case an ajax submit has modified it.
      $order_storage = $this->entityTypeManager
        ->getStorage('commerce_order');
      $this->order = $order_storage
        ->load($this->order
        ->id());
    }
    $steps = $this
      ->getSteps();
    $form['#tree'] = TRUE;
    $form['#step_id'] = $step_id;
    $form['#title'] = $steps[$step_id]['label'];
    $form['#theme'] = [
      'commerce_checkout_form',
    ];
    $form['#attached']['library'][] = 'commerce_checkout/form';

    // Workaround for core bug #2897377.
    $form['#id'] = Html::getId($form_state
      ->getBuildInfo()['form_id']);
    if ($this
      ->hasSidebar($step_id)) {
      $form['sidebar']['order_summary'] = [
        '#theme' => 'commerce_checkout_order_summary',
        '#order_entity' => $this->order,
        '#checkout_step' => $step_id,
      ];
    }
    $form['actions'] = $this
      ->actions($form, $form_state);

    // Make sure the cache is removed if the parent entity or the order change.
    CacheableMetadata::createFromRenderArray($form)
      ->addCacheableDependency($this->parentEntity)
      ->addCacheableDependency($this->order)
      ->applyTo($form);
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    if ($next_step_id = $this
      ->getNextStepId($form['#step_id'])) {
      $this->order
        ->set('checkout_step', $next_step_id);
      $this
        ->onStepChange($next_step_id);
      $form_state
        ->setRedirect('commerce_checkout.form', [
        'commerce_order' => $this->order
          ->id(),
        'step' => $next_step_id,
      ]);
    }
    $this->order
      ->save();
  }

  /**
   * Reacts to the current step changing.
   *
   * Called before saving the order and redirecting.
   *
   * Handles the following logic
   * 1) Locks the order before the payment page,
   * 2) Unlocks the order when leaving the payment page
   * 3) Places the order before the complete page.
   *
   * @param string $step_id
   *   The new step ID.
   */
  protected function onStepChange($step_id) {

    // Lock the order while on the 'payment' checkout step. Unlock elsewhere.
    if ($step_id == 'payment') {
      $this->order
        ->lock();
    }
    elseif ($step_id != 'payment') {
      $this->order
        ->unlock();
    }

    // Place the order.
    if ($step_id == 'complete' && $this->order
      ->getState()
      ->getId() == 'draft') {

      // Notify other modules.
      $event = new OrderEvent($this->order);
      $this->eventDispatcher
        ->dispatch(CheckoutEvents::COMPLETION, $event);
      $this->order
        ->getState()
        ->applyTransitionById('place');
    }
  }

  /**
   * Gets whether the given step has a sidebar.
   *
   * @param string $step_id
   *   The step ID.
   *
   * @return bool
   *   TRUE if the given step has a sidebar, FALSE otherwise.
   */
  protected function hasSidebar($step_id) {
    $steps = $this
      ->getSteps();
    return !empty($steps[$step_id]['has_sidebar']);
  }

  /**
   * Builds the actions element for the current form.
   *
   * @param array $form
   *   The current form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current form state.
   *
   * @return array
   *   The actions element.
   */
  protected function actions(array $form, FormStateInterface $form_state) {
    $steps = $this
      ->getSteps();
    $next_step_id = $this
      ->getNextStepId($form['#step_id']);
    $previous_step_id = $this
      ->getPreviousStepId($form['#step_id']);
    $has_next_step = $next_step_id && isset($steps[$next_step_id]['next_label']);
    $has_previous_step = $previous_step_id && isset($steps[$previous_step_id]['previous_label']);
    $actions = [
      '#type' => 'actions',
      '#access' => $has_next_step,
    ];
    if ($has_next_step) {
      $actions['next'] = [
        '#type' => 'submit',
        '#value' => $steps[$next_step_id]['next_label'],
        '#button_type' => 'primary',
        '#submit' => [
          '::submitForm',
        ],
      ];
      if ($has_previous_step) {
        $label = $steps[$previous_step_id]['previous_label'];
        $options = [
          'attributes' => [
            'class' => [
              'link--previous',
            ],
          ],
        ];
        $actions['next']['#suffix'] = Link::createFromRoute($label, 'commerce_checkout.form', [
          'commerce_order' => $this->order
            ->id(),
          'step' => $previous_step_id,
        ], $options)
          ->toString();
      }
    }
    return $actions;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
AjaxFormTrait::ajaxRefreshForm public static function Ajax handler for refreshing an entire form.
CheckoutFlowBase::$entityTypeManager protected property The entity manager.
CheckoutFlowBase::$eventDispatcher protected property The event dispatcher.
CheckoutFlowBase::$order protected property The current order.
CheckoutFlowBase::$parentEntity protected property The parent config entity.
CheckoutFlowBase::$visibleSteps protected property Static cache of visible steps.
CheckoutFlowBase::actions protected function Builds the actions element for the current form.
CheckoutFlowBase::buildConfigurationForm public function Form constructor. Overrides PluginFormInterface::buildConfigurationForm 1
CheckoutFlowBase::buildForm public function Form constructor. Overrides FormInterface::buildForm 1
CheckoutFlowBase::calculateDependencies public function Calculates dependencies for the configured plugin. Overrides DependentPluginInterface::calculateDependencies 1
CheckoutFlowBase::create public static function Creates an instance of the plugin. Overrides ContainerFactoryPluginInterface::create 1
CheckoutFlowBase::defaultConfiguration public function Gets default configuration for this plugin. Overrides ConfigurableInterface::defaultConfiguration 1
CheckoutFlowBase::getBaseFormId public function Returns a string identifying the base form. Overrides BaseFormIdInterface::getBaseFormId
CheckoutFlowBase::getConfiguration public function Gets this plugin's configuration. Overrides ConfigurableInterface::getConfiguration
CheckoutFlowBase::getFormId public function Returns a unique string identifying the form. Overrides FormInterface::getFormId
CheckoutFlowBase::getNextStepId public function Gets the next step ID for the given step ID. Overrides CheckoutFlowInterface::getNextStepId
CheckoutFlowBase::getOrder public function Gets the current order. Overrides CheckoutFlowInterface::getOrder
CheckoutFlowBase::getPreviousStepId public function Gets the previous step ID for the given step ID. Overrides CheckoutFlowInterface::getPreviousStepId
CheckoutFlowBase::getSteps public function Gets the defined steps. Overrides CheckoutFlowInterface::getSteps 1
CheckoutFlowBase::getVisibleSteps public function Gets the visible steps. Overrides CheckoutFlowInterface::getVisibleSteps
CheckoutFlowBase::hasSidebar protected function Gets whether the given step has a sidebar.
CheckoutFlowBase::isStepVisible protected function Gets whether the given step is visible. 1
CheckoutFlowBase::onStepChange protected function Reacts to the current step changing.
CheckoutFlowBase::redirectToStep public function Redirects an order to a specific step in the checkout. Overrides CheckoutFlowInterface::redirectToStep
CheckoutFlowBase::setConfiguration public function Sets the configuration for this plugin instance. Overrides ConfigurableInterface::setConfiguration
CheckoutFlowBase::submitConfigurationForm public function Form submission handler. Overrides PluginFormInterface::submitConfigurationForm 1
CheckoutFlowBase::submitForm public function Form submission handler. Overrides FormInterface::submitForm 1
CheckoutFlowBase::validateConfigurationForm public function Form validation handler. Overrides PluginFormInterface::validateConfigurationForm
CheckoutFlowBase::validateForm public function Form validation handler. Overrides FormInterface::validateForm 1
CheckoutFlowBase::__construct public function Constructs a new CheckoutFlowBase object. Overrides PluginBase::__construct 1
CheckoutFlowBase::__sleep public function Overrides DependencySerializationTrait::__sleep 1
CheckoutFlowBase::__wakeup public function Overrides DependencySerializationTrait::__wakeup
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 3
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.