Your browser doesn't support the features required by impress.js, so you are presented with a simplified version of this presentation.
For the best experience please use the latest Chrome, Safari or Firefox browser.
Interface, Class & Object
Interface
Interface
Interface
namespace Drupal\Core\Block;
/**
* The interface for "title" blocks.
*
* A title block shows the title returned by the controller.
*
* @ingroup block_api
*
* @see \Drupal\Core\Render\Element\PageTitle
*/
interface TitleBlockPluginInterface extends BlockPluginInterface {
/**
* Sets the title.
*
* @param string|array $title
* The page title: either a string for plain titles or a render array for
* formatted titles.
*/
public function setTitle($title);
}
Class
Class
Class
namespace Drupal\Core\Block\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\TitleBlockPluginInterface;
/**
* Provides a block to display the page title.
*
* @Block(
* id = "page_title_block",
* admin_label = @Translation("Page title"),
* forms = {
* "settings_tray" = FALSE,
* },
* )
*/
class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface {
/**
* The page title: a string (plain title) or a render array (formatted title).
*
* @var string|array
*/
protected $title = '';
/**
* {@inheritdoc}
*/
public function setTitle($title) {
$this->title = $title;
return $this;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return ['label_display' => FALSE];
}
/**
* {@inheritdoc}
*/
public function build() {
return [
'#type' => 'page_title',
'#title' => $this->title,
];
}
}
Object
Object
// Simplified example of object creation
$pageTitle = new PageTitleBlock($configuration, $plugin_id, $plugin_definition)
Inheritance
Inheritance
Composition
Composition
Controller
namespace Drupal\block_content\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\block_content\BlockContentTypeInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
class BlockContentController extends ControllerBase {
.
.
.
}
Controller
block_content.add_page:
path: '/block/add'
defaults:
_controller: '\Drupal\block_content\Controller\BlockContentController::add'
_title: 'Add custom block'
options:
_admin_route: TRUE
requirements:
_permission: 'administer blocks'
block_content.add_form:
path: '/block/add/{block_content_type}'
defaults:
_controller: '\Drupal\block_content\Controller\BlockContentController::addForm'
_title_callback: '\Drupal\block_content\Controller\BlockContentController::getAddFormTitle'
options:
_admin_route: TRUE
requirements:
_permission: 'administer blocks'
Plugin
Plugin
namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
/**
* Plugin implementation of the 'number_integer' formatter.
*
* The 'Default' formatter is different for integer fields on the one hand, and
* for decimal and float fields on the other hand, in order to be able to use
* different settings.
*
* @FieldFormatter(
* id = "number_integer",
* label = @Translation("Default"),
* field_types = {
* "integer"
* }
* )
*/
class IntegerFormatter extends NumericFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'thousand_separator' => '',
'prefix_suffix' => TRUE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
protected function numberFormat($number) {
return number_format($number, 0, '', $this->getSetting('thousand_separator'));
}
}
Plugin
Service
Service
services:
theme.negotiator.block.admin_demo:
class: Drupal\block\Theme\AdminDemoNegotiator
tags:
- { name: theme_negotiator, priority: 1000 }
block.page_display_variant_subscriber:
class: Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber
tags:
- { name: event_subscriber }
block.repository:
class: Drupal\block\BlockRepository
arguments: ['@entity_type.manager', '@theme.manager', '@context.handler']
Service
Services
block.repository: class: Drupal\block\BlockRepository arguments: ['@entity_type.manager', '@theme.manager', '@context.handler']
core/modules/block/src/BlockRepository.php/** * Constructs a new BlockRepository. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager service. * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager * The theme manager. * @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler * The plugin context handler. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, ThemeManagerInterface $theme_manager, ContextHandlerInterface $context_handler) { $this->blockStorage = $entity_type_manager->getStorage('block'); $this->themeManager = $theme_manager; $this->contextHandler = $context_handler; }
The Goal: Donation Specific Flow
Starting it off: a Controller
commerce_donate.donation.default:
path: '/donate'
defaults:
_controller: '\Drupal\commerce_donate\Controller\DonationController::frontController'
_title: 'Donate'
requirements:
_permission: 'make donation'
commerce_donate.donation_controller_formPage:
path: '/donate/{commerce_order}/{step}'
defaults:
_controller: '\Drupal\commerce_donate\Controller\DonationController::nextStep'
_title: 'Donate'
requirements:
_custom_access: '\Drupal\commerce_donate\Controller\DonationController::checkAccess'
options:
parameters:
commerce_order:
type: entity:commerce_order
Starting it off: a Controller
namespace Drupal\commerce_donate\Controller;
use Drupal\commerce_cart\CartSession;
use Drupal\commerce_cart\CartSessionInterface;
use Drupal\commerce_checkout\CheckoutOrderManagerInterface;
use Drupal\commerce_store\CurrentStoreInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\commerce_donate\NewOrder;
use Drupal\commerce_donate\Plugin\Commerce\CheckoutFlow\DonationCheckoutFlow;
use Drupal\commerce_donate\Routing\DonationSecuredRedirectResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Class DonationController.
*/
class DonationController implements ContainerInjectionInterface {
/**
* Drupal\commerce_checkout\CheckoutOrderManagerInterface definition.
*
* @var \Drupal\commerce_checkout\CheckoutOrderManagerInterface
*/
protected $checkoutOrderManager;
/**
* Drupal\Core\Form\FormBuilderInterface definition.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Injected service.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The current store.
*
* @var \Drupal\commerce_store\CurrentStoreInterface
*/
protected $currentStore;
/**
* The cart session is used for access to the order on the thank you pane.
*
* @var \Drupal\commerce_cart\CartSessionInterface
*/
protected $cartSession;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Injected service.
*
* @var \Drupal\commerce_donate\NewOrder
*/
protected $newOrders;
/**
* DonationController constructor.
*
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* Injected service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Injected service
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* Injected service.
* @param \Drupal\commerce_store\CurrentStoreInterface $current_store
* Injected service.
* @param \Drupal\commerce_checkout\CheckoutOrderManagerInterface $commerce_order_manager
* Injected service.
* @param \Drupal\commerce_cart\CartSessionInterface $session
* Injected service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Injected service.
* @param \Drupal\commerce_donate\NewOrder $newOrder
* Injected service.
*/
public function __construct(
FormBuilderInterface $form_builder,
AccountInterface $current_user,
RequestStack $request_stack,
CurrentStoreInterface $current_store,
CheckoutOrderManagerInterface $commerce_order_manager,
CartSessionInterface $session,
EntityTypeManagerInterface $entity_type_manager,
NewOrder $newOrder
) {
$this->formBuilder = $form_builder;
$this->currentUser = $current_user;
$this->requestStack = $request_stack;
$this->currentStore = $current_store;
$this->checkoutOrderManager = $commerce_order_manager;
$this->cartSession = $session;
$this->entityTypeManager = $entity_type_manager;
$this->newOrders = $newOrder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('form_builder'),
$container->get('current_user'),
$container->get('request_stack'),
$container->get('commerce_store.current_store'),
$container->get('commerce_checkout.checkout_order_manager'),
$container->get('commerce_cart.cart_session'),
$container->get('entity_type.manager'),
$container->get('commerce_donate.new_order')
);
}
/**
* Builds the initial presentation of the form provided by the donation flow.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return \Drupal\commerce_donate\Routing\DonationSecuredRedirectResponse
* Redirect to an order specific flow.
*/
public function frontController(RouteMatchInterface $route_match) {
$orderIds = $this->cartSession->getCartIds();
// If the requested step is not available, both this controller and the
// default Commerce controller redirect to the order's indicated step.
// We need a default value in case our checkout flow has not been used.
$stepId = 'placeholder';
if (empty($orderIds)) {
// Create an order to get the flow plugin.
$order = $this->newOrders->get('single');
$order->save();
$this->cartSession->addCartId($order->id());
}
else {
$order = $this->entityTypeManager->getStorage('commerce_order')->load(reset($orderIds));
}
// Always start at the first available step.
$checkoutFlow = $this->checkoutOrderManager->getCheckoutFlow($order);
$checkoutFlowPlugin = $checkoutFlow->getPlugin();
// There are no order parameters so CheckoutFlowBase misses the order.
if ($checkoutFlowPlugin instanceof DonationCheckoutFlow) {
$checkoutFlowPlugin->conditionalOrderValue($order);
$stepId = $this->checkoutOrderManager->getCheckoutStepId($order);
}
$parameters = [
'commerce_order' => $order->id(),
'step' => $stepId,
];
return $this->constructStepRedirect($parameters);
}
/**
* Builds and processes the form provided by the order's checkout flow.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return array|\Drupal\commerce_donate\Routing\DonationSecuredRedirectResponse
* The render form.
*/
public function nextStep(RouteMatchInterface $route_match) {
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $route_match->getParameter('commerce_order');
$requestedStepId = $route_match->getParameter('step');
$parameters = [
'commerce_order' => $order->id(),
];
$stepId = $this->checkoutOrderManager->getCheckoutStepId($order, $requestedStepId);
if ($requestedStepId != $stepId) {
$parameters['step'] = $stepId;
return $this->constructStepRedirect($parameters);
}
$checkout_flow = $this->checkoutOrderManager->getCheckoutFlow($order);
$checkout_flow_plugin = $checkout_flow->getPlugin();
return $this->formBuilder->getForm($checkout_flow_plugin, $stepId);
}
/**
* Builds and processes a quick donation flow.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return \Drupal\commerce_donate\Routing\DonationSecuredRedirectResponse
* Our own redirection into the flow with order prepared.
*/
public function quickDonation(RouteMatchInterface $route_match) {
// Create an order.
// Always start at the first billing step.
$stepId = 'payment';
$order = $this->newOrders->get('single');
$order->set('checkout_step', $stepId);
$orderItems = $order->getItems();
// Should only be one.
$donation = reset($orderItems);
$donationAmount = $donation->field_donation_amount;
$amount = $route_match->getParameter('amount');
$donationAmount->setValue([
'number' => $amount,
'currency_code' => $this->currentStore->getStore()
->getDefaultCurrencyCode(),
]);
$donation->save();
$checkoutFlow = $this->checkoutOrderManager->getCheckoutFlow($order);
$checkoutFlowPlugin = $checkoutFlow->getPlugin();
if ($checkoutFlowPlugin instanceof DonationCheckoutFlow) {
$checkoutFlowPlugin->conditionalOrderValue($order);
}
$order->save();
$this->cartSession->addCartId($order->id());
$parameters = [
'commerce_order' => $order->id(),
'step' => $stepId,
];
return $this->constructStepRedirect($parameters);
}
/**
* Checks access for the donation flow.
*
* Assigned as custom access for route
* commerce_donate.donation_controller_formPage
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* Drupal route match service.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user account.
*
* @return \Drupal\Core\Access\AccessResult
* The access result.
*/
public function checkAccess(
RouteMatchInterface $route_match,
AccountInterface $account
) {
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $route_match->getParameter('commerce_order');
if ($order->getState()->value == 'canceled') {
return AccessResult::forbidden()->addCacheableDependency($order);
}
// The user can checkout only their own non-empty orders.
if ($account->isAuthenticated()) {
$customer_check = $account->id() == $order->getCustomerId();
}
else {
$active_cart = $this->cartSession->hasCartId(
$order->id(),
CartSession::ACTIVE
);
$completed_cart = $this->cartSession->hasCartId(
$order->id(),
CartSession::COMPLETED
);
$customer_check = $active_cart || $completed_cart;
}
$access = AccessResult::allowedIf($customer_check)
->andIf(AccessResult::allowedIf($order->hasItems()))
->andIf(
AccessResult::allowedIfHasPermission($account, 'make donation')
)
->addCacheableDependency($order);
return $access;
}
/**
* Utility method to build a redirect.
*
* @param array $parameters
* The url parameters.
*
* @return \Drupal\commerce_donate\Routing\DonationSecuredRedirectResponse
* The prepared redirect.
*/
protected function constructStepRedirect(array $parameters) {
$url = Url::fromRoute('commerce_donate.donation_controller_formPage', $parameters);
$query = $this->requestStack->getCurrentRequest()->query;
if ($query->has('donate_return')) {
$url->setOption('query', ['donate_return' => $query->get('donate_return')]);
}
$generatedUrl = $url->toString(TRUE);
return new DonationSecuredRedirectResponse($generatedUrl->getGeneratedUrl());
}
}
NewOrder service
NewOrder service
namespace Drupal\commerce_donate;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_order\Entity\OrderItem;
use Drupal\commerce_price\Price;
use Drupal\commerce_store\CurrentStoreInterface;
use Drupal\Core\Session\AccountProxy;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Service for creating a new order with a donation item.
*
* Used in the checkout flow and the donateNow route method
* in DonationController.
*/
class NewOrder {
/**
* The current store.
*
* @var \Drupal\commerce_store\CurrentStoreInterface
*/
protected $currentStore;
/**
* Injected service.
*
* @var \Drupal\Core\Session\AccountProxy
*/
protected $user;
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request|null
*/
protected $currentRequest;
/**
* NewOrder constructor.
*
* @param \Drupal\commerce_store\CurrentStoreInterface $currentStore
* Injected service.
* @param \Drupal\Core\Session\AccountProxy $accountProxy
* Injected service.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* Injected service.
*/
public function __construct(CurrentStoreInterface $currentStore, AccountProxy $accountProxy, RequestStack $requestStack) {
$this->currentStore = $currentStore;
$this->user = $accountProxy;
$this->currentRequest = $requestStack->getCurrentRequest();
}
/**
* Creates a new Order object for the donation.
*
* @param string $giftType
* Optional gift type.
*
* @return \Drupal\commerce_order\Entity\Order
* The new order object.
*/
public function get($giftType = 'single') {
/** @var \Drupal\commerce_order\Entity\Order $order */
$order = Order::create(
[
'type' => 'default',
'state' => 'draft',
'store_id' => $this->currentStore->getStore()->id(),
]
);
if ($this->user->isAuthenticated()) {
$order->set('uid', $this->user->id());
}
// Start at zero.
$price = new Price(
0,
$this->currentStore->getStore()->getDefaultCurrencyCode()
);
$donation = OrderItem::create(
[
'type' => 'donation',
]
);
/** @var \Drupal\commerce_order\Entity\OrderItem $donation */
$donation->setUnitPrice($price);
$donation->field_gift_type->setValue($giftType);
$this->prepopulate($donation);
$order->addItem($donation);
return $order;
}
/**
* Prepopulate any matching OrderItem fields with query paramters.
*
* @param \Drupal\commerce_order\Entity\OrderItem $donation
* Our OrderItems are donations.
*/
public function prepopulate(OrderItem $donation) {
foreach ($this->currentRequest->query as $key => $value) {
if ($donation->hasField('field_' . $key)) {
$donation->set('field_' . $key, $value);
}
}
}
}
Plugins within Plugins
Plugins within Plugins
namespace Drupal\commerce_donate\Plugin\Commerce\CheckoutFlow;
use Drupal\commerce\Response\NeedsRedirectException;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesInterface;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A custom checkout flow for donations.
*
* @CommerceCheckoutFlow(
* id = "donation_checkout_flow",
* label = @Translation("Donation Flow"),
* )
*/
class DonationCheckoutFlow extends CheckoutFlowWithPanesBase implements CheckoutFlowWithPanesInterface
{
/**
* Injected service.
*
* @var \Drupal\Core\Session\AccountProxy
*/
protected $user;
/**
* Drupal\Core\Routing\CurrentRouteMatch definition.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Current default currency code.
*
* @var string
*/
protected $currency;
/**
* Injected service.
*
* @var \CommerceGuys\Intl\Formatter\CurrencyFormatterInterface
*/
protected $currencyFormatter;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* {@inheritdoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$pane_id,
$pane_definition
) {
$instance = parent::create(
$container,
$configuration,
$pane_id,
$pane_definition
);
$instance->user = $container->get('current_user');
$instance->requestStack = $container->get('request_stack');
$instance->currencyFormatter = $container->get(
'commerce_price.currency_formatter'
);
$instance->routeMatch = $container->get('current_route_match');
return $instance;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration()
{
$config = parent::defaultConfiguration();
$config['offline_link_title'] = '';
$config['offline_link_url'] = '';
return $config;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(
array $form,
FormStateInterface $form_state
) {
$form = parent::buildConfigurationForm($form, $form_state);
$link_title = $this->configuration['offline_link_title'];
$link_url = $this->configuration['offline_link_url'];
$form['offline_link_title'] = [
'#type' => 'textfield',
'#title' => $this->t('Offline donation link title'),
'#default_value' => empty($link_title) ? '' : $link_title,
];
$form['offline_link_url'] = [
'#type' => 'url',
'#title' => $this->t('Offline donation link URL'),
'#default_value' => empty($link_url) ? '' : $link_url,
'#description' => 'You may provide a link to a PDF or other file type that your users may download and use to send donations by mail.',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(
array &$form,
FormStateInterface $form_state
) {
parent::submitConfigurationForm($form, $form_state);
if (!$form_state->getErrors()) {
$values = $form_state->getValue($form['#parents']);
$this->configuration['offline_link_title'] = $values['offline_link_title'];
$this->configuration['offline_link_url'] = $values['offline_link_url'];
}
}
/**
* {@inheritdoc}
*/
public function redirectToStep($step_id)
{
// May throw a redirect exception with settings to commerce checkout.
try {
parent::redirectToStep($step_id);
} catch (NeedsRedirectException $e) {
// Use our controller instead.
throw new NeedsRedirectException(
Url::fromRoute(
'commerce_donate.donation_controller_formPage',
[
'commerce_order' => $this->order->id(),
'step' => $step_id,
]
)->toString()
);
}
}
/**
* {@inheritdoc}
*/
public function getSteps()
{
// Note that previous_label and next_label are not the labels
// shown on the step itself. Instead, they are the labels shown
// when going back to the step, or proceeding to the step.
$steps =
[
'donation' => [
'label' => $this->t('Your Donation'),
'next_label' => $this->t('Continue'),
],
'dedication' => [
'label' => $this->t('Dedication Information'),
'next_label' => $this->t('Continue'),
],
]
+ parent::getSteps();
// Adjust names to match our use case:
$steps['payment']['label'] = $this->t('Billing Information');
$steps['payment']['next_label'] = $this->t('Continue to Payment');
$steps['complete']['next_label'] = $this->t('Donate');
if ($this->order instanceof OrderInterface && $this->order->hasItems()) {
$items = $this->order->getItems();
// Only one donation OrderItem supported.
$donations = array_filter(
$items,
function ($item) {
return $item->bundle() == 'donation';
}
);
$donation = reset($donations);
if ($donation->hasField('field_donation_amount') && !$donation->field_donation_amount->isEmpty()) {
// Append the amount on the payment button text.
$amountFormatOptions = [
'currency_display' => 'symbol',
'minimum_fraction_digits' => 0,
];
$donationAmount = $donation->field_donation_amount->first()->toPrice();
$displayAmount = $this->currencyFormatter->format(
$donationAmount->getNumber(),
$this->getCurrency(),
$amountFormatOptions
);
// Check for monthly.
if (
$donation->hasField('field_gift_type')
&& !$donation->field_gift_type->isEmpty()
&& $donation->field_gift_type->first()->value == 'recurring') {
$displayAmount .= '/mo';
}
$steps['complete']['next_label'] = $this->t(
'Donate @amount',
['@amount' => $displayAmount]
);
}
}
// All steps in the donation flow have no sidebar.
foreach ($steps as &$step) {
$step['has_sidebar'] = false;
}
return $steps;
}
/**
* {@inheritdoc}
*/
public function buildForm(
array $form,
FormStateInterface $form_state,
$step_id = null
) {
$this->order->recalculateTotalPrice();
// Get the basic form.
$form = parent::buildForm($form, $form_state, $step_id);
$form['#attributes']['autocomplete'] = 'off';
// Now build the summaries, if there are any.
if ($form['#step_id'] != 'complete') {
$summarySteps = $this->panesToSummarize($form['#step_id'], $form_state);
$this->buildSummaries($form, $summarySteps);
}
// Identify the step in a class for analytics.
$form['actions']['next']['#attributes']['class'][] = Html::cleanCssIdentifier(
"donation-flow-step--$step_id"
);
$form['#attributes']['class'][] = Html::cleanCssIdentifier(
"donation-flow-step--$step_id"
);
$link_url = $this->configuration['offline_link_url'] ?? null;
if ($link_url && $step_id == 'donation') {
$link_title = $this->configuration['offline_link_title'];
$form['pdf_link'] = [
'#title' => $link_title,
'#type' => 'link',
'#url' => Url::fromUri($link_url),
'#weight' => 200,
'#attributes' => [
'target' => '_blank',
'rel' => 'noopener noreferrer',
'class' => ['pdf-link'],
],
'#theme_wrappers' => ['container'],
];
}
return $form;
}
/**
* Utility method to generate a step url.
*
* Used in this flow in and building the summary.
*
* @param string $step
* The step ID.
*
* @return \Drupal\Core\Url
* The url to the step.
*
* @see \Drupal\commerce_donate\Plugin\Commerce\CheckoutPane\DonationItemPaneBase::doSummaryBuild
*/
public function getStepUrl($step)
{
$options = [];
$query = $this->requestStack->getCurrentRequest()->query;
if ($query->has('donate_return')) {
$returnPath = ['donate_return' => $query->get('donate_return')];
$options = ['query' => $returnPath];
}
return Url::fromRoute(
'commerce_donate.donation_controller_formPage',
[
'commerce_order' => $this->order->id(),
'step' => $step,
],
$options
);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state)
{
// Steps are dynamic based on the submission, so clear to recalculate.
unset($this->visibleSteps);
parent::submitForm($form, $form_state);
// Override the parent redirect data.
$nextStepId = $this->getNextStepId($form['#step_id']);
$form_state->setRedirectUrl($this->getStepUrl($nextStepId));
}
/**
* Sort through panes to populate an array of panes to summarize.
*
* Only panes in visible steps should summarize.
*
* @param string $current_step
* The current step id.
* @param \Drupal\Core\Form\FormStateInterface $formState
* The current form state.
*
* @return \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface[]
* The selected panes.
*/
protected function panesToSummarize(
$current_step,
FormStateInterface $formState
) {
/** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface[] $possiblePanes */
$possiblePanes = array_filter(
$this->getPanes(),
function ($pane) {
return !in_array($pane->getStepId(), ['_sidebar', '_disabled']);
}
);
$visible = $this->getVisibleSteps();
$stepIds = array_keys($visible);
$currentIndex = array_search($current_step, $stepIds);
$indices = array_flip($stepIds);
$stepsCompleted = [];
foreach ($indices as $step => $index) {
if ($index < $currentIndex) {
$stepsCompleted[$step] = true;
}
}
if (isset($indices["complete"])) {
// Don't list the thank you page.
unset($indices["complete"]);
}
$panes = array_filter(
$possiblePanes,
function ($pane) use ($stepsCompleted) {
/** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface $pane */
return isset($stepsCompleted[$pane->getStepId()]);
}
);
$summaries = [];
$step_number = 1;
foreach ($indices as $step => $index) {
if (isset($stepsCompleted[$step])) {
$summaries[$step] = [
'index' => $step_number,
'label' => $visible[$step]['label'],
'url' => $this->getStepUrl($step),
'panes' => [],
];
}
++$step_number;
}
foreach ($panes as $pane) {
$summaries[$pane->getStepId()]['panes'][] = $pane;
}
return $summaries;
}
/**
* Helper method to build the full summary display and complete overview.
*
* @param array $form
* The form array being built.
* @param array $summarySteps
* Array with summary data.
*/
protected function buildSummaries(array &$form, array $summarySteps)
{
$form['summaries'] = [
'#type' => 'container',
'#weight' => -80,
'#attributes' => [
'class' => ['donation-summaries'],
],
];
// Adapted from Review::buildPaneForm.
foreach ($summarySteps as $summaryStep => $data) {
$form['summaries'][$summaryStep] = [
'#type' => 'container',
'#attributes' => [
'class' => ['donation-summary-step'],
],
'summaries' => [],
'edit_link' => [
'#title' => $this->t('Edit'),
'#type' => 'link',
'#url' => $data['url'],
'#attributes' => [
'class' => [
'summary-edit',
'edit-donation',
],
],
],
];
if (isset($data['panes'])) {
/** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface $pane */
foreach ($data['panes'] as $pane) {
$form['summaries'][$summaryStep]['summaries'][] = $pane->buildPaneSummary(
);
}
}
}
}
/**
* Helper function to set the order object if it has not been set before.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
*/
public function conditionalOrderValue(OrderInterface $order)
{
if (!$this->order instanceof OrderInterface) {
$this->order = $order;
}
}
/**
* Helper function to obtain the current currency code.
*
* @return string
*/
protected function getCurrency() {
if (!empty($this->currency)) {
return $this->currency;
}
if ($this->order instanceof OrderInterface) {
$this->currency = $this->order->getStore()
->getDefaultCurrencyCode();
return $this->currency;
}
return '';
}
}
Plugins within Plugins
namespace Drupal\commerce_donate\Plugin\Commerce\CheckoutPane;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A custom pane for creating Donation OrderItems.
*
* @CommerceCheckoutPane(
* id = "commerce_donation_pane",
* label = @Translation("Donation"),
* display_label = @Translation("Your Donation"),
* default_step = "donation",
* wrapper_element = "fieldset",
* )
*/
class DonationCheckoutPane extends DonationItemPaneBase implements CheckoutPaneInterface, ContainerFactoryPluginInterface {
/**
* Is commerce_recurring available?
*
* Flag for monthly donation elements.
*
* @var bool
*/
protected $hasRecurring;
/**
* {@inheritdoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
CheckoutFlowInterface $checkout_flow = NULL
) {
$instance = parent::create(
$container,
$configuration,
$plugin_id,
$plugin_definition,
$checkout_flow
);
$instance->setFormDisplay('donation');
$instance->hasRecurring = $instance->moduleHandler->moduleExists('commerce_recurring');
return $instance;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$config = parent::defaultConfiguration();
$config['message'] = $this->hasRecurring ? 'Your first donation will be charged today and then monthly starting on' : '';
return $config;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationSummary() {
$summary = [];
if (!empty($this->configuration['message'])) {
$summary[] = $this->t(
'Monthly giving message: @message',
['@message' => $this->configuration['message']]
);
}
return implode(', ', $summary);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(
array $form,
FormStateInterface $form_state
) {
$form = parent::buildConfigurationForm($form, $form_state);
if ($this->hasRecurring) {
$message = $this->configuration['message'];
$form['message'] = [
'#type' => 'textfield',
'#title' => $this
->t('Monthly giving message'),
'#default_value' => empty($message) ? '' : $message,
'#description' => $this->t(
'The date one month ahead will be appended to this string'
),
'#size' => 128,
'#maxlength' => 128,
'#required' => TRUE,
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(
array &$form,
FormStateInterface $form_state
) {
parent::submitConfigurationForm($form, $form_state);
if (!$form_state->getErrors()) {
$values = $form_state->getValue($form['#parents']);
$this->configuration['message'] = $values['message'];
}
}
/**
* {@inheritdoc}
*/
public function buildPaneForm(
array $pane_form,
FormStateInterface $form_state,
array &$complete_form
) {
$class = get_class($this);
$pane_form = parent::buildPaneForm($pane_form, $form_state, $complete_form);
// Add ajax to the gift type.
if (isset($pane_form['field_gift_type']['widget'])) {
$pane_form['field_gift_type']['widget']['#ajax'] = [
'callback' => [$class, 'ajaxAmountUpdate'],
'progress' => 'none',
];
}
// Block progress if there is no javascript.
$complete_form["actions"]['#attributes']['class'] = $complete_form["actions"]['#attributes']['class'] ?? [];
$pane_form['#attributes']['class'] = $pane_form['#attributes']['class'] ?? [];
$complete_form["actions"]['#attributes']['class'][] = 'js-show';
$pane_form['#attributes']['class'][] = 'js-show';
return $pane_form;
}
/**
* An ajax responder for gift type change.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $formState
* The current form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The ajax response.
*/
public static function ajaxAmountUpdate(
array $form,
FormStateInterface $formState
) {
$response = new AjaxResponse();
$response->addCommand(
new ReplaceCommand(
'fieldset[data-donation-level="fieldset"]',
$form['commerce_donation_pane']['field_donation_amount']['widget'][0]['donation_level']
)
);
return $response;
}
/**
* {@inheritdoc}
*/
public function buildPaneSummary() {
$viewMode = 'donation';
return $this->doSummaryBuild($viewMode);
}
}
See it in action: Live Demo!
“Heeded my words not, did you? Pass on what you have learned. Strength, mastery.”
“But weakness, folly, failure also. Yes, failure most of all. The greatest teacher, failure is.”
Photo Credits